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:
michael
2026-05-26 10:50:47 -07:00
parent 1f54a690d0
commit 40a079a539
4 changed files with 266 additions and 0 deletions

View File

@@ -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

View File

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

View File

@@ -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();
}