feat: define CredentialStore serialization contract
Adds in-tree SerializationSink/Source interfaces matching the shape of the AMD-H Element API, plus writeTo/readFrom on CredentialStore and a round-trip test. The AMD-H adapter (added in a later epic) just bridges Element -> these interfaces, so the field layout is testable today without depending on a JCOP 4.5 image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,11 @@ final class CredentialStore {
|
|||||||
* accommodates a typical Aliro Access Document with room to spare. */
|
* accommodates a typical Aliro Access Document with room to spare. */
|
||||||
static final short ACCESS_DOC_MAX_LEN = 1024;
|
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()}.
|
/** Publish-point read by AliroApplet/StepUpApplet via {@link #get()}.
|
||||||
* PersonalizationApplet owns the actual instance; this is just an alias
|
* PersonalizationApplet owns the actual instance; this is just an alias
|
||||||
* so other applets in the same package can find it without SIO. The
|
* so other applets in the same package can find it without SIO. The
|
||||||
@@ -194,6 +199,87 @@ final class CredentialStore {
|
|||||||
return 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(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
|
* Test-only hook. The store is a static singleton, which makes unit
|
||||||
* tests interfere with each other unless each test starts from a
|
* tests interfere with each other unless each test starts from a
|
||||||
|
|||||||
@@ -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}.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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)}.
|
||||||
|
*
|
||||||
|
* <p>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<Object> events = new ArrayList<Object>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SerializationSink write(byte b) {
|
||||||
|
events.add(Byte.valueOf(b));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SerializationSink write(short s) {
|
||||||
|
events.add(Short.valueOf(s));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SerializationSink write(boolean z) {
|
||||||
|
events.add(Boolean.valueOf(z));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SerializationSink write(byte[] arr) {
|
||||||
|
// Defensive copy: Element treats stored references as
|
||||||
|
// immutable from the caller's perspective.
|
||||||
|
byte[] copy = new byte[arr.length];
|
||||||
|
System.arraycopy(arr, 0, copy, 0, arr.length);
|
||||||
|
events.add(copy);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
SerializationSource toSource() {
|
||||||
|
return new ReplaySource(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ReplaySource implements SerializationSource {
|
||||||
|
private final List<Object> events;
|
||||||
|
private int idx;
|
||||||
|
|
||||||
|
ReplaySource(List<Object> events) {
|
||||||
|
this.events = events;
|
||||||
|
this.idx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte readByte() {
|
||||||
|
return ((Byte) events.get(idx++)).byteValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public short readShort() {
|
||||||
|
return ((Short) events.get(idx++)).shortValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readBoolean() {
|
||||||
|
return ((Boolean) events.get(idx++)).booleanValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] readByteArray() {
|
||||||
|
return (byte[]) events.get(idx++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user