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,165 @@
package com.dangerousthings.aliro;
import com.licel.jcardsim.smartcardio.CardSimulator;
import javacard.framework.AID;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Tests for the PersonalizationApplet — our proprietary provisioning
* channel that loads the Access Credential long-term private key and
* reader public keys before the Aliro flow runs.
*/
class PersonalizationAppletTest {
private static final byte CLA = (byte) 0x80;
private static final byte INS_SET_CRED_PRIV = (byte) 0x20;
private static final byte INS_SET_CRED_PUBK = (byte) 0x21;
private static final byte INS_SET_READER_PUBK = (byte) 0x22;
private static final byte INS_WRITE_ACCESS_DOC = (byte) 0x23;
private static final byte INS_FINALIZE_ACCESS_DOC = (byte) 0x24;
private static final byte INS_COMMIT = (byte) 0x2C;
private CardSimulator sim;
@BeforeEach
void setUp() {
CredentialStore.get().resetForTesting();
sim = new CardSimulator();
AID aid = new AID(AliroAids.PROVISIONING, (short) 0, (byte) AliroAids.PROVISIONING.length);
sim.installApplet(aid, PersonalizationApplet.class);
selectProvisioning();
}
private void selectProvisioning() {
CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, AliroAids.PROVISIONING, 256);
ResponseAPDU r = sim.transmitCommand(select);
assertEquals(0x9000, r.getSW(), "SELECT provisioning AID must succeed");
}
private ResponseAPDU send(byte ins, byte[] data) {
CommandAPDU c = new CommandAPDU(CLA & 0xFF, ins & 0xFF, 0x00, 0x00,
data == null ? new byte[0] : data, 256);
return sim.transmitCommand(c);
}
private ResponseAPDU sendWithP1P2(byte ins, int p1p2, byte[] data) {
CommandAPDU c = new CommandAPDU(CLA & 0xFF, ins & 0xFF,
(p1p2 >> 8) & 0xFF, p1p2 & 0xFF,
data == null ? new byte[0] : data, 256);
return sim.transmitCommand(c);
}
@Test
void setCredentialPrivKeyWith32BytesSucceeds() {
byte[] key = new byte[32];
for (int i = 0; i < key.length; i++) key[i] = (byte) i;
assertEquals(0x9000, send(INS_SET_CRED_PRIV, key).getSW());
}
@Test
void setCredentialPrivKeyWithWrongLengthFails() {
byte[] key = new byte[31];
assertEquals(0x6A80, send(INS_SET_CRED_PRIV, key).getSW(),
"credential private key length != 32 must be rejected");
}
@Test
void setCredentialPubKeyWith64BytesSucceeds() {
byte[] pub = new byte[64];
for (int i = 0; i < pub.length; i++) pub[i] = (byte) (i * 3);
assertEquals(0x9000, send(INS_SET_CRED_PUBK, pub).getSW());
}
@Test
void setCredentialPubKeyWithWrongLengthFails() {
byte[] pub = new byte[63];
assertEquals(0x6A80, send(INS_SET_CRED_PUBK, pub).getSW(),
"credential public key length != 64 must be rejected");
}
@Test
void setCredentialPubKeyAfterCommitIsLocked() {
assertEquals(0x9000, send(INS_COMMIT, null).getSW());
byte[] pub = new byte[64];
assertEquals(0x6985, send(INS_SET_CRED_PUBK, pub).getSW(),
"writes after COMMIT must return SW_CONDITIONS_NOT_SATISFIED");
}
@Test
void setReaderPubKeyWith64BytesSucceeds() {
byte[] pub = new byte[64];
for (int i = 0; i < pub.length; i++) pub[i] = (byte) (i * 7);
assertEquals(0x9000, send(INS_SET_READER_PUBK, pub).getSW());
}
@Test
void accessDocumentChunkedWriteAndFinalizeRoundTrip() {
byte[] ad = new byte[420];
for (int i = 0; i < ad.length; i++) ad[i] = (byte) ((i * 13) ^ 0xA5);
// Two chunks: [0..200), [200..420)
byte[] chunk1 = java.util.Arrays.copyOfRange(ad, 0, 200);
byte[] chunk2 = java.util.Arrays.copyOfRange(ad, 200, 420);
assertEquals(0x9000, sendWithP1P2(INS_WRITE_ACCESS_DOC, 0, chunk1).getSW());
assertEquals(0x9000, sendWithP1P2(INS_WRITE_ACCESS_DOC, 200, chunk2).getSW());
assertEquals(0x9000, sendWithP1P2(INS_FINALIZE_ACCESS_DOC, ad.length, null).getSW());
// Verify via the test-only hook: bytes must match.
byte[] stored = new byte[ad.length];
CredentialStore.get().copyAccessDocument(stored, (short) 0, (short) 0, (short) ad.length);
org.junit.jupiter.api.Assertions.assertArrayEquals(ad, stored);
assertEquals(ad.length, CredentialStore.get().getAccessDocumentLen());
org.junit.jupiter.api.Assertions.assertTrue(CredentialStore.get().hasAccessDocument());
}
@Test
void accessDocumentWriteBeyondMaxSizeFails() {
byte[] payload = new byte[50];
short maxOff = (short) (CredentialStore.ACCESS_DOC_MAX_LEN - 10); // 10 bytes short of max
assertEquals(0x6A80, sendWithP1P2(INS_WRITE_ACCESS_DOC, maxOff, payload).getSW(),
"a 50-byte chunk at offset MAX-10 overruns and must be rejected");
}
@Test
void accessDocumentFinalizeBeyondMaxSizeFails() {
byte[] chunk = new byte[10];
assertEquals(0x9000, sendWithP1P2(INS_WRITE_ACCESS_DOC, 0, chunk).getSW());
int tooBig = CredentialStore.ACCESS_DOC_MAX_LEN + 1;
assertEquals(0x6A80, sendWithP1P2(INS_FINALIZE_ACCESS_DOC, tooBig, null).getSW(),
"finalize length > ACCESS_DOC_MAX_LEN must be rejected");
}
@Test
void accessDocumentWriteAfterCommitIsLocked() {
assertEquals(0x9000, send(INS_COMMIT, null).getSW());
byte[] chunk = new byte[8];
assertEquals(0x6985, sendWithP1P2(INS_WRITE_ACCESS_DOC, 0, chunk).getSW());
assertEquals(0x6985, sendWithP1P2(INS_FINALIZE_ACCESS_DOC, 8, null).getSW());
}
@Test
void commitReturns9000() {
assertEquals(0x9000, send(INS_COMMIT, null).getSW());
}
@Test
void setAfterCommitIsLocked() {
assertEquals(0x9000, send(INS_COMMIT, null).getSW());
byte[] key = new byte[32];
assertEquals(0x6985, send(INS_SET_CRED_PRIV, key).getSW(),
"writes after COMMIT must return SW_CONDITIONS_NOT_SATISFIED");
}
@Test
void secondCommitIsLocked() {
assertEquals(0x9000, send(INS_COMMIT, null).getSW());
assertEquals(0x6985, send(INS_COMMIT, null).getSW(),
"a second COMMIT must also be refused");
}
}