feat(tools): add ECDSA P-256 firmware signing tool
This commit is contained in:
41
tools/sign_firmware.py
Normal file
41
tools/sign_firmware.py
Normal file
@@ -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()
|
||||||
63
tools/test_sign_firmware.py
Normal file
63
tools/test_sign_firmware.py
Normal file
@@ -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()))
|
||||||
Reference in New Issue
Block a user