#!/usr/bin/env python3 """ Create a separate NEW UML L0 RCA keypair and candidate certificate using the exact TheEnbyperor/c-its reference commit e3bb3b82480d6df4237e2a8c35ea0dd7eade25b4. THIS DOES NOT REPLACE OR RECREATE THE EU-REGISTERED ROOT. The result is not trusted or registered until it completes a deliberate registration/re-key process. The new private key never leaves this machine and is permanently stored only as encrypted PKCS#8 PEM. A 32-byte private scalar is passed to the already-built local Rust process over stdin and is never written to disk unencrypted. """ from __future__ import annotations import getpass import hashlib import os from pathlib import Path import re import shutil import subprocess import sys import tarfile import zipfile from datetime import datetime UPSTREAM_COMMIT = "e3bb3b82480d6df4237e2a8c35ea0dd7eade25b4" TEMPLATE_SHA256 = "7168E341764188E140EF591634A9D2488D7FDACDA667EE53AFD566A8034ED5DB" CERTIFICATE_ID = "0_Urban-Mobility-Lab-Root-CA_L0" HERE = Path(__file__).resolve().parent ROOT = HERE.parent TEMPLATE = ROOT / "rca" / "AFD566A8034ED5DB.oer" PATCH_SOURCE = HERE / "src" / "create_new_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" def die(msg: str) -> None: raise SystemExit("\nERROR: " + msg) def run(cmd, *, cwd=None, input_bytes=None, capture=False, env=None): print("+", " ".join(str(x) for x in cmd)) return subprocess.run( cmd, cwd=cwd, input=input_bytes, check=True, text=False, capture_output=capture, env=env, ) def require_program(name: str) -> None: if shutil.which(name) is None: die( f"'{name}' is not installed or not on PATH. " "Install Git and the Rust toolchain (cargo/rustc) first. " "On Windows, WSL2 Ubuntu is a good fallback if native compilation causes problems." ) def validate_submission_date(s: str) -> str: try: datetime.strptime(s, "%Y-%m-%d") except ValueError: die("Submission date must be YYYY-MM-DD.") return s def prepare_upstream() -> None: """Create a disposable build tree from the bundled pinned source.""" 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) # Replace only the root-generator utility. Security/crypto/ASN.1 implementation # remains the pinned upstream commit. shutil.copy2(PATCH_SOURCE, UPSTREAM_DIR / "src" / "util" / "generate_root.rs") shutil.copy2(TEMPLATE, UPSTREAM_DIR / TEMPLATE.name) # Host-build fix for Windows/native desktop targets: # ieee80211 pulls embedded defmt support through default features, but a # normal host executable has no defmt transport/logger providing symbols # such as _defmt_panic, _defmt_acquire and _defmt_write. cargo_toml = UPSTREAM_DIR / "Cargo.toml" cargo_text = cargo_toml.read_text(encoding="utf-8") original_ieee = 'ieee80211 = "0.5.9"' patched_ieee = 'ieee80211 = { version = "0.5.9", default-features = false }' if original_ieee in cargo_text: cargo_text = cargo_text.replace(original_ieee, patched_ieee, 1) cargo_toml.write_text(cargo_text, encoding="utf-8") print("Applied host-build patch: ieee80211 default features disabled (defmt removed).") elif patched_ieee in cargo_text: print("Host-build patch already present.") else: die("Expected ieee80211 dependency line not found in pinned Cargo.toml.") 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: print("\nBuilding the pinned c-its generator BEFORE generating any private key...") try: run( [ "cargo", "build", "--offline", "--release", "--features", "build-binary", "--bin", "c-its-generate-root", ], cwd=UPSTREAM_DIR, ) except subprocess.CalledProcessError as exc: die( "The pinned c-its host build failed. No RCA private key has been generated. " "If the remaining linker error still mentions _defmt_*, delete the package's " "_upstream_cits directory and rerun this V2 script. " f"Cargo exit code: {exc.returncode}" ) exe = UPSTREAM_DIR / "target" / "release" / ( "c-its-generate-root.exe" if os.name == "nt" else "c-its-generate-root" ) if not exe.exists(): die(f"Build reported success but executable is missing: {exe}") return exe def generate_keypair_and_certificate(exe: Path) -> Path: try: from cryptography.hazmat.primitives.asymmetric import ec, utils from cryptography.hazmat.primitives import hashes, serialization except ImportError: die("Python package 'cryptography' is required. Run: python -m pip install cryptography") submission_date = validate_submission_date( input("\nActual submission date (YYYY-MM-DD): ").strip() ) while True: pw1 = getpass.getpass("Choose NEW RCA private-key passphrase: ") pw2 = getpass.getpass("Repeat passphrase: ") if pw1 != pw2: print("Passphrases do not match. Try again.") continue if len(pw1) < 12: print("Use at least 12 characters.") continue break stamp = datetime.now().strftime("%Y%m%d_%H%M%S") root = HERE / f"UML_L0_RCA_NEW_{stamp}" submit = root / "CANDIDATE_SUBMISSION_NOT_REGISTERED" private_dir = root / "PRIVATE_DO_NOT_SHARE" public_dir = root / "PUBLIC_AND_VALIDATION" rust_out = root / "_rust_output" for d in (submit, private_dir, public_dir, rust_out): d.mkdir(parents=True, exist_ok=False) print("\nGenerating a fresh NIST P-256 keypair locally...") key = ec.generate_private_key(ec.SECP256R1()) encrypted_pem = key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.BestAvailableEncryption(pw1.encode("utf-8")), ) public_pem = key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) private_path = private_dir / "UML_L0_RCA_private_encrypted.pem" public_path = public_dir / "UML_L0_RCA_public.pem" private_path.write_bytes(encrypted_pem) public_path.write_bytes(public_pem) try: private_path.chmod(0o600) except OSError: pass # The scalar is kept in RAM and sent only over stdin to the local Rust binary. scalar = bytearray(key.private_numbers().private_value.to_bytes(32, "big")) env = os.environ.copy() env["UML_TEMPLATE"] = str(UPSTREAM_DIR / TEMPLATE.name) env["UML_OUTPUT_DIR"] = str(rust_out) try: proc = run( [str(exe)], cwd=UPSTREAM_DIR, input_bytes=(bytes(scalar).hex().upper() + "\n").encode("ascii"), capture=True, env=env, ) finally: for i in range(len(scalar)): scalar[i] = 0 stdout = proc.stdout.decode("utf-8", errors="replace") stderr = proc.stderr.decode("utf-8", errors="replace") print(stdout) if stderr.strip(): print(stderr, file=sys.stderr) m = re.search(r"^RESULT_ID8=([0-9A-F]{16})$", stdout, re.MULTILINE) if not m: die("Rust generator succeeded but RESULT_ID8 was not found.") id8 = m.group(1) oer_src = rust_out / f"{id8}.oer" if not oer_src.exists(): die(f"Expected certificate not found: {oer_src}") cert_bytes = oer_src.read_bytes() cert_sha256 = hashlib.sha256(cert_bytes).hexdigest().upper() if cert_sha256[-16:] != id8: die("Independent Python HashedID8 check failed.") # Independently reconstruct the IEEE/ETSI self-signed prehash from the TBS # emitted by rasn and verify the c-its ECDSA signature with Python cryptography. details = {} for line in (rust_out / "signing_details.txt").read_text(encoding="utf-8").splitlines(): if "=" in line: k, v = line.split("=", 1) details[k] = v tbs = (rust_out / "tbs.oer").read_bytes() h_tbs = hashlib.sha256(tbs).digest() h_empty = hashlib.sha256(b"").digest() prehash = hashlib.sha256(h_tbs + h_empty).digest() if details.get("TBS_SHA256") != h_tbs.hex().upper(): die("Independent TBS hash disagrees with Rust output.") if details.get("SIGNING_PREHASH_SHA256") != prehash.hex().upper(): die("Independent IEEE/ETSI signing prehash disagrees with Rust output.") r = int(details["ECDSA_R"], 16) s = int(details["ECDSA_S"], 16) der_sig = utils.encode_dss_signature(r, s) try: key.public_key().verify( der_sig, prehash, ec.ECDSA(utils.Prehashed(hashes.SHA256())), ) except Exception as exc: die(f"Independent Python ECDSA verification FAILED: {exc}") # Copy only the public submission certificate. oer_submit = submit / f"{id8}.oer" shutil.copy2(oer_src, oer_submit) txt_name = f"{id8}_{CERTIFICATE_ID}_{submission_date}.txt" txt_submit = submit / txt_name txt_submit.write_text(f"""EU CCMS CPOC L0 ECTL — RCA descriptive information =================================================== Full Company Name / RCA Operator: Hochschule für Angewandte Wissenschaften Hamburg (HAW Hamburg) Urban Mobility Lab RCA Name: Urban Mobility Lab L0 Root CA RCA Distribution Centre URL: https://cits-dc.uml-hamburg.de Contact: uml-haw@proton.me CertificateID: {CERTIFICATE_ID} Certificate HashedID8: {id8} CertificateID / acronym explanation: - 0: CPA-ID reserved for the L0 environment. - Urban-Mobility-Lab-Root-CA: Root CA operated for the Urban Mobility Lab at HAW Hamburg. - L0: EU CCMS L0 research/test environment. Purpose of testing: The RCA is used for research, field testing and proof-of-concept activities of the Urban Mobility Lab in the context of the micrOBU project. micrOBU investigates the integration of vulnerable road users into C-ITS through development of a lightweight, compact and low-cost VRU ITS-S using an MCU + mobile-phone architecture. Contextual constraints / deviations: - This is a candidate new RCA. It is not registered and must not be deployed until the EU CCMS CPOC has completed the intended registration or re-key procedure. - The implementation, including the C-ITS protocol stack, signing functionality and VRU Awareness Basic Service, is under active development. Full implementation conformity is not claimed. - The declared RCA Distribution Centre is the intended permanent DC URL. At the time of this submission, the cits-dc.uml-hamburg.de subdomain and the required /getctl and /getcrl endpoints are still being commissioned and are not yet operational. - The RCA intentionally includes ITS-AID 638 (VRU Awareness basic service) with bitmap SSP range 01/FF for L0 VAM/VBS research. ITS-AID 638 is a standardized ETSI ITS-AID but is not contained in the current CPOC Protocol Release 3.3 Table 4 list; its inclusion is therefore an intentional L0 profile deviation. - The RCA private key is generated and maintained locally as an encrypted PKCS#8 NIST P-256 key for this L0 research environment. This software-based key storage is not claimed to satisfy production/L2 cryptographic-module requirements. Re-key / revocation / relationship to existing certificates: Candidate new RCA. Coordinate its relationship to existing registrations with the EU CCMS CPOC before submission or use. Submission date: {submission_date} """, encoding="utf-8") # Preserve public validation evidence. shutil.copy2(rust_out / "certificate_report.json", public_dir / "certificate_report.json") shutil.copy2(rust_out / "signing_details.txt", public_dir / "signing_details.txt") shutil.copy2(rust_out / "tbs.oer", public_dir / "tbs.oer") spki_der = key.public_key().public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) spki_sha256 = hashlib.sha256(spki_der).hexdigest().upper() manifest = f"""UML L0 RCA generation manifest ============================= Reference implementation: https://github.com/TheEnbyperor/c-its Pinned commit: {UPSTREAM_COMMIT} Reference root-generator behavior: - rasn::oer encoding - c_its::security::crypto::PrivateKey::sign() - self-signed signer-certificate input = empty byte string - NIST P-256 / SHA-256 Template certificate SHA-256: {TEMPLATE_SHA256} NEW certificate: {id8}.oer Certificate SHA-256: {cert_sha256} HashedID8: {id8} New public-key SPKI SHA-256: {spki_sha256} Validation: PASS - c-its parsed final certificate PASS - c-its self-signature verification PASS - c-its parse/re-encode stability PASS - Python independently recomputed HashedID8 PASS - Python independently recomputed IEEE/ETSI signing prehash PASS - Python cryptography independently verified ECDSA signature over that prehash PRIVATE KEY: {private_path.name} Encrypted PKCS#8 PEM. DO NOT SUBMIT OR UPLOAD. """ (public_dir / "generation_manifest.txt").write_text(manifest, encoding="utf-8") # Remove duplicate Rust certificate; the canonical submission copy is in SUBMIT. shutil.rmtree(rust_out) print("\nSUCCESS") print("A NEW, UNREGISTERED keypair and candidate certificate were generated locally.") print("Do not deploy it as the registered root. EU registration/re-keying is required.") print(f"\nCANDIDATE SUBMISSION FILES (NOT REGISTERED):\n {oer_submit}\n {txt_submit}") print(f"\nKEEP PRIVATE:\n {private_path}") print(f"\nSAFE PUBLIC/VALIDATION FILES:\n {public_dir}") return root def main() -> None: require_program("cargo") if not TEMPLATE.exists(): die(f"Missing bundled template: {TEMPLATE}") got = hashlib.sha256(TEMPLATE.read_bytes()).hexdigest().upper() if got != TEMPLATE_SHA256: die(f"Bundled template hash mismatch: {got}") prepare_upstream() exe = build_generator() root = generate_keypair_and_certificate(exe) print(f"\nResult folder: {root}") if __name__ == "__main__": main()