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. * *

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. * *

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. * *

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]}. * *

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; } }