Files
aliro-project/harness/tests/test_reader_auth1.py
Dangerous Things 782074f6ae Initial snapshot: Aliro applet, harness, Nucleo NFC10A1 reader port
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>
2026-05-02 10:17:46 -07:00

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)