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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user