#!/usr/bin/env python3 """Generate ECDSA P-256 signing keypair for OTA firmware verification.""" import argparse import os from pathlib import Path from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import serialization def generate(secrets_dir: Path, header_out: Path) -> None: secrets_dir.mkdir(parents=True, exist_ok=True) key = ec.generate_private_key(ec.SECP256R1()) pem = key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ) key_path = secrets_dir / "firmware_signing_key.pem" key_path.write_bytes(pem) key_path.chmod(0o600) pub_bytes = key.public_key().public_bytes( encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint, ) assert len(pub_bytes) == 65 and pub_bytes[0] == 0x04 hex_values = ", ".join(f"0x{b:02x}" for b in pub_bytes) header = ( "#pragma once\n" "// Auto-generated by tools/gen_signing_key.py — DO NOT EDIT\n" "// ECDSA P-256 public key, uncompressed X9.62 (04 || X || Y)\n" f"static const uint8_t kOtaPublicKey[65] = {{{hex_values}}};\n" ) header_out.parent.mkdir(parents=True, exist_ok=True) header_out.write_text(header) print(f"Private key → {key_path}") print(f"Public key header → {header_out}") def main() -> None: p = argparse.ArgumentParser(description=__doc__) p.add_argument("--secrets-dir", default="secrets", help="Directory for private key (default: secrets/)") p.add_argument("--header-out", default="firmware/lib/ota_updater/ota_pubkey.h", help="Path to write the C header") args = p.parse_args() generate(Path(args.secrets_dir), Path(args.header_out)) if __name__ == "__main__": main()