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>
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
import pytest
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
|
|
|
|
from aliro_harness.reader.auth1 import (
|
|
build_auth1_apdu,
|
|
build_auth1_data,
|
|
build_table_812,
|
|
build_table_813,
|
|
sign_table_812_raw,
|
|
)
|
|
|
|
|
|
def test_table_812_layout_per_spec_table_8_12():
|
|
t = build_table_812(
|
|
reader_id_32=b"\x4d" * 32,
|
|
credential_ephem_pub_x=b"\x86" * 32,
|
|
reader_ephem_pub_x=b"\x87" * 32,
|
|
transaction_id=b"\x4c" * 16,
|
|
)
|
|
assert t[0:2] == bytes([0x4D, 0x20])
|
|
assert t[2:34] == b"\x4d" * 32
|
|
assert t[34:36] == bytes([0x86, 0x20])
|
|
assert t[36:68] == b"\x86" * 32
|
|
assert t[68:70] == bytes([0x87, 0x20])
|
|
assert t[70:102] == b"\x87" * 32
|
|
assert t[102:104] == bytes([0x4C, 0x10])
|
|
assert t[104:120] == b"\x4c" * 16
|
|
assert t[120:122] == bytes([0x93, 0x04])
|
|
assert t[122:126] == bytes.fromhex("415D9569") # reader sig usage
|
|
# 5 TLVs: 34+34+34+18+6 = 126 bytes (plan's "128" was off-by-2; byte
|
|
# offsets above are self-consistent and match ReaderSide.buildTable812).
|
|
assert len(t) == 126
|
|
|
|
|
|
def test_table_813_uses_ud_usage_tag():
|
|
"""Same TLV positions as 8-12, but the 0x93 value = UD usage tag."""
|
|
kwargs = dict(
|
|
reader_id_32=b"\x4d" * 32,
|
|
credential_ephem_pub_x=b"\x86" * 32,
|
|
reader_ephem_pub_x=b"\x87" * 32,
|
|
transaction_id=b"\x4c" * 16,
|
|
)
|
|
t = build_table_813(**kwargs)
|
|
t12 = build_table_812(**kwargs)
|
|
# Everything up through the 0x93 0x04 header is identical...
|
|
assert t[0:122] == t12[0:122]
|
|
assert t[120:122] == bytes([0x93, 0x04])
|
|
# ...but the usage tag differs.
|
|
assert t[122:126] == bytes.fromhex("4E887B4C")
|
|
assert t12[122:126] == bytes.fromhex("415D9569")
|
|
assert len(t) == 126
|
|
|
|
|
|
def test_sign_then_verify_round_trip():
|
|
"""Sign Table 8-12 with a fresh P-256 key, verify with public half.
|
|
Raw 64B r||s must re-assemble into a DER signature that verifies."""
|
|
priv = ec.generate_private_key(ec.SECP256R1())
|
|
table_812 = build_table_812(
|
|
reader_id_32=b"\x01" * 32,
|
|
credential_ephem_pub_x=b"\x02" * 32,
|
|
reader_ephem_pub_x=b"\x03" * 32,
|
|
transaction_id=b"\x04" * 16,
|
|
)
|
|
raw_sig = sign_table_812_raw(table_812, priv)
|
|
assert len(raw_sig) == 64
|
|
|
|
r = int.from_bytes(raw_sig[:32], "big")
|
|
s = int.from_bytes(raw_sig[32:], "big")
|
|
der = encode_dss_signature(r, s)
|
|
# Should verify -- raises InvalidSignature if not.
|
|
priv.public_key().verify(der, table_812, ec.ECDSA(hashes.SHA256()))
|
|
|
|
|
|
def test_build_auth1_data_with_cmd_params_zero():
|
|
"""cmd_params=0 ('key_slot' path) still emits the 0x41/0x9E TLV shell."""
|
|
sig = b"\xaa" * 64
|
|
data = build_auth1_data(sig, command_parameters=0x00)
|
|
assert data[:5] == bytes([0x41, 0x01, 0x00, 0x9E, 0x40])
|
|
assert data[5:69] == sig
|
|
assert len(data) == 69
|
|
|
|
|
|
def test_build_auth1_data_rejects_wrong_sig_length():
|
|
with pytest.raises(ValueError, match="64B raw"):
|
|
build_auth1_data(b"\x00" * 63)
|
|
|
|
|
|
def test_build_auth1_apdu_short_form_wrap():
|
|
"""CLA=0x80, INS=0x81, P1=P2=0, Lc=len(data), then data."""
|
|
data = bytes([0x41, 0x01, 0x01]) + bytes([0x9E, 0x40]) + b"\xcc" * 64
|
|
apdu = build_auth1_apdu(data)
|
|
assert apdu[0] == 0x80 # CLA
|
|
assert apdu[1] == 0x81 # INS_AUTH1
|
|
assert apdu[2] == 0x00 # P1
|
|
assert apdu[3] == 0x00 # P2
|
|
assert apdu[4] == len(data) # Lc (short form)
|
|
assert apdu[5:] == data
|
|
assert len(apdu) == 5 + len(data)
|