Keep the colleague's microbu-esp32c5 tree in this repository
obu-firmware builds against vanetza-idf from microbu-esp32c5/external, but that tree was gitignored, so a clone of this repository could not build the firmware it ships. It is now committed here as ordinary files in its own folder, microbu-esp32c5/: the colleague's commit cf4b99f plus the V2X2MAP bridge's signature verification (--trust) used on the bench. Nothing is fetched from or pushed to the colleague's repository; this repository and its remotes carry everything. The folder's own .gitignore keeps build output, downloaded components and private key material out, as it did there; the committed file set is identical to that repository's tracked files. The ESP32-C5 is still flashed from obu-firmware/, which only takes vanetza-idf from microbu-esp32c5/, so the two stay separate folders. FLASHING.md says how to take a newer version of the colleague's tree (copy it over the folder, rebuild, test, commit).
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild and verify the registered UML L0 RCA with its existing private key."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import getpass
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
|
||||
UPSTREAM_COMMIT = "e3bb3b82480d6df4237e2a8c35ea0dd7eade25b4"
|
||||
REGISTERED_SHA256 = "7168E341764188E140EF591634A9D2488D7FDACDA667EE53AFD566A8034ED5DB"
|
||||
REGISTERED_ID8 = "AFD566A8034ED5DB"
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent
|
||||
REGISTERED_CERT = ROOT / "rca" / f"{REGISTERED_ID8}.oer"
|
||||
REGISTERED_PUBLIC_KEY = ROOT / "rca" / "UML_L0_RCA_public.pem"
|
||||
REGISTERED_PRIVATE_KEY = ROOT / "private" / "UML_L0_RCA_private_encrypted.pem"
|
||||
PATCH_SOURCE = HERE / "src" / "generate_root.rs"
|
||||
BUNDLED_SOURCE = HERE / "vendor" / "c-its-source-e3bb3b8.zip"
|
||||
VENDORED_CRATES = HERE / "vendor" / "cargo-crates.tar.gz"
|
||||
UPSTREAM_DIR = HERE / "_build_cits"
|
||||
CARGO_VENDOR_DIR = HERE / "_cargo_vendor"
|
||||
OUTPUT_DIR = HERE / "rebuild-output"
|
||||
TEMPLATE_NAME = "registered_rca.oer"
|
||||
|
||||
|
||||
def die(message: str) -> None:
|
||||
raise SystemExit(f"\nERROR: {message}")
|
||||
|
||||
|
||||
def run(command: list[str], *, cwd: Path, input_bytes: bytes | None = None,
|
||||
capture: bool = False, env: dict[str, str] | None = None):
|
||||
print("+", " ".join(command))
|
||||
return subprocess.run(
|
||||
command, cwd=cwd, input=input_bytes, check=True, capture_output=capture, env=env
|
||||
)
|
||||
|
||||
|
||||
def require_program(name: str) -> None:
|
||||
if shutil.which(name) is None:
|
||||
die(f"'{name}' is required and was not found on PATH.")
|
||||
|
||||
|
||||
def verify_inputs() -> None:
|
||||
for path in (REGISTERED_CERT, REGISTERED_PUBLIC_KEY, REGISTERED_PRIVATE_KEY, PATCH_SOURCE):
|
||||
if not path.is_file():
|
||||
die(f"Required file is missing: {path}")
|
||||
actual = hashlib.sha256(REGISTERED_CERT.read_bytes()).hexdigest().upper()
|
||||
if actual != REGISTERED_SHA256:
|
||||
die(f"Registered certificate hash mismatch: {actual}")
|
||||
|
||||
|
||||
def prepare_upstream() -> None:
|
||||
if not BUNDLED_SOURCE.is_file() or not VENDORED_CRATES.is_file():
|
||||
die("Bundled c-its source or Cargo crate archive is missing.")
|
||||
if UPSTREAM_DIR.exists():
|
||||
shutil.rmtree(UPSTREAM_DIR)
|
||||
if CARGO_VENDOR_DIR.exists():
|
||||
shutil.rmtree(CARGO_VENDOR_DIR)
|
||||
UPSTREAM_DIR.mkdir()
|
||||
CARGO_VENDOR_DIR.mkdir()
|
||||
with zipfile.ZipFile(BUNDLED_SOURCE) as archive:
|
||||
archive.extractall(UPSTREAM_DIR)
|
||||
with tarfile.open(VENDORED_CRATES, "r:gz") as archive:
|
||||
archive.extractall(CARGO_VENDOR_DIR)
|
||||
|
||||
shutil.copy2(PATCH_SOURCE, UPSTREAM_DIR / "src" / "util" / "generate_root.rs")
|
||||
shutil.copy2(REGISTERED_CERT, UPSTREAM_DIR / TEMPLATE_NAME)
|
||||
|
||||
cargo_toml = UPSTREAM_DIR / "Cargo.toml"
|
||||
text = cargo_toml.read_text(encoding="utf-8")
|
||||
old = 'ieee80211 = "0.5.9"'
|
||||
new = 'ieee80211 = { version = "0.5.9", default-features = false }'
|
||||
if old in text:
|
||||
cargo_toml.write_text(text.replace(old, new, 1), encoding="utf-8")
|
||||
elif new not in text:
|
||||
die("Expected ieee80211 dependency was not found in the pinned source.")
|
||||
|
||||
cargo_config = UPSTREAM_DIR / ".cargo" / "config.toml"
|
||||
cargo_config.parent.mkdir()
|
||||
cargo_config.write_text(
|
||||
'[source.crates-io]\nreplace-with = "vendored-sources"\n\n'
|
||||
'[source.vendored-sources]\ndirectory = "../_cargo_vendor"\n\n'
|
||||
'[net]\noffline = true\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def build_generator() -> Path:
|
||||
run(
|
||||
["cargo", "build", "--offline", "--release", "--features", "build-binary", "--bin", "c-its-generate-root"],
|
||||
cwd=UPSTREAM_DIR,
|
||||
)
|
||||
executable = UPSTREAM_DIR / "target" / "release" / (
|
||||
"c-its-generate-root.exe" if os.name == "nt" else "c-its-generate-root"
|
||||
)
|
||||
if not executable.is_file():
|
||||
die(f"Generator executable is missing: {executable}")
|
||||
return executable
|
||||
|
||||
|
||||
def load_registered_scalar() -> bytearray:
|
||||
try:
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
except ImportError:
|
||||
die("Install the Python package 'cryptography'.")
|
||||
|
||||
password = getpass.getpass("Registered RCA private-key passphrase: ").encode("utf-8")
|
||||
try:
|
||||
key = serialization.load_pem_private_key(REGISTERED_PRIVATE_KEY.read_bytes(), password=password)
|
||||
except Exception as exc:
|
||||
die(f"Could not unlock the registered private key: {exc}")
|
||||
|
||||
if not isinstance(key, ec.EllipticCurvePrivateKey) or not isinstance(key.curve, ec.SECP256R1):
|
||||
die("Registered key is not NIST P-256.")
|
||||
expected_public = serialization.load_pem_public_key(REGISTERED_PUBLIC_KEY.read_bytes())
|
||||
if key.public_key().public_numbers() != expected_public.public_numbers():
|
||||
die("Private key does not match the registered public key.")
|
||||
return bytearray(key.private_numbers().private_value.to_bytes(32, "big"))
|
||||
|
||||
|
||||
def rebuild(executable: Path) -> None:
|
||||
scalar = load_registered_scalar()
|
||||
if OUTPUT_DIR.exists():
|
||||
shutil.rmtree(OUTPUT_DIR)
|
||||
OUTPUT_DIR.mkdir()
|
||||
|
||||
env = os.environ.copy()
|
||||
env["UML_TEMPLATE"] = str(UPSTREAM_DIR / TEMPLATE_NAME)
|
||||
env["UML_OUTPUT_DIR"] = str(OUTPUT_DIR)
|
||||
try:
|
||||
result = run(
|
||||
[str(executable)], cwd=UPSTREAM_DIR,
|
||||
input_bytes=(bytes(scalar).hex().upper() + "\n").encode("ascii"),
|
||||
capture=True, env=env,
|
||||
)
|
||||
finally:
|
||||
for index in range(len(scalar)):
|
||||
scalar[index] = 0
|
||||
|
||||
sys.stdout.write(result.stdout.decode("utf-8", errors="replace"))
|
||||
sys.stderr.write(result.stderr.decode("utf-8", errors="replace"))
|
||||
rebuilt = OUTPUT_DIR / f"{REGISTERED_ID8}.oer"
|
||||
if not rebuilt.is_file():
|
||||
die(f"Expected rebuilt certificate is missing: {rebuilt}")
|
||||
if rebuilt.read_bytes() != REGISTERED_CERT.read_bytes():
|
||||
die("Rebuilt certificate is not byte-for-byte identical to the EU-registered certificate.")
|
||||
print("\nPASS: rebuilt certificate exactly matches the registered certificate.")
|
||||
print(f"Validation output: {OUTPUT_DIR}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
require_program("cargo")
|
||||
verify_inputs()
|
||||
prepare_upstream()
|
||||
rebuild(build_generator())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user