Files
aliro-project/applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java
michael d24d72e119 chore(applet): drop M1 stub comments + dead paths + naming consistency (M2F.2)
Sweep stale M1-era cruft and milestone task markers obsoleted by M2:

- StepUpApplet class Javadoc: fix stale @link target for
  finalizeAccessDocument (now takes verifier+scratch65), drop "TODO"
  prose for shipped M2A.3 verify-at-finalize, rewrite "INS_EXCHANGE stub
  was retired" past-tense note as present-tense ownership statement.
- StepUpApplet: drop "M1B / M1C" / "M2E.1" / "M2E.2" / "M2D.3+M2D.4"
  task markers from comments and method Javadocs; describe current
  behavior, not milestone provenance.
- StepUpApplet: rewrite "decrypt-and-discard" sink Javadoc — that sink
  now also stages the EXCHANGE Reader Status request plaintext.
- StepUpSession: drop "matching M1 behaviour" Javadoc trailers.
- AliroApplet bitmap comment: align with the new method name and drop
  the M1 historical aside that's now adjacent to the only behavior left.
- AliroAppletTest: rewrite "Once M1B.1 / M1C.1 land..." as present tense.
- CredentialStore.markAccessDocumentVerified() → ForTesting suffix to
  match the sibling markAccessDocumentFinalizedForTesting() and add an
  explanatory Javadoc clarifying that production callers reach the
  verified state via finalizeAccessDocument(short, CoseVerifier, byte[]).
  Two test call sites updated.

No behavior change. Tests: 147 / 0 / 3 pre-existing GCM errors. Net
diff +46/-44 = +2 LOC (the rename's explanatory Javadoc minus the cruft).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 17:01:12 -07:00

200 lines
7.5 KiB
Java

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)}.
*
* <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_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<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++);
}
}
}