Three components, all bench-validated to varying depths: - applet/: CSA Aliro v1.0 Java Card applet for J3R180. AUTH0 + AUTH1 expedited-standard flow end-to-end green via PC/SC bench reader (aliro-bench-test). Userland AES-256-GCM and HMAC-SHA-256 layered on top of J3R180's primitives because the card lacks both natively. P-256 curve params seeded explicitly per J3R180's quirk. - harness/: Python orchestrator (aliro-trustgen, aliro-personalize, aliro-bench-test) for trust-bundle generation, card personalization via PersonalizationApplet, and PC/SC AUTH0+AUTH1 transactions. 126 pytest cases passing. - reader/STM32CubeExpansion_ALIRO_V1_0_0/: ST X-CUBE-ALIRO V1.0.0 with our NFC10A1 port (NUCLEO-U545RE-Q + X-NUCLEO-NFC10A1, ST25R200 shared with NFC09A1). nfc10-only/ project, NFC10A1 BSP shim, ALIRO_TRUST_OVERRIDE include into vendor's provisioning.c, and an ALIRO_APDU_TRACE wrapper around demoTransceiveBlocking. Boots, detects the J3R180, completes SELECT + AUTH0; AUTH1 currently fails with RFAL ERR_PROTO (0xB) — under investigation, see docs/plans/2026-04-20-nucleo-nfc10a1-port.md and bench-notes/. Excluded: x-cube-aliro.zip vendor archive, harness/.venv, build dirs, generated aliro_trust.h (contains private reader scalar), all PEMs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
3.6 KiB
Python
93 lines
3.6 KiB
Python
import pytest
|
|
from cryptography.exceptions import InvalidTag
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
|
|
from aliro_harness.reader.auth1_response import (
|
|
decrypt_auth1_response,
|
|
verify_ud_signature,
|
|
)
|
|
|
|
|
|
def _gen_ud_keypair_and_pub_uncompressed():
|
|
priv = ec.generate_private_key(ec.SECP256R1())
|
|
pub_uncompressed = priv.public_key().public_bytes(
|
|
encoding=serialization.Encoding.X962,
|
|
format=serialization.PublicFormat.UncompressedPoint,
|
|
)
|
|
return priv, pub_uncompressed
|
|
|
|
|
|
def _sign_raw(priv, message: bytes) -> bytes:
|
|
der = priv.sign(message, ec.ECDSA(hashes.SHA256()))
|
|
r, s = decode_dss_signature(der)
|
|
return r.to_bytes(32, "big") + s.to_bytes(32, "big")
|
|
|
|
|
|
def test_decrypt_auth1_response_roundtrips_with_device_counter_1():
|
|
sk = b"\x42" * 32
|
|
iv = b"\x00" * 7 + b"\x01" + b"\x00\x00\x00\x01"
|
|
plaintext = b"\xAB" * 17
|
|
ct_and_tag = AESGCM(sk).encrypt(iv, plaintext, associated_data=None)
|
|
assert decrypt_auth1_response(sk, ct_and_tag) == plaintext
|
|
|
|
|
|
def test_decrypt_auth1_response_fails_on_tampered_ciphertext():
|
|
sk = b"\x42" * 32
|
|
iv = b"\x00" * 7 + b"\x01" + b"\x00\x00\x00\x01"
|
|
plaintext = b"\xAB" * 17
|
|
ct_and_tag = bytearray(AESGCM(sk).encrypt(iv, plaintext, associated_data=None))
|
|
ct_and_tag[0] ^= 0x01
|
|
with pytest.raises(InvalidTag):
|
|
decrypt_auth1_response(sk, bytes(ct_and_tag))
|
|
|
|
|
|
def test_verify_ud_signature_round_trip_returns_true():
|
|
priv, pub_uncompressed = _gen_ud_keypair_and_pub_uncompressed()
|
|
table_813 = b"\x00\x01\x02\x03" + b"table-8-13-test-bytes" * 3
|
|
raw_sig = _sign_raw(priv, table_813)
|
|
assert len(pub_uncompressed) == 65
|
|
assert verify_ud_signature(pub_uncompressed, table_813, raw_sig) is True
|
|
|
|
|
|
def test_verify_ud_signature_returns_false_on_tampered_table():
|
|
priv, pub_uncompressed = _gen_ud_keypair_and_pub_uncompressed()
|
|
table_813 = b"table-8-13-original-bytes-here!!"
|
|
raw_sig = _sign_raw(priv, table_813)
|
|
tampered = bytearray(table_813)
|
|
tampered[0] ^= 0x01
|
|
assert verify_ud_signature(pub_uncompressed, bytes(tampered), raw_sig) is False
|
|
|
|
|
|
def test_verify_ud_signature_returns_false_on_tampered_sig():
|
|
priv, pub_uncompressed = _gen_ud_keypair_and_pub_uncompressed()
|
|
table_813 = b"table-8-13-original-bytes-here!!"
|
|
raw_sig = bytearray(_sign_raw(priv, table_813))
|
|
raw_sig[0] ^= 0x01
|
|
assert verify_ud_signature(pub_uncompressed, table_813, bytes(raw_sig)) is False
|
|
|
|
|
|
def test_verify_ud_signature_rejects_wrong_sig_length():
|
|
_, pub_uncompressed = _gen_ud_keypair_and_pub_uncompressed()
|
|
with pytest.raises(ValueError):
|
|
verify_ud_signature(pub_uncompressed, b"data", b"\x00" * 63)
|
|
|
|
|
|
def test_decrypt_auth1_response_fails_with_wrong_counter():
|
|
"""Pins the hardcoded counter=1 behavior. If someone "fixes" the counter
|
|
to 0 or 2 without thinking, this test fails loudly."""
|
|
sk = b"\x42" * 32
|
|
wrong_iv = b"\x00" * 7 + b"\x01" + b"\x00\x00\x00\x02" # counter=2
|
|
ct = AESGCM(sk).encrypt(wrong_iv, b"hello", associated_data=None)
|
|
with pytest.raises(InvalidTag):
|
|
decrypt_auth1_response(sk, ct)
|
|
|
|
|
|
def test_verify_ud_signature_rejects_malformed_pubkey():
|
|
"""A short or off-curve pubkey blob must raise (not return False).
|
|
Guards against a silent ``cryptography`` behavior change."""
|
|
with pytest.raises(ValueError):
|
|
verify_ud_signature(b"\x04" + b"\x00" * 10, b"data", b"\x00" * 64)
|