diff --git a/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java b/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java index f8c5ca6..99945a9 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java +++ b/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java @@ -33,6 +33,11 @@ final class CredentialStore { * 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 = 1; + /** 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 @@ -194,6 +199,87 @@ final class CredentialStore { 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(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.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 diff --git a/applet/src/main/java/com/dangerousthings/aliro/SerializationSink.java b/applet/src/main/java/com/dangerousthings/aliro/SerializationSink.java new file mode 100644 index 0000000..c581d89 --- /dev/null +++ b/applet/src/main/java/com/dangerousthings/aliro/SerializationSink.java @@ -0,0 +1,23 @@ +package com.dangerousthings.aliro; + +/** + * Minimal write-side abstraction matching the shape of the GlobalPlatform + * Amendment H {@code org.globalplatform.upgrade.Element} write API. The + * AMD-H adapter (added in a later epic, J3R452-only) is a one-class bridge + * that implements this interface against the real {@code Element}. + * + *
Keeping the interface in the main source tree means + * {@link CredentialStore#writeTo(SerializationSink)} can be authored, + * tested, and locked down today without pulling in the AMD-H package, which + * only exists on JCOP 4.5 / J3R452-class cards. + * + *
Element semantics: once a byte[] reference is written, the storage + * provider treats it as immutable from the caller's perspective. Test + * fakes that buffer writes should make defensive copies to mirror this. + */ +interface SerializationSink { + SerializationSink write(byte b); + SerializationSink write(short s); + SerializationSink write(boolean z); + SerializationSink write(byte[] arr); +} diff --git a/applet/src/main/java/com/dangerousthings/aliro/SerializationSource.java b/applet/src/main/java/com/dangerousthings/aliro/SerializationSource.java new file mode 100644 index 0000000..36d0e51 --- /dev/null +++ b/applet/src/main/java/com/dangerousthings/aliro/SerializationSource.java @@ -0,0 +1,13 @@ +package com.dangerousthings.aliro; + +/** + * Minimal read-side abstraction matching the shape of the GlobalPlatform + * Amendment H {@code org.globalplatform.upgrade.Element} read API. See + * {@link SerializationSink} for the rationale. + */ +interface SerializationSource { + byte readByte(); + short readShort(); + boolean readBoolean(); + byte[] readByteArray(); +} diff --git a/applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java b/applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java new file mode 100644 index 0000000..b3b505d --- /dev/null +++ b/applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java @@ -0,0 +1,144 @@ +package com.dangerousthings.aliro; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Locks down the byte-for-byte field order written by + * {@link CredentialStore#writeTo(SerializationSink)} / read back by + * {@link CredentialStore#readFrom(SerializationSource)}. + * + *
This test exists today against an in-tree + * {@link SerializationSink}/{@link SerializationSource} pair (see + * {@code RecordingSink} below) so the field shape is testable before the + * GP Amendment H {@code org.globalplatform.upgrade.Element} adapter is + * wired in. When the AMD-H adapter lands, it just bridges {@code Element} + * to those two interfaces and this test stays the regression spec. + */ +class CredentialStoreSerializationTest { + + private static final byte[] KNOWN_32_BYTES = makeRange(32, 0xA0); + private static final byte[] KNOWN_64_BYTES = makeRange(64, 0x10); + private static final byte[] KNOWN_64_BYTES_B = makeRange(64, 0x80); + private static final byte[] KNOWN_DOC_BYTES = makeDoc(128); + + private static byte[] makeRange(int len, int start) { + byte[] out = new byte[len]; + for (int i = 0; i < len; i++) { + out[i] = (byte) (start + i); + } + return out; + } + + private static byte[] makeDoc(int len) { + byte[] out = new byte[len]; + for (int i = 0; i < len; i++) { + // A distinguishable, non-trivial pattern that isn't a simple + // run — flushes out off-by-one read/write bugs. + out[i] = (byte) ((i * 31 + 7) & 0xFF); + } + return out; + } + + @Test + void credentialStoreRoundTripsViaSerializationContract() { + CredentialStore src = CredentialStore.bootstrap(); + src.resetForTesting(); + src.setCredentialPrivKey(KNOWN_32_BYTES, (short) 0); + src.setCredentialPubKey(KNOWN_64_BYTES, (short) 0); + src.setReaderPubKey(KNOWN_64_BYTES_B, (short) 0); + src.writeAccessDocumentChunk(KNOWN_DOC_BYTES, (short) 0, (short) 0, + (short) KNOWN_DOC_BYTES.length); + src.finalizeAccessDocument((short) KNOWN_DOC_BYTES.length); + src.commit(); + + RecordingSink buf = new RecordingSink(); + src.writeTo(buf); + + CredentialStore restored = CredentialStore.readFrom(buf.toSource()); + + assertArrayEquals(KNOWN_32_BYTES, restored.copyCredentialPrivKey()); + assertArrayEquals(KNOWN_64_BYTES, restored.copyCredentialPubKey()); + assertArrayEquals(KNOWN_64_BYTES_B, restored.copyReaderPubKey()); + assertArrayEquals(KNOWN_DOC_BYTES, restored.copyAccessDocument()); + assertTrue(restored.isCommitted()); + } + + /** + * Test-only buffer that captures sink writes as a typed event log and + * replays them as a {@link SerializationSource}. Mirrors AMD-H + * {@code Element} semantics by making a defensive copy of every + * {@code byte[]} handed to {@link #write(byte[])} — once written, the + * stored data must not be affected by subsequent caller mutations. + */ + private static final class RecordingSink implements SerializationSink { + private final List