Files
aliro-project/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java
michael 531b1d3186 feat(applet): accessDocumentVerified flag in CredentialStore (M2A.1)
Persistent boolean flag for the cached IssuerAuth verify result. Field
sits beside accessDocumentFinalized in writeTo/readFrom (FIELD_VERSION
bumped 1->2 -- schema change requires re-personalize). reset() clears.
Test pins the serialize/deserialize round-trip via the existing
RecordingSink/ReplaySource helpers.

Foundation for M2A.2 (CoseVerifier) and M2A.3 (verify-at-finalize wiring).
2026-06-17 16:03:22 -07:00

325 lines
12 KiB
Java

package com.dangerousthings.aliro;
/**
* Shared, package-private provisioning store. Holds the Access Credential
* private key and (eventually) reader public keys. The
* {@link PersonalizationApplet} writes into it; {@link AliroApplet} reads
* from it when it needs long-term keys for AUTH1 / step-up.
*
* <p>Ownership: the single live instance is owned by the
* {@link PersonalizationApplet} instance (created in its constructor via
* {@link #bootstrap()}). The static {@link #INSTANCE} field is just an
* in-package publish-point so AliroApplet/StepUpApplet can find it via
* {@link #get()} without going through a {@code Shareable} SIO.
*
* <p>The on-instance ownership matters for GlobalPlatform Amendment H
* (Executable Load File Upgrade): AMD-H preserves registered applet
* instances and their reachable object graph but wipes static fields. With
* the live reference held on the PersonalizationApplet instance, an upgrade
* keeps enrollment data; PersonalizationApplet's restore hook then calls
* {@link #republish(CredentialStore)} to re-establish the static alias.
*
* <p>After {@link #commit()} is called, further writes via the
* personalization interface are refused. The only way to re-unlock is
* a factory reset, which is not yet implemented.
*/
final class CredentialStore {
static final short CRED_PRIV_KEY_LEN = 32;
static final short CRED_PUBK_LEN = 64;
static final short READER_PUBK_LEN = 64;
/** Max Access Document blob size (serialized COSE_Sign1 bytes). 1 KB
* accommodates a typical Aliro Access Document with room to spare. */
static final short ACCESS_DOC_MAX_LEN = 1024;
/** Stable field order for AMD-H Element serialization. Append-only —
* never reorder or remove without bumping the package version and
* writing an explicit migration step in the new ELF's onRestore. */
static final byte FIELD_VERSION = 2;
/** Publish-point read by AliroApplet/StepUpApplet via {@link #get()}.
* PersonalizationApplet owns the actual instance; this is just an alias
* so other applets in the same package can find it without SIO. The
* reference is re-published after an AMD-H restore (see
* PersonalizationApplet.onRestore). */
private static CredentialStore INSTANCE;
private final byte[] credentialPrivKey;
private boolean credentialPrivKeySet;
private final byte[] credentialPubKey;
private boolean credentialPubKeySet;
private final byte[] readerPubKey;
private boolean readerPubKeySet;
private final byte[] accessDocument;
private short accessDocumentLen;
private boolean accessDocumentFinalized;
private boolean accessDocumentVerified;
private boolean committed;
private CredentialStore() {
credentialPrivKey = new byte[CRED_PRIV_KEY_LEN];
credentialPubKey = new byte[CRED_PUBK_LEN];
readerPubKey = new byte[READER_PUBK_LEN];
accessDocument = new byte[ACCESS_DOC_MAX_LEN];
}
/** Called once from {@link PersonalizationApplet}'s constructor. */
static CredentialStore bootstrap() {
INSTANCE = new CredentialStore();
return INSTANCE;
}
/** Called by {@code PersonalizationApplet.onRestore} to re-publish a
* restored store after an AMD-H Executable Load File upgrade, where
* static fields are wiped but the PersonalizationApplet instance (and
* its CredentialStore reference) survive. */
static void republish(CredentialStore restored) {
INSTANCE = restored;
}
static CredentialStore get() {
return INSTANCE;
}
boolean isCommitted() {
return committed;
}
void commit() {
committed = true;
}
/**
* Returns true iff a credential private key has been written. Callers
* that need the actual bytes should use {@link #copyCredentialPrivKey}.
*/
boolean hasCredentialPrivKey() {
return credentialPrivKeySet;
}
void setCredentialPrivKey(byte[] src, short off) {
javacard.framework.Util.arrayCopyNonAtomic(
src, off, credentialPrivKey, (short) 0, CRED_PRIV_KEY_LEN);
credentialPrivKeySet = true;
}
short copyCredentialPrivKey(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
credentialPrivKey, (short) 0, dst, dstOff, CRED_PRIV_KEY_LEN);
return CRED_PRIV_KEY_LEN;
}
boolean hasCredentialPubKey() {
return credentialPubKeySet;
}
void setCredentialPubKey(byte[] src, short off) {
javacard.framework.Util.arrayCopyNonAtomic(
src, off, credentialPubKey, (short) 0, CRED_PUBK_LEN);
credentialPubKeySet = true;
}
short copyCredentialPubKey(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
credentialPubKey, (short) 0, dst, dstOff, CRED_PUBK_LEN);
return CRED_PUBK_LEN;
}
/** Copies only the 32-byte x-coordinate of credential_PubK (spec uses x-only in salt_volatile). */
short copyCredentialPubKeyX(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
credentialPubKey, (short) 0, dst, dstOff, (short) 32);
return (short) 32;
}
boolean hasReaderPubKey() {
return readerPubKeySet;
}
void setReaderPubKey(byte[] src, short off) {
javacard.framework.Util.arrayCopyNonAtomic(
src, off, readerPubKey, (short) 0, READER_PUBK_LEN);
readerPubKeySet = true;
}
short copyReaderPubKey(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
readerPubKey, (short) 0, dst, dstOff, READER_PUBK_LEN);
return READER_PUBK_LEN;
}
/** Copies only the 32-byte x-coordinate of the reader_PubK. */
short copyReaderPubKeyX(byte[] dst, short dstOff) {
javacard.framework.Util.arrayCopyNonAtomic(
readerPubKey, (short) 0, dst, dstOff, (short) 32);
return (short) 32;
}
/**
* Stores an Access Document chunk at the given destination offset.
* Returns true iff the write stayed within {@link #ACCESS_DOC_MAX_LEN};
* otherwise the store is unchanged.
*/
boolean writeAccessDocumentChunk(byte[] src, short srcOff, short dstOff, short len) {
if ((short) (dstOff + len) > ACCESS_DOC_MAX_LEN || dstOff < 0 || len < 0) {
return false;
}
javacard.framework.Util.arrayCopyNonAtomic(src, srcOff, accessDocument, dstOff, len);
return true;
}
/**
* Marks the Access Document as provisioned with {@code totalLen} bytes
* of valid content starting at offset 0. Returns false (and does not
* mutate state) if {@code totalLen} is outside {@code [0, ACCESS_DOC_MAX_LEN]}.
*
* <p>TODO (Step-Up impl, opt 4a): wrap this in
* {@code JCSystem.beginTransaction()} along with a one-shot IssuerAuth
* COSE_Sign1 verify against the stored Credential Issuer public key, and
* set a persistent {@code accessDocumentVerified} flag. Caching the
* verify result saves ~100 ms per Step-Up transaction at the cost of one
* extra persistent byte + the assumption that the Credential Issuer
* trust anchor is fixed for the card's lifetime (opt 4b — true for DT's
* implantable use case but document the limitation). The verify-flag
* write and the {@code accessDocumentFinalized} flip MUST land in the
* same atomic transaction so partial state can't ship an unverified
* document marked verified.
*/
boolean finalizeAccessDocument(short totalLen) {
if (totalLen < 0 || totalLen > ACCESS_DOC_MAX_LEN) {
return false;
}
accessDocumentLen = totalLen;
accessDocumentFinalized = true;
return true;
}
boolean hasAccessDocument() {
return accessDocumentFinalized;
}
void markAccessDocumentVerified() {
accessDocumentVerified = true;
}
boolean isAccessDocumentVerified() {
return accessDocumentVerified;
}
short getAccessDocumentLen() {
return accessDocumentLen;
}
short copyAccessDocument(byte[] dst, short dstOff, short srcOff, short len) {
javacard.framework.Util.arrayCopyNonAtomic(accessDocument, srcOff, dst, dstOff, len);
return len;
}
/**
* Writes the full credential store state to the given sink in
* {@link #FIELD_VERSION} layout. The field order is locked — see the
* FIELD_VERSION javadoc.
*/
void writeTo(SerializationSink sink) {
sink.write(FIELD_VERSION);
sink.write(committed);
sink.write(credentialPrivKeySet);
sink.write(credentialPubKeySet);
sink.write(readerPubKeySet);
sink.write(accessDocumentFinalized);
sink.write(accessDocumentVerified);
sink.write(accessDocumentLen);
sink.write(credentialPrivKey);
sink.write(credentialPubKey);
sink.write(readerPubKey);
sink.write(accessDocument);
}
/**
* Reads a full credential store snapshot from the given source written
* by {@link #writeTo(SerializationSink)}.
*/
static CredentialStore readFrom(SerializationSource src) {
byte v = src.readByte();
if (v != FIELD_VERSION) {
javacard.framework.ISOException.throwIt(
javacard.framework.ISO7816.SW_DATA_INVALID);
}
CredentialStore s = new CredentialStore();
s.committed = src.readBoolean();
s.credentialPrivKeySet = src.readBoolean();
s.credentialPubKeySet = src.readBoolean();
s.readerPubKeySet = src.readBoolean();
s.accessDocumentFinalized = src.readBoolean();
s.accessDocumentVerified = src.readBoolean();
s.accessDocumentLen = src.readShort();
byte[] a;
a = src.readByteArray();
javacard.framework.Util.arrayCopy(a, (short) 0, s.credentialPrivKey, (short) 0, CRED_PRIV_KEY_LEN);
a = src.readByteArray();
javacard.framework.Util.arrayCopy(a, (short) 0, s.credentialPubKey, (short) 0, CRED_PUBK_LEN);
a = src.readByteArray();
javacard.framework.Util.arrayCopy(a, (short) 0, s.readerPubKey, (short) 0, READER_PUBK_LEN);
a = src.readByteArray();
javacard.framework.Util.arrayCopy(a, (short) 0, s.accessDocument, (short) 0, ACCESS_DOC_MAX_LEN);
return s;
}
/** Test-only: returns a fresh byte[] copy of the credential private key. */
byte[] copyCredentialPrivKey() {
byte[] out = new byte[CRED_PRIV_KEY_LEN];
javacard.framework.Util.arrayCopyNonAtomic(
credentialPrivKey, (short) 0, out, (short) 0, CRED_PRIV_KEY_LEN);
return out;
}
/** Test-only: returns a fresh byte[] copy of the credential public key. */
byte[] copyCredentialPubKey() {
byte[] out = new byte[CRED_PUBK_LEN];
javacard.framework.Util.arrayCopyNonAtomic(
credentialPubKey, (short) 0, out, (short) 0, CRED_PUBK_LEN);
return out;
}
/** Test-only: returns a fresh byte[] copy of the reader public key. */
byte[] copyReaderPubKey() {
byte[] out = new byte[READER_PUBK_LEN];
javacard.framework.Util.arrayCopyNonAtomic(
readerPubKey, (short) 0, out, (short) 0, READER_PUBK_LEN);
return out;
}
/** Test-only: returns a fresh byte[] copy of the active Access Document
* bytes (length = {@link #accessDocumentLen}, not the full buffer). */
byte[] copyAccessDocument() {
byte[] out = new byte[accessDocumentLen];
javacard.framework.Util.arrayCopyNonAtomic(
accessDocument, (short) 0, out, (short) 0, accessDocumentLen);
return out;
}
/**
* Test-only hook. The store is a static singleton, which makes unit
* tests interfere with each other unless each test starts from a
* known-blank slate. Package-private and named for clarity.
*/
void resetForTesting() {
javacard.framework.Util.arrayFillNonAtomic(credentialPrivKey, (short) 0, CRED_PRIV_KEY_LEN, (byte) 0);
javacard.framework.Util.arrayFillNonAtomic(credentialPubKey, (short) 0, CRED_PUBK_LEN, (byte) 0);
javacard.framework.Util.arrayFillNonAtomic(readerPubKey, (short) 0, READER_PUBK_LEN, (byte) 0);
javacard.framework.Util.arrayFillNonAtomic(accessDocument, (short) 0, ACCESS_DOC_MAX_LEN, (byte) 0);
credentialPrivKeySet = false;
credentialPubKeySet = false;
readerPubKeySet = false;
accessDocumentLen = 0;
accessDocumentFinalized = false;
accessDocumentVerified = false;
committed = false;
}
}