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:
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user