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>
87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""AUTH1 command builder + Table 8-12 reader-signature input.
|
|
|
|
Mirrors applet/src/test/.../ReaderSide.java buildTable812 + buildAuth1Data.
|
|
"""
|
|
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
|
|
|
|
CLA = 0x80
|
|
INS_AUTH1 = 0x81
|
|
|
|
USAGE_READER_AUTH1 = bytes([0x41, 0x5D, 0x95, 0x69])
|
|
USAGE_UD_AUTH1 = bytes([0x4E, 0x88, 0x7B, 0x4C])
|
|
|
|
|
|
def build_table_812(
|
|
*,
|
|
reader_id_32: bytes,
|
|
credential_ephem_pub_x: bytes,
|
|
reader_ephem_pub_x: bytes,
|
|
transaction_id: bytes,
|
|
) -> bytes:
|
|
return _build_table_8_12_or_13(
|
|
reader_id_32, credential_ephem_pub_x, reader_ephem_pub_x,
|
|
transaction_id, USAGE_READER_AUTH1,
|
|
)
|
|
|
|
|
|
def build_table_813(
|
|
*,
|
|
reader_id_32: bytes,
|
|
credential_ephem_pub_x: bytes,
|
|
reader_ephem_pub_x: bytes,
|
|
transaction_id: bytes,
|
|
) -> bytes:
|
|
"""Same TLV shape as Table 8-12, only the usage tag differs."""
|
|
return _build_table_8_12_or_13(
|
|
reader_id_32, credential_ephem_pub_x, reader_ephem_pub_x,
|
|
transaction_id, USAGE_UD_AUTH1,
|
|
)
|
|
|
|
|
|
def _build_table_8_12_or_13(
|
|
rid: bytes, cred_x: bytes, reader_x: bytes, txn: bytes, usage: bytes,
|
|
) -> bytes:
|
|
# Match auth0.py / build_auth1_data discipline: validate fixed-length
|
|
# inputs up front. Java callers get an ArrayIndexOutOfBoundsException
|
|
# naturally; Python slicing would silently produce a wrong-length blob.
|
|
if len(rid) != 32:
|
|
raise ValueError(f"reader_id_32 must be 32B (got {len(rid)})")
|
|
if len(cred_x) != 32:
|
|
raise ValueError(f"credential_ephem_pub_x must be 32B (got {len(cred_x)})")
|
|
if len(reader_x) != 32:
|
|
raise ValueError(f"reader_ephem_pub_x must be 32B (got {len(reader_x)})")
|
|
if len(txn) != 16:
|
|
raise ValueError(f"transaction_id must be 16B (got {len(txn)})")
|
|
return (
|
|
bytes([0x4D, 0x20]) + rid
|
|
+ bytes([0x86, 0x20]) + cred_x
|
|
+ bytes([0x87, 0x20]) + reader_x
|
|
+ bytes([0x4C, 0x10]) + txn
|
|
+ bytes([0x93, 0x04]) + usage
|
|
)
|
|
|
|
|
|
def sign_table_812_raw(table_812: bytes, reader_priv: ec.EllipticCurvePrivateKey) -> bytes:
|
|
"""Returns 64B raw r||s ECDSA-SHA-256 signature."""
|
|
der = reader_priv.sign(table_812, ec.ECDSA(hashes.SHA256()))
|
|
r, s = decode_dss_signature(der)
|
|
return r.to_bytes(32, "big") + s.to_bytes(32, "big")
|
|
|
|
|
|
def build_auth1_data(raw_sig_64: bytes, command_parameters: int = 0x01) -> bytes:
|
|
"""AUTH1 command data field per spec Table 8-10. cmd_params bit 0 = 1
|
|
means 'return credential_PubK' (simpler than the SHA-1 key_slot path)."""
|
|
if len(raw_sig_64) != 64:
|
|
raise ValueError("reader signature must be 64B raw r||s")
|
|
return (
|
|
bytes([0x41, 0x01, command_parameters])
|
|
+ bytes([0x9E, 0x40]) + raw_sig_64
|
|
)
|
|
|
|
|
|
def build_auth1_apdu(data: bytes) -> bytes:
|
|
return bytes([CLA, INS_AUTH1, 0x00, 0x00, len(data)]) + data
|