Files
MicrOBU/microbu-esp32c5/pki/uml-l0-rca/reference-generator/rebuild_registered_rca.py
T
Ashin Walpola 0e9525162d 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).
2026-09-23 17:46:40 +02:00

169 lines
6.1 KiB
Python

#!/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()