feat(applet): CoseVerifier RFC 9052 verify (M2A.2)

Adds CoseVerifier.verifyCoseSign1 — single-purpose RFC 9052 COSE_Sign1
verifier for the Aliro IssuerAuth signature, used once at personalization
(M2A.3) to set CredentialStore.accessDocumentVerified.

Wire walk uses hardcoded offsets that assume the
aliro_harness.issuer.cose shape (matches RFC 9052 §3 but is not a generic
CBOR parser). TODO marker for M2B.3 to swap in StructuralCbor.elementSpan.

Extracts SECP256R1_{P,A,B,G,R} into Secp256r1Params so AliroApplet and
CoseVerifier share one copy. seed(KeyPair) handles full keypair seeding;
seedPublic(ECPublicKey) covers the verify-only case for the new verifier.

Test vector: deterministic-seeded P-256 key + literal payload, signed
once via the harness cose helpers and self-verified, hex-pasted into
CoseVerifierTest. Two cases (good signature; flipped payload byte).

Regression: 80 tests green; the 3 pre-existing GCM errors from the
jcardsim m2-volume swap (AliroCryptoTest#jcardsimSupportsAesGcm,
AliroGcmTest#{partialFinalBlock,roundTripAgainstJcardsimAEADCipher})
still error, unrelated to this change.
This commit is contained in:
michael
2026-06-17 14:07:47 -07:00
parent 531b1d3186
commit 3868678bf9
4 changed files with 486 additions and 55 deletions

View File

@@ -0,0 +1,86 @@
package com.dangerousthings.aliro;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for {@link CoseVerifier} — RFC 9052 COSE_Sign1 verification over
* ECDSA P-256 + SHA-256, used to authenticate the Aliro Access Document
* IssuerAuth signature.
*
* <p>Test vector generated once by harness/src/aliro_harness/issuer/cose.py
* using a deterministic private key seed ("M2A.2-cose-verifier-test-key!!!_"
* reduced mod n) and the literal payload "M2A.2 known-good payload".
* Self-verified via verify_cose_sign1 before being copied here.
*
* <p>ECDSA k is randomised inside the python {@code cryptography} library;
* regenerating the vector would produce a different signature. The hex
* constants below are the canonical M2A.2 known-answer pair.
*/
class CoseVerifierTest {
/** secp256r1 public key, uncompressed SEC1 (0x04 || X(32) || Y(32)). */
private static final byte[] ISSUER_PUB_UNCOMP = hex(
"04d28f7bbdb8bc7c4d1bbcb7b76e6bdffaa4994f978f316df8d5341bf164e36b82"
+ "76f90f72b5680bfd21901de452ea133f35ef44fd34e5b14ddb9a7e14ca700a0a");
/**
* COSE_Sign1 wire bytes (CBOR-encoded 4-element array) for the test
* vector above. Layout:
* <pre>
* [ 0] 0x84 outer array(4)
* [ 1] 0x43 protected bstr header (len 3)
* [ 2..4] a1 01 26 {1: -7} → ES256 alg id
* [ 5] 0xa1 unprotected map(1)
* [ 6] 0x04 key 4 (kid)
* [ 7] 0x48 bstr(8)
* [ 8..15] kid 01 02 03 04 05 06 07 08
* [ 16] 0x58 payload bstr 1-byte len
* [ 17] 0x18 len = 24
* [ 18..41] "M2A.2 known-good payload" (24 bytes)
* [ 42] 0x58 sig bstr 1-byte len
* [ 43] 0x40 len = 64
* [ 44..107] raw r||s ECDSA-P256 signature (64 bytes)
* </pre>
*/
private static final byte[] COSE_SIGN1_GOOD = hex(
"8443a10126a1044801020304050607085818"
+ "4d32412e32206b6e6f776e2d676f6f64207061796c6f6164"
+ "5840"
+ "02e1c7996e16b3c47c020d7894f38f2c3850abc10a1a53dd56051c80412ba203"
+ "447598813bab380e0d286dc55985c24d123a7bc98f1f31419679d4f2dde0e916");
@Test
void verifyCoseSign1_validSignature_returnsTrue() {
CoseVerifier verifier = new CoseVerifier();
boolean ok = verifier.verifyCoseSign1(
COSE_SIGN1_GOOD, (short) 0, (short) COSE_SIGN1_GOOD.length,
ISSUER_PUB_UNCOMP, (short) 0);
assertTrue(ok, "known-good COSE_Sign1 must verify against issuer pubkey");
}
@Test
void verifyCoseSign1_tamperedPayload_returnsFalse() {
// Flip one bit in the payload region (offset 25 = mid-payload).
byte[] tampered = new byte[COSE_SIGN1_GOOD.length];
System.arraycopy(COSE_SIGN1_GOOD, 0, tampered, 0, COSE_SIGN1_GOOD.length);
tampered[25] ^= (byte) 0x01;
CoseVerifier verifier = new CoseVerifier();
boolean ok = verifier.verifyCoseSign1(
tampered, (short) 0, (short) tampered.length,
ISSUER_PUB_UNCOMP, (short) 0);
assertFalse(ok, "tampered payload must NOT verify");
}
private static byte[] hex(String s) {
s = s.replaceAll("\\s+", "");
byte[] out = new byte[s.length() / 2];
for (int i = 0; i < out.length; i++) {
out[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
}
return out;
}
}