diff --git a/tools/sign_firmware.py b/tools/sign_firmware.py new file mode 100644 index 0000000..2556028 --- /dev/null +++ b/tools/sign_firmware.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Sign a firmware binary with ECDSA P-256. Outputs a raw 64-byte r||s .sig file.""" +import argparse +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: + return serialization.load_pem_private_key(key_path.read_bytes(), password=None) + + +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) + 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") + + sig = sign_firmware(firmware, key_path) + out_path.write_bytes(sig) + print(f"Signed {firmware.name} → {out_path} ({len(sig)} bytes)") + + +if __name__ == "__main__": + main() diff --git a/tools/test_sign_firmware.py b/tools/test_sign_firmware.py new file mode 100644 index 0000000..987131d --- /dev/null +++ b/tools/test_sign_firmware.py @@ -0,0 +1,63 @@ +import os, sys, tempfile +from pathlib import Path + +import pytest +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + +REPO_ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(REPO_ROOT / "tools")) +from sign_firmware import sign_firmware, load_private_key + + +@pytest.fixture() +def keypair(tmp_path): + key = ec.generate_private_key(ec.SECP256R1()) + pem_path = tmp_path / "key.pem" + pem_path.write_bytes(key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + )) + return key, pem_path + + +def test_signature_is_64_bytes(keypair, tmp_path): + key, key_path = keypair + firmware = tmp_path / "fw.bin" + firmware.write_bytes(b"fake firmware data" * 100) + sig = sign_firmware(firmware, key_path) + assert len(sig) == 64 + + +def test_signature_verifies(keypair, tmp_path): + key, key_path = keypair + data = b"test firmware payload" + firmware = tmp_path / "fw.bin" + firmware.write_bytes(data) + sig_raw = sign_firmware(firmware, key_path) + + # Convert raw r||s back to DER for cryptography lib verify + r = int.from_bytes(sig_raw[:32], 'big') + s = int.from_bytes(sig_raw[32:], 'big') + from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature + sig_der = encode_dss_signature(r, s) + + key.public_key().verify(sig_der, data, ec.ECDSA(hashes.SHA256())) + + +def test_wrong_key_fails_verification(keypair, tmp_path): + key, key_path = keypair + firmware = tmp_path / "fw.bin" + firmware.write_bytes(b"firmware") + sig_raw = sign_firmware(firmware, key_path) + + other_key = ec.generate_private_key(ec.SECP256R1()) + r = int.from_bytes(sig_raw[:32], 'big') + s = int.from_bytes(sig_raw[32:], 'big') + from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature + sig_der = encode_dss_signature(r, s) + + with pytest.raises(Exception): + other_key.public_key().verify(sig_der, b"firmware", ec.ECDSA(hashes.SHA256()))