feat(tools): Signierwerkzeug fuer Migrationsimages
nl_sign.py signiert/prueft W600-Images im RSA-4096-PKCS#1-v1.5-Format der alten Klyqa-Firmware (Check_OTA_Image() in qc_ssl_layer.c): SHA256 ueber die gesamte Datei, angehaengt als exakt 512 Byte - derselbe Block wie der ninlight-Trailer, daher die Ablehnung bereits getrailerter Eingaben. Unterstuetzt sowohl cryptography als auch pycryptodome/pycryptodomex als Backend. Gegen das echte Klyqa-Schluesselpaar aus ~/workspace/fw-sign bitgleich verifiziert. tools/test_host.sh haengt jetzt die Python-Tests aus tools/mkimage/tests und tools/station/tests an ctest an.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
TOOL = HERE.parent / "nl_sign.py"
|
||||
sys.path.insert(0, str(HERE.parent))
|
||||
|
||||
import nl_sign # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def key():
|
||||
"""Ein eigener 4096-Bit-Schluessel, damit kein echter Signierschluessel in die Tests
|
||||
gerät. Einmal pro Modul, weil das Erzeugen ein paar Sekunden dauert."""
|
||||
return nl_sign.generate_test_key()
|
||||
|
||||
|
||||
def test_signature_is_512_bytes(key, tmp_path):
|
||||
raw = b"\xa5" * 4096
|
||||
signed = nl_sign.sign_image(raw, key.private_pem)
|
||||
assert len(signed) == len(raw) + 512
|
||||
assert signed[: len(raw)] == raw
|
||||
|
||||
|
||||
def test_signature_verifies(key):
|
||||
raw = b"firmware" * 512
|
||||
signed = nl_sign.sign_image(raw, key.private_pem)
|
||||
assert nl_sign.verify_image(signed, key.public_der) is True
|
||||
|
||||
|
||||
def test_modified_payload_fails_verification(key):
|
||||
raw = bytearray(b"firmware" * 512)
|
||||
signed = bytearray(nl_sign.sign_image(bytes(raw), key.private_pem))
|
||||
signed[17] ^= 0xFF
|
||||
assert nl_sign.verify_image(bytes(signed), key.public_der) is False
|
||||
|
||||
|
||||
def test_signing_is_deterministic(key):
|
||||
raw = b"x" * 1024
|
||||
assert nl_sign.sign_image(raw, key.private_pem) == nl_sign.sign_image(raw, key.private_pem)
|
||||
|
||||
|
||||
def test_cli_roundtrip(key, tmp_path):
|
||||
src = tmp_path / "in_gz.img"
|
||||
dst = tmp_path / "out.img"
|
||||
pub = tmp_path / "pub.der"
|
||||
priv = tmp_path / "key.pem"
|
||||
src.write_bytes(b"\x01\x02\x03" * 1000)
|
||||
priv.write_bytes(key.private_pem)
|
||||
pub.write_bytes(key.public_der)
|
||||
subprocess.run([sys.executable, str(TOOL), "sign", "--key", str(priv),
|
||||
"--in", str(src), "--out", str(dst)], check=True)
|
||||
assert dst.stat().st_size == src.stat().st_size + 512
|
||||
subprocess.run([sys.executable, str(TOOL), "verify", "--pub", str(pub),
|
||||
"--in", str(dst)], check=True)
|
||||
|
||||
|
||||
def test_cli_verify_rejects_tampered(key, tmp_path):
|
||||
src = tmp_path / "in_gz.img"
|
||||
dst = tmp_path / "out.img"
|
||||
pub = tmp_path / "pub.der"
|
||||
priv = tmp_path / "key.pem"
|
||||
src.write_bytes(b"\x07" * 2000)
|
||||
priv.write_bytes(key.private_pem)
|
||||
pub.write_bytes(key.public_der)
|
||||
subprocess.run([sys.executable, str(TOOL), "sign", "--key", str(priv),
|
||||
"--in", str(src), "--out", str(dst)], check=True)
|
||||
data = bytearray(dst.read_bytes())
|
||||
data[0] ^= 0x01
|
||||
dst.write_bytes(bytes(data))
|
||||
r = subprocess.run([sys.executable, str(TOOL), "verify", "--pub", str(pub),
|
||||
"--in", str(dst)])
|
||||
assert r.returncode != 0
|
||||
|
||||
|
||||
def test_refuses_already_signed_looking_input(key, tmp_path):
|
||||
"""Ein Image mit ninlight-Trailer ist ein OTA-Image, kein Rohbild - zweimal 512 Byte
|
||||
anzuhaengen ergaebe eine Datei, die die alte Firmware ablehnt."""
|
||||
raw = b"\x00" * 1024 + b"NINL" + b"\x01" + b"\x00" * 507
|
||||
with pytest.raises(nl_sign.SignError):
|
||||
nl_sign.sign_image(raw, key.private_pem)
|
||||
Reference in New Issue
Block a user