Files
aliro-project/applet/src/test/java/com/dangerousthings/aliro/AliroGcmTest.java
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

230 lines
9.4 KiB
Java

package com.dangerousthings.aliro;
import javacard.security.AESKey;
import javacard.security.KeyBuilder;
import javacardx.crypto.AEADCipher;
import javacardx.crypto.Cipher;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
/**
* Tests for {@link AliroGcm} — our userland AES-256-GCM, built on the JC
* single-block AES-ECB primitive. Needed because the target card (NXP J3R180)
* does not expose {@code AEADCipher.ALG_AES_GCM}. The NIST test vectors come
* from {@code gcmEncryptExtIV256.rsp} (the CMVP AEAD AES-256 suite).
*/
class AliroGcmTest {
@Test
void nistTestCase13_emptyMessage_emptyAad_returnsTagOnly() {
// K=0^256, IV=0^96, P=A=empty, expected T = 530f8afbc74536b9a963b4f1c4cb738b
byte[] key = new byte[32];
byte[] iv = new byte[12];
byte[] pt = new byte[0];
byte[] expectedTag = hex("530f8afbc74536b9a963b4f1c4cb738b");
byte[] out = new byte[16];
AliroGcm gcm = new AliroGcm();
short n = gcm.encrypt(key, (short) 0, iv, (short) 0, pt, (short) 0, (short) 0, out, (short) 0);
assertEquals(16, n, "empty plaintext → 16 bytes output (just the tag)");
assertArrayEquals(expectedTag, out, "NIST GCM AES-256 Test Case 13 tag");
}
@Test
void nistTestCase14_singleZeroBlock_returnsCiphertextAndTag() {
// K=0^256, IV=0^96, P=0^128, A=empty
// C = cea7403d4d606b6e074ec5d3baf39d18
// T = d0d1c8a799996bf0265b98b5d48ab919
byte[] key = new byte[32];
byte[] iv = new byte[12];
byte[] pt = new byte[16];
byte[] expectedCt = hex("cea7403d4d606b6e074ec5d3baf39d18");
byte[] expectedTag = hex("d0d1c8a799996bf0265b98b5d48ab919");
byte[] out = new byte[32];
AliroGcm gcm = new AliroGcm();
short n = gcm.encrypt(key, (short) 0, iv, (short) 0, pt, (short) 0, (short) pt.length, out, (short) 0);
assertEquals(32, n, "16B plaintext + 16B tag");
byte[] ct = new byte[16];
System.arraycopy(out, 0, ct, 0, 16);
byte[] tag = new byte[16];
System.arraycopy(out, 16, tag, 0, 16);
assertArrayEquals(expectedCt, ct, "NIST GCM AES-256 Test Case 14 ciphertext");
assertArrayEquals(expectedTag, tag, "NIST GCM AES-256 Test Case 14 tag");
}
@Test
void nistTestCase15_fourBlocks_realKey() {
// K = feffe9928665731c6d6a8f9467308308 feffe9928665731c6d6a8f9467308308
// IV = cafebabefacedbaddecaf888
// P = 4 blocks, A = empty
byte[] key = hex("feffe9928665731c6d6a8f9467308308"
+ "feffe9928665731c6d6a8f9467308308");
byte[] iv = hex("cafebabefacedbaddecaf888");
byte[] pt = hex("d9313225f88406e5a55909c5aff5269a"
+ "86a7a9531534f7da2e4c303d8a318a72"
+ "1c3c0c95956809532fcf0e2449a6b525"
+ "b16aedf5aa0de657ba637b391aafd255");
byte[] expectedCt = hex("522dc1f099567d07f47f37a32a84427d"
+ "643a8cdcbfe5c0c97598a2bd2555d1aa"
+ "8cb08e48590dbb3da7b08b1056828838"
+ "c5f61e6393ba7a0abcc9f662898015ad");
byte[] expectedTag = hex("b094dac5d93471bdec1a502270e3cc6c");
byte[] out = new byte[pt.length + 16];
AliroGcm gcm = new AliroGcm();
short n = gcm.encrypt(key, (short) 0, iv, (short) 0, pt, (short) 0, (short) pt.length, out, (short) 0);
assertEquals(pt.length + 16, n);
byte[] ct = new byte[pt.length];
System.arraycopy(out, 0, ct, 0, pt.length);
byte[] tag = new byte[16];
System.arraycopy(out, pt.length, tag, 0, 16);
assertArrayEquals(expectedCt, ct, "NIST GCM AES-256 Test Case 15 ciphertext");
assertArrayEquals(expectedTag, tag, "NIST GCM AES-256 Test Case 15 tag");
}
/**
* End-to-end: encrypt with AliroGcm, decrypt with jcardsim's AEADCipher
* (available in tests via the patched jcardsim master in m2 volume).
* Confirms wire-compatibility with the standard implementation.
*/
@Test
void roundTripAgainstJcardsimAEADCipher() {
byte[] key = new byte[32];
for (int i = 0; i < 32; i++) key[i] = (byte) (i * 7 + 3);
byte[] iv = new byte[12];
for (int i = 0; i < 12; i++) iv[i] = (byte) (0xA0 ^ i);
byte[] pt = new byte[137]; // realistic Aliro Table 8-11 size
for (int i = 0; i < pt.length; i++) pt[i] = (byte) (i ^ 0x5C);
byte[] out = new byte[pt.length + 16];
AliroGcm gcm = new AliroGcm();
gcm.encrypt(key, (short) 0, iv, (short) 0,
pt, (short) 0, (short) pt.length, out, (short) 0);
// Decrypt with jcardsim's AEADCipher.
AESKey k = (AESKey) KeyBuilder.buildKey(
KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false);
k.setKey(key, (short) 0);
AEADCipher jcGcm = (AEADCipher) Cipher.getInstance(AEADCipher.ALG_AES_GCM, false);
jcGcm.init(k, Cipher.MODE_DECRYPT, iv, (short) 0, (short) iv.length);
byte[] dec = new byte[pt.length + 16];
short decLen = jcGcm.doFinal(out, (short) 0, (short) out.length, dec, (short) 0);
assertEquals(pt.length, decLen, "jcardsim GCM decrypt returns pt length");
byte[] ptOut = new byte[pt.length];
System.arraycopy(dec, 0, ptOut, 0, pt.length);
assertArrayEquals(pt, ptOut,
"AliroGcm ciphertext must decrypt correctly under jcardsim AEADCipher");
}
/**
* Partial final block: plaintext whose length isn't a multiple of 16.
* GCTR must handle the partial block correctly (XOR only |P| mod 16 bytes
* of the last keystream block), and GHASH must zero-pad.
*/
@Test
void partialFinalBlock() {
byte[] key = new byte[32];
for (int i = 0; i < 32; i++) key[i] = (byte) i;
byte[] iv = new byte[12];
iv[11] = 0x01;
byte[] pt = new byte[17]; // 1 full block + 1 byte
for (int i = 0; i < 17; i++) pt[i] = (byte) (0xAA + i);
byte[] out = new byte[pt.length + 16];
AliroGcm gcm = new AliroGcm();
short n = gcm.encrypt(key, (short) 0, iv, (short) 0,
pt, (short) 0, (short) pt.length, out, (short) 0);
assertEquals(pt.length + 16, n,
"ciphertext length must equal plaintext length (GCM is length-preserving)");
// Round-trip through jcardsim for correctness.
AESKey k = (AESKey) KeyBuilder.buildKey(
KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false);
k.setKey(key, (short) 0);
AEADCipher jcGcm = (AEADCipher) Cipher.getInstance(AEADCipher.ALG_AES_GCM, false);
jcGcm.init(k, Cipher.MODE_DECRYPT, iv, (short) 0, (short) iv.length);
byte[] dec = new byte[pt.length + 16];
short decLen = jcGcm.doFinal(out, (short) 0, (short) out.length, dec, (short) 0);
assertEquals(pt.length, decLen);
byte[] ptOut = new byte[pt.length];
System.arraycopy(dec, 0, ptOut, 0, pt.length);
assertArrayEquals(pt, ptOut, "17-byte plaintext must round-trip");
}
@Test
void differentKeysProduceDifferentTags() {
byte[] iv = new byte[12];
iv[11] = 0x01;
byte[] pt = new byte[] { 0x11, 0x22, 0x33, 0x44 };
byte[] key1 = new byte[32];
byte[] key2 = new byte[32];
key2[0] = 0x01; // differ in one bit
AliroGcm gcm = new AliroGcm();
byte[] out1 = new byte[pt.length + 16];
byte[] out2 = new byte[pt.length + 16];
gcm.encrypt(key1, (short) 0, iv, (short) 0, pt, (short) 0, (short) pt.length, out1, (short) 0);
gcm.encrypt(key2, (short) 0, iv, (short) 0, pt, (short) 0, (short) pt.length, out2, (short) 0);
// Extract tags (last 16 bytes).
byte[] tag1 = new byte[16];
byte[] tag2 = new byte[16];
System.arraycopy(out1, pt.length, tag1, 0, 16);
System.arraycopy(out2, pt.length, tag2, 0, 16);
assertFalse(java.util.Arrays.equals(tag1, tag2),
"different keys must produce different tags");
}
@Test
void differentIvsProduceDifferentCiphertexts() {
byte[] key = new byte[32];
for (int i = 0; i < 32; i++) key[i] = (byte) i;
byte[] pt = new byte[32];
for (int i = 0; i < 32; i++) pt[i] = (byte) (0x10 + i);
byte[] iv1 = new byte[12];
iv1[11] = 0x01;
byte[] iv2 = new byte[12];
iv2[11] = 0x02;
AliroGcm gcm = new AliroGcm();
byte[] out1 = new byte[pt.length + 16];
byte[] out2 = new byte[pt.length + 16];
gcm.encrypt(key, (short) 0, iv1, (short) 0, pt, (short) 0, (short) pt.length, out1, (short) 0);
gcm.encrypt(key, (short) 0, iv2, (short) 0, pt, (short) 0, (short) pt.length, out2, (short) 0);
byte[] ct1 = new byte[pt.length];
byte[] ct2 = new byte[pt.length];
System.arraycopy(out1, 0, ct1, 0, pt.length);
System.arraycopy(out2, 0, ct2, 0, pt.length);
assertFalse(java.util.Arrays.equals(ct1, ct2),
"different IVs must produce different ciphertexts (CTR stream differs)");
}
private static byte[] hex(String s) {
s = s.replaceAll("\\s+", "");
byte[] out = new byte[s.length() / 2];
for (int i = 0; i < out.length; i++) {
out[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
}
return out;
}
}