package com.dangerousthings.aliro; import java.util.ArrayList; import java.util.List; import javacard.framework.ISO7816; import javacard.framework.ISOException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; 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_B = makeRange(64, 0x80); private static final byte[] KNOWN_64_BYTES_ISSUER = makeRange(64, 0x40); 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; } @Test void credentialStoreRoundTripsViaSerializationContract() { // Deliberately distinguishable mix (T/T/F/T/F) so that a silent // reorder among the boolean writeTo lines would change which field // ends up true/false after readFrom — and fail the per-predicate // assertions below. If all five booleans were true the reorder // would be undetectable. CredentialStore src = CredentialStore.bootstrap(); src.resetForTesting(); src.setCredentialPrivKey(KNOWN_32_BYTES, (short) 0); // Intentionally do NOT setCredentialPubKey: leave credentialPubKeySet=false. src.setReaderPubKey(KNOWN_64_BYTES_B, (short) 0); // Exercise the new credentialIssuerPubKey slot (M2A.3, FIELD_VERSION=3). src.setCredentialIssuerPubKey(KNOWN_64_BYTES_ISSUER, (short) 0); // Intentionally do NOT finalizeAccessDocument: leave accessDocumentFinalized=false // and accessDocumentLen=0. With len=0, copyAccessDocument() returns an empty array. src.commit(); RecordingSink buf = new RecordingSink(); src.writeTo(buf); CredentialStore restored = CredentialStore.readFrom(buf.toSource()); // Byte-array fields that were set must round-trip exactly. assertArrayEquals(KNOWN_32_BYTES, restored.copyCredentialPrivKey()); assertArrayEquals(KNOWN_64_BYTES_B, restored.copyReaderPubKey()); // Issuer pubkey round-trips as 0x04 || x || y (uncompressed SEC1). byte[] expectedIssuerUncomp = new byte[65]; expectedIssuerUncomp[0] = 0x04; System.arraycopy(KNOWN_64_BYTES_ISSUER, 0, expectedIssuerUncomp, 1, 64); byte[] gotIssuerUncomp = new byte[65]; restored.copyCredentialIssuerPubKeyUncomp(gotIssuerUncomp, (short) 0); assertArrayEquals(expectedIssuerUncomp, gotIssuerUncomp); assertTrue(restored.hasCredentialIssuerPubKey()); // The unset credential pubkey buffer should round-trip as all-zero — // resetForTesting cleared it and we never wrote to it. assertArrayEquals(new byte[CredentialStore.CRED_PUBK_LEN], restored.copyCredentialPubKey()); // accessDocumentLen=0, so copyAccessDocument() returns an empty array. assertArrayEquals(new byte[0], restored.copyAccessDocument()); // Per-predicate assertions: any swap in the boolean section of // writeTo flips at least one of these. assertTrue(restored.isCommitted()); assertTrue(restored.hasCredentialPrivKey()); assertFalse(restored.hasCredentialPubKey()); assertTrue(restored.hasReaderPubKey()); assertFalse(restored.hasAccessDocument()); } @Test void accessDocumentVerifiedRoundTripsThroughSerialize() { CredentialStore s1 = CredentialStore.bootstrap(); s1.resetForTesting(); s1.markAccessDocumentVerifiedForTesting(); RecordingSink buf = new RecordingSink(); s1.writeTo(buf); CredentialStore s2 = CredentialStore.readFrom(buf.toSource()); assertTrue(s2.isAccessDocumentVerified(), "verified flag must survive serialize/deserialize round-trip"); } /** * Forges a payload with a bogus FIELD_VERSION byte and asserts that * {@link CredentialStore#readFrom} rejects it with * {@code ISOException(SW_DATA_INVALID)}. The version check is the very * first read in readFrom, so no further padding is required — the * replay source is never read past the version byte. */ @Test void readFromRejectsWrongFieldVersion() { RecordingSink buf = new RecordingSink(); buf.write((byte) 0x7F); // != FIELD_VERSION ISOException ex = assertThrows(ISOException.class, () -> CredentialStore.readFrom(buf.toSource())); assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason()); } /** * 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 events = new ArrayList(); @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 events; private int idx; ReplaySource(List 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++); } } }