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

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