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:
265
applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java
Normal file
265
applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java
Normal file
@@ -0,0 +1,265 @@
|
||||
package com.dangerousthings.aliro;
|
||||
|
||||
import javacard.security.ECPrivateKey;
|
||||
import javacard.security.KeyAgreement;
|
||||
|
||||
/**
|
||||
* Low-level Aliro cryptographic primitives, factored out so they can be
|
||||
* unit-tested independently of the APDU dispatch in {@link AliroApplet}.
|
||||
*
|
||||
* <p>Intended to be instantiated once per applet (or test) and reused —
|
||||
* the bundled {@link KeyAgreement} and {@link AliroHmac} objects are
|
||||
* expensive to allocate.
|
||||
*
|
||||
* <p>HMAC-SHA-256 is supplied by {@link AliroHmac}, our userland
|
||||
* implementation. The target card (NXP J3R180) does not expose
|
||||
* {@code KeyBuilder.TYPE_HMAC} at any tested key size nor
|
||||
* {@code Signature.ALG_HMAC_SHA_256}, so we build HMAC on top of the card's
|
||||
* raw SHA-256 primitive per RFC 2104.
|
||||
*/
|
||||
final class AliroCrypto {
|
||||
|
||||
private static final short HASH_LEN = 32;
|
||||
|
||||
/** Maximum expanded-input buffer size for HKDF-Expand: one T(i-1) block
|
||||
* (32B) + info (up to ~200B, defensively) + one counter byte. Rounded up
|
||||
* to 256B. The largest info we currently pass is the 32-byte salt_volatile
|
||||
* info for expedited-standard derivation; step-up uses 8-byte ASCII info. */
|
||||
private static final short EXPAND_SCRATCH_LEN = 256;
|
||||
|
||||
private KeyAgreement ecdhPlain;
|
||||
private AliroHmac aliroHmac;
|
||||
|
||||
/** Reusable scratch for one HMAC output (T(i)) and one counter byte. */
|
||||
private byte[] hkdfPrevT;
|
||||
|
||||
/** Scratch for assembling the HMAC message for HKDF-Expand: T(i-1) || info || counter. */
|
||||
private byte[] expandScratch;
|
||||
|
||||
/** Scratch for {@link #deriveKdh}: 32B for Z_AB then 32B for PRK. */
|
||||
private byte[] kdfWorkbuf;
|
||||
private static final short KDF_WORKBUF_LEN = 64;
|
||||
|
||||
/** 32 zero bytes — RFC 5869 §2.2 fallback when {@link #hkdfExtract} is
|
||||
* called with an empty salt. Allocated in the constructor; Java Card
|
||||
* bans {@code new} in static initializers, so we can't make this static-final. */
|
||||
private byte[] emptySaltZeros;
|
||||
|
||||
AliroCrypto() {
|
||||
try {
|
||||
ecdhPlain = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false);
|
||||
} catch (Throwable t) {
|
||||
javacard.framework.ISOException.throwIt((short) 0x6FC1);
|
||||
}
|
||||
// Userland HMAC-SHA-256 on top of MessageDigest.ALG_SHA_256. Diagnostic
|
||||
// status word 0x6FC7 gives us one regression cycle of visibility on the
|
||||
// real card before we strip it.
|
||||
try {
|
||||
aliroHmac = new AliroHmac();
|
||||
} catch (Throwable t) {
|
||||
javacard.framework.ISOException.throwIt((short) 0x6FC7);
|
||||
}
|
||||
try {
|
||||
hkdfPrevT = javacard.framework.JCSystem.makeTransientByteArray(
|
||||
HASH_LEN, javacard.framework.JCSystem.CLEAR_ON_DESELECT);
|
||||
expandScratch = javacard.framework.JCSystem.makeTransientByteArray(
|
||||
EXPAND_SCRATCH_LEN, javacard.framework.JCSystem.CLEAR_ON_DESELECT);
|
||||
kdfWorkbuf = javacard.framework.JCSystem.makeTransientByteArray(
|
||||
KDF_WORKBUF_LEN, javacard.framework.JCSystem.CLEAR_ON_DESELECT);
|
||||
} catch (Throwable t) {
|
||||
javacard.framework.ISOException.throwIt((short) 0x6FC4);
|
||||
}
|
||||
try {
|
||||
emptySaltZeros = new byte[32]; // persistent, defaults to all zeros
|
||||
} catch (Throwable t) {
|
||||
javacard.framework.ISOException.throwIt((short) 0x6FC5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the x-coordinate of the ECDH shared point, given a local
|
||||
* P-256 private key and the peer's uncompressed public point
|
||||
* (0x04 || x || y, 65 bytes). Returns 32 (the x-coordinate length).
|
||||
*/
|
||||
short computeEcdhSharedX(
|
||||
ECPrivateKey priv,
|
||||
byte[] peerPubUncomp, short peerPubOff,
|
||||
byte[] out, short outOff) {
|
||||
ecdhPlain.init(priv);
|
||||
return ecdhPlain.generateSecret(peerPubUncomp, peerPubOff, (short) 65, out, outOff);
|
||||
}
|
||||
|
||||
/**
|
||||
* HKDF-Extract per RFC 5869 §2.2 with HMAC-SHA-256:
|
||||
* {@code PRK = HMAC(salt, IKM)}.
|
||||
* Returns 32 (HashLen).
|
||||
*/
|
||||
short hkdfExtract(
|
||||
byte[] salt, short saltOff, short saltLen,
|
||||
byte[] ikm, short ikmOff, short ikmLen,
|
||||
byte[] prk, short prkOff) {
|
||||
// RFC 5869 §2.2: empty salt is treated as HashLen zero bytes.
|
||||
if (saltLen == 0) {
|
||||
return aliroHmac.compute(
|
||||
emptySaltZeros, (short) 0, HASH_LEN,
|
||||
ikm, ikmOff, ikmLen,
|
||||
prk, prkOff);
|
||||
}
|
||||
return aliroHmac.compute(
|
||||
salt, saltOff, saltLen,
|
||||
ikm, ikmOff, ikmLen,
|
||||
prk, prkOff);
|
||||
}
|
||||
|
||||
/**
|
||||
* HKDF-Expand per RFC 5869 §2.3 with HMAC-SHA-256. Generates {@code L}
|
||||
* bytes of output keying material, where {@code L} must be ≤ 255 *
|
||||
* HashLen (8160 bytes — far above any Aliro need). The info region must
|
||||
* be at most {@code EXPAND_SCRATCH_LEN - HASH_LEN - 1} bytes long
|
||||
* (= 223 bytes with the current buffer sizing).
|
||||
*/
|
||||
short hkdfExpand(
|
||||
byte[] prk, short prkOff, short prkLen,
|
||||
byte[] info, short infoOff, short infoLen,
|
||||
short L,
|
||||
byte[] out, short outOff) {
|
||||
|
||||
// T(0) = empty; T(i) = HMAC(PRK, T(i-1) || info || i). Concatenate
|
||||
// T(1)..T(N) and truncate to L bytes.
|
||||
short produced = 0;
|
||||
short prevTLen = 0;
|
||||
byte counter = (byte) 1;
|
||||
|
||||
while (produced < L) {
|
||||
// Assemble the HMAC message as T(i-1) || info || counter in expandScratch.
|
||||
short msgLen = 0;
|
||||
if (prevTLen != 0) {
|
||||
javacard.framework.Util.arrayCopyNonAtomic(
|
||||
hkdfPrevT, (short) 0, expandScratch, msgLen, prevTLen);
|
||||
msgLen += prevTLen;
|
||||
}
|
||||
if (infoLen != 0) {
|
||||
javacard.framework.Util.arrayCopyNonAtomic(
|
||||
info, infoOff, expandScratch, msgLen, infoLen);
|
||||
msgLen += infoLen;
|
||||
}
|
||||
expandScratch[msgLen] = counter;
|
||||
msgLen++;
|
||||
|
||||
aliroHmac.compute(
|
||||
prk, prkOff, prkLen,
|
||||
expandScratch, (short) 0, msgLen,
|
||||
hkdfPrevT, (short) 0);
|
||||
prevTLen = HASH_LEN;
|
||||
|
||||
short remaining = (short) (L - produced);
|
||||
short toCopy = remaining < HASH_LEN ? remaining : HASH_LEN;
|
||||
javacard.framework.Util.arrayCopyNonAtomic(
|
||||
hkdfPrevT, (short) 0, out, (short) (outOff + produced), toCopy);
|
||||
produced += toCopy;
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Wipe the assembled message scratch — it held T(i-1) values (derived
|
||||
// from the HKDF PRK) and the info bytes. CLEAR_ON_DESELECT cleans up
|
||||
// at deselect, but multi-APDU sessions would otherwise see stale T(i).
|
||||
javacard.framework.Util.arrayFillNonAtomic(
|
||||
expandScratch, (short) 0, EXPAND_SCRATCH_LEN, (byte) 0);
|
||||
|
||||
return produced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives Kdh per Aliro §8.3.1.4 (with the §8.3.1.5 HKDF substitution
|
||||
* noted in the spec): {@code Kdh = HKDF(IKM=ECDH_x(priv, peerPub),
|
||||
* salt=txnId, info=∅, L=32)}. Writes 32 bytes to {@code out[outOff..]}
|
||||
* and returns 32.
|
||||
*/
|
||||
short deriveKdh(
|
||||
ECPrivateKey priv,
|
||||
byte[] peerPubUncomp, short peerPubOff,
|
||||
byte[] txnId, short txnIdOff, short txnIdLen,
|
||||
byte[] out, short outOff) {
|
||||
computeEcdhSharedX(priv, peerPubUncomp, peerPubOff, kdfWorkbuf, (short) 0);
|
||||
hkdfExtract(
|
||||
txnId, txnIdOff, txnIdLen,
|
||||
kdfWorkbuf, (short) 0, HASH_LEN,
|
||||
kdfWorkbuf, HASH_LEN);
|
||||
hkdfExpand(
|
||||
kdfWorkbuf, HASH_LEN, HASH_LEN,
|
||||
kdfWorkbuf, (short) 0, (short) 0,
|
||||
HASH_LEN,
|
||||
out, outOff);
|
||||
return HASH_LEN;
|
||||
}
|
||||
|
||||
/** ASCII info strings for step-up session key derivation, per mdoc 9.1.1.5. */
|
||||
private static final byte[] STEP_UP_INFO_SK_DEVICE = {
|
||||
(byte) 'S', (byte) 'K', (byte) 'D', (byte) 'e',
|
||||
(byte) 'v', (byte) 'i', (byte) 'c', (byte) 'e'
|
||||
};
|
||||
private static final byte[] STEP_UP_INFO_SK_READER = {
|
||||
(byte) 'S', (byte) 'K', (byte) 'R', (byte) 'e',
|
||||
(byte) 'a', (byte) 'd', (byte) 'e', (byte) 'r'
|
||||
};
|
||||
|
||||
/**
|
||||
* Derives {@code StepUpSKDevice} and {@code StepUpSKReader} per Aliro
|
||||
* §8.4.3, which points at mdoc [6] clause 9.1.1.5 with two Aliro-specific
|
||||
* changes: IKM is the {@code StepUpSK} computed in §8.3.1.13 and the salt
|
||||
* is empty. Info remains the ASCII bytes of {@code "SKDevice"} and
|
||||
* {@code "SKReader"}.
|
||||
*
|
||||
* <p>Writes 32 bytes each to {@code skDeviceOut[skDeviceOff..]} and
|
||||
* {@code skReaderOut[skReaderOff..]}.
|
||||
*/
|
||||
short deriveStepUpSessionKeys(
|
||||
byte[] stepUpSK, short stepUpSKOff,
|
||||
byte[] skDeviceOut, short skDeviceOff,
|
||||
byte[] skReaderOut, short skReaderOff) {
|
||||
// HKDF-Extract once (shared across the two Expand calls): empty salt.
|
||||
hkdfExtract(
|
||||
kdfWorkbuf, (short) 0, (short) 0, // empty salt — use first 0 bytes of anything
|
||||
stepUpSK, stepUpSKOff, HASH_LEN,
|
||||
kdfWorkbuf, HASH_LEN); // PRK at [32..64)
|
||||
|
||||
hkdfExpand(
|
||||
kdfWorkbuf, HASH_LEN, HASH_LEN,
|
||||
STEP_UP_INFO_SK_DEVICE, (short) 0, (short) STEP_UP_INFO_SK_DEVICE.length,
|
||||
HASH_LEN,
|
||||
skDeviceOut, skDeviceOff);
|
||||
|
||||
hkdfExpand(
|
||||
kdfWorkbuf, HASH_LEN, HASH_LEN,
|
||||
STEP_UP_INFO_SK_READER, (short) 0, (short) STEP_UP_INFO_SK_READER.length,
|
||||
HASH_LEN,
|
||||
skReaderOut, skReaderOff);
|
||||
|
||||
return HASH_LEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the 160-byte {@code derived_keys_volatile} for the
|
||||
* expedited-standard phase per Aliro §8.3.1.13. Output is laid out as
|
||||
* ExpeditedSKReader[0..32), ExpeditedSKDevice[32..64), StepUpSK[64..96),
|
||||
* BleSK[96..128), URSK[128..160). For NFC-only operation only the
|
||||
* first 96 bytes are load-bearing, but the spec mandates generating all
|
||||
* 160 so the caller can discard what it doesn't need.
|
||||
*/
|
||||
short deriveExpeditedStandardKeys(
|
||||
byte[] kdh, short kdhOff,
|
||||
byte[] saltVolatile, short saltOff, short saltLen,
|
||||
byte[] info, short infoOff, short infoLen,
|
||||
byte[] out, short outOff) {
|
||||
hkdfExtract(
|
||||
saltVolatile, saltOff, saltLen,
|
||||
kdh, kdhOff, HASH_LEN,
|
||||
kdfWorkbuf, (short) 0);
|
||||
hkdfExpand(
|
||||
kdfWorkbuf, (short) 0, HASH_LEN,
|
||||
info, infoOff, infoLen,
|
||||
(short) 160,
|
||||
out, outOff);
|
||||
return (short) 160;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user