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>
200 lines
7.5 KiB
Java
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++);
|
|
}
|
|
}
|
|
}
|