Files
Michael Sauer 78cf01ca6c fix(tools): SignError statt roher ValueError bei kaputtem Schluesselmaterial
nl_sign.py verify/sign gaben bei syntaktisch ungueltigem --pub/--key-Inhalt
(falsches DER/PEM-Format) bisher eine unbehandelte ValueError (pycryptodome
teils IndexError/TypeError) an den Aufrufer durch - die CLI brach dadurch
mit Python-Traceback ab statt mit der vorgesehenen "error: ..."-Meldung und
Exitcode 1. Beide Backends (cryptography und pycryptodome/pycryptodomex)
kapseln das Einlesen des privaten wie des oeffentlichen Schluessels jetzt in
try/except und werfen SignError mit einer festen, schluesselinhaltsfreien
Meldung (via "from None", damit die urspruengliche Bibliotheksausnahme nicht
implizit mitgeschleppt wird). Ein ungueltiger Schluessel kann dadurch nie zu
verify_image() == True fuehren.

Vier neue Tests in test_nl_sign.py: je ein Modul- und CLI-Test fuer kaputtes
--pub und kaputtes --key, die SignError bzw. Exitcode != 0 ohne Traceback auf
stderr festnageln.
2026-09-13 03:24:02 +02:00

133 lines
4.8 KiB
Python

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)
def test_verify_rejects_garbage_pub_key(key):
"""Ein kaputter --pub-Inhalt (kein gueltiges DER/PEM) muss als SignError gemeldet
werden, nicht als roher ValueError/Traceback - und darf niemals True ergeben."""
raw = b"x" * 1024
signed = nl_sign.sign_image(raw, key.private_pem)
with pytest.raises(nl_sign.SignError):
nl_sign.verify_image(signed, b"das ist definitiv kein Schluessel")
def test_sign_rejects_garbage_private_key():
"""Ein kaputter --key-Inhalt muss ebenso als SignError gemeldet werden."""
raw = b"x" * 1024
with pytest.raises(nl_sign.SignError):
nl_sign.sign_image(raw, b"das ist definitiv kein Schluessel")
def test_cli_verify_rejects_garbage_pub_key(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(b"das ist definitiv kein Schluessel")
subprocess.run([sys.executable, str(TOOL), "sign", "--key", str(priv),
"--in", str(src), "--out", str(dst)], check=True)
r = subprocess.run([sys.executable, str(TOOL), "verify", "--pub", str(pub),
"--in", str(dst)], capture_output=True, text=True)
assert r.returncode != 0
assert "Traceback" not in r.stderr
assert "error:" in r.stderr.lower()
def test_cli_sign_rejects_garbage_private_key(tmp_path):
src = tmp_path / "in_gz.img"
dst = tmp_path / "out.img"
priv = tmp_path / "key.pem"
src.write_bytes(b"\x01\x02\x03" * 1000)
priv.write_bytes(b"das ist definitiv kein Schluessel")
r = subprocess.run([sys.executable, str(TOOL), "sign", "--key", str(priv),
"--in", str(src), "--out", str(dst)], capture_output=True, text=True)
assert r.returncode != 0
assert "Traceback" not in r.stderr
assert "error:" in r.stderr.lower()