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>
This commit is contained in:
2026-05-02 10:17:46 -07:00
commit 782074f6ae
8786 changed files with 2902373 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
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)