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).
169 lines
6.1 KiB
Python
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()
|