#!/usr/bin/env python3 """Sign a firmware binary with ECDSA P-256. Outputs a raw 64-byte r||s .sig file.""" import argparse import sys from pathlib import Path from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature def load_private_key(key_path: Path) -> ec.EllipticCurvePrivateKey: key = serialization.load_pem_private_key(key_path.read_bytes(), password=None) if not isinstance(key, ec.EllipticCurvePrivateKey): raise ValueError("Key must be an EC private key") if not isinstance(key.curve, ec.SECP256R1): raise ValueError(f"Key must use SECP256R1 curve, got {key.curve.name}") return key def sign_firmware(firmware_path: Path, key_path: Path) -> bytes: key = load_private_key(key_path) data = firmware_path.read_bytes() sig_der = key.sign(data, ec.ECDSA(hashes.SHA256())) r, s = decode_dss_signature(sig_der) # Returns raw 64-byte r‖s (not DER) — mbedtls_ecdsa_verify expects this layout return r.to_bytes(32, 'big') + s.to_bytes(32, 'big') def main() -> None: p = argparse.ArgumentParser(description=__doc__) p.add_argument("firmware", help="Path to firmware .bin") p.add_argument("--key", default="secrets/firmware_signing_key.pem", help="Path to PEM private key") p.add_argument("--out", help="Output .sig path (default: firmware.bin.sig)") args = p.parse_args() firmware = Path(args.firmware) key_path = Path(args.key) out_path = Path(args.out) if args.out else firmware.with_suffix(".bin.sig") try: sig = sign_firmware(firmware, key_path) except (FileNotFoundError, ValueError) as e: print(f"Error: {e}", file=sys.stderr) raise SystemExit(1) out_path.write_bytes(sig) print(f"Signed {firmware.name} → {out_path} ({len(sig)} bytes)") if __name__ == "__main__": main()