Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
import { encode } from "@suaveplan/cbor";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
buildAuthenticatorData,
buildClientDataJSON,
buildGenericEnvelope,
buildPackedSelfEd25519,
buildPackedSelfEs256,
FLAG_AT,
FLAG_UP,
getEcdsaP256TestKey,
getEd25519TestKey,
} from "../../__fixtures__/index.js";
import {
WEBAUTHN_CODES,
type WebAuthnError,
} from "../../errors/error-codes.js";
import { parseAttestationObject } from "../../parser/attestation-object.js";
import { verifyPackedAttestation } from "./packed.js";
const origin = "https://example.com";
const challenge = new Uint8Array(32).fill(0xaa);
function setup() {
const key = getEcdsaP256TestKey();
const authData = buildAuthenticatorData({
rpId: "example.com",
flags: FLAG_UP | FLAG_AT,
credentialId: new Uint8Array(16).fill(0xcc),
credentialPublicKey: key.coseKey,
});
const clientData = buildClientDataJSON({
type: "webauthn.create",
challenge,
origin,
});
return { key, authData, clientData };
}
describe("verifyPackedAttestation (self path)", () => {
it("accepts an ES256 self-attestation good vector", {
timeout: 5000,
}, async () => {
const { key, authData, clientData } = setup();
const env = buildPackedSelfEs256({
authData: authData.bytes,
clientDataHash: clientData.hash,
key,
});
const parsed = parseAttestationObject(env);
const out = await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: authData.rpIdHash,
});
expect(out.attestationType).toBe("self");
expect(out.trustPath).toEqual([]);
});
it("accepts an EdDSA self-attestation good vector", {
timeout: 5000,
}, async () => {
const ed = getEd25519TestKey();
const authData = buildAuthenticatorData({
rpId: "example.com",
flags: FLAG_UP | FLAG_AT,
credentialId: new Uint8Array(16).fill(0xdd),
credentialPublicKey: ed.coseKey,
});
const clientData = buildClientDataJSON({
type: "webauthn.create",
challenge,
origin,
});
const env = buildPackedSelfEd25519({
authData: authData.bytes,
clientDataHash: clientData.hash,
key: ed,
});
const parsed = parseAttestationObject(env);
const out = await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: authData.rpIdHash,
});
expect(out.attestationType).toBe("self");
});
it("rejects self-attestation with tampered signature", {
timeout: 5000,
}, async () => {
const { key, authData, clientData } = setup();
const env = buildPackedSelfEs256({
authData: authData.bytes,
clientDataHash: clientData.hash,
key,
tamperSig: true,
});
const parsed = parseAttestationObject(env);
try {
await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: authData.rpIdHash,
});
} catch (err) {
expect((err as WebAuthnError).code).toBe(
WEBAUTHN_CODES.SIGNATURE_INVALID
);
return;
}
throw new Error("expected throw");
});
it("rejects self-attestation when rpIdHash mismatches", {
timeout: 5000,
}, async () => {
const { key, authData, clientData } = setup();
const env = buildPackedSelfEs256({
authData: authData.bytes,
clientDataHash: clientData.hash,
key,
});
const parsed = parseAttestationObject(env);
try {
await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: new Uint8Array(32).fill(0xee),
});
} catch (err) {
expect((err as WebAuthnError).code).toBe(
WEBAUTHN_CODES.RPID_HASH_MISMATCH
);
return;
}
throw new Error("expected throw");
});
it("rejects self-attestation when statement alg disagrees with credential alg", {
timeout: 5000,
}, async () => {
const { key, authData, clientData } = setup();
// Build envelope with alg=-8 but credential is ES256 (-7).
const env = buildPackedSelfEs256({
authData: authData.bytes,
clientDataHash: clientData.hash,
key,
alg: -8,
});
const parsed = parseAttestationObject(env);
try {
await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: authData.rpIdHash,
});
} catch (err) {
expect((err as WebAuthnError).code).toBe(
WEBAUTHN_CODES.MALFORMED_RESPONSE
);
return;
}
throw new Error("expected throw");
});
it("rejects when credential public key missing", {
timeout: 5000,
}, async () => {
// Authenticator data without AT flag — credentialPublicKey absent.
const authData = buildAuthenticatorData({
rpId: "example.com",
flags: FLAG_UP,
});
const clientData = buildClientDataJSON({
type: "webauthn.create",
challenge,
origin,
});
const attStmt = new Map<string, unknown>([
["alg", -7],
["sig", new Uint8Array(64)],
]);
const env = buildGenericEnvelope("packed", attStmt, authData.bytes);
const parsed = parseAttestationObject(env);
try {
await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: authData.rpIdHash,
});
} catch (err) {
expect((err as WebAuthnError).code).toBe(
WEBAUTHN_CODES.MALFORMED_RESPONSE
);
return;
}
throw new Error("expected throw");
});
});
describe("verifyPackedAttestation (full path with x5c)", () => {
it("accepts and surfaces trustPath on basic attestation", {
timeout: 5000,
}, async () => {
const { key, authData, clientData } = setup();
// Build packed envelope with non-empty x5c — the verifier surfaces
// the chain but does not validate it (chain anchoring is caller's
// responsibility per the source comment).
const attStmt = new Map<string, unknown>();
attStmt.set("alg", -7);
attStmt.set("sig", new Uint8Array(64));
attStmt.set("x5c", [new Uint8Array(32).fill(7)]);
const env = encode(
new Map<string, unknown>([
["fmt", "packed"],
["attStmt", attStmt],
["authData", authData.bytes],
])
);
const parsed = parseAttestationObject(env);
const out = await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: authData.rpIdHash,
});
expect(out.attestationType).toBe("basic");
expect(out.trustPath.length).toBe(1);
void key;
});
it("rejects when x5c array empty", { timeout: 5000 }, async () => {
const { authData, clientData } = setup();
const attStmt = new Map<string, unknown>();
attStmt.set("alg", -7);
attStmt.set("sig", new Uint8Array(64));
attStmt.set("x5c", []);
const env = encode(
new Map<string, unknown>([
["fmt", "packed"],
["attStmt", attStmt],
["authData", authData.bytes],
])
);
const parsed = parseAttestationObject(env);
try {
await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: authData.rpIdHash,
});
} catch (err) {
expect((err as WebAuthnError).code).toBe(
WEBAUTHN_CODES.MALFORMED_RESPONSE
);
return;
}
throw new Error("expected throw");
});
it("rejects rpIdHash mismatch on full attestation", {
timeout: 5000,
}, async () => {
const { authData, clientData } = setup();
const attStmt = new Map<string, unknown>();
attStmt.set("alg", -7);
attStmt.set("sig", new Uint8Array(64));
attStmt.set("x5c", [new Uint8Array(32)]);
const env = encode(
new Map<string, unknown>([
["fmt", "packed"],
["attStmt", attStmt],
["authData", authData.bytes],
])
);
const parsed = parseAttestationObject(env);
try {
await verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: clientData.hash,
expectedRpIdHash: new Uint8Array(32).fill(0xee),
});
} catch (err) {
expect((err as WebAuthnError).code).toBe(
WEBAUTHN_CODES.RPID_HASH_MISMATCH
);
return;
}
throw new Error("expected throw");
});
});
describe("verifyPackedAttestation (malformed statement)", () => {
function buildBadStmt(stmt: Map<string, unknown>) {
const { authData } = setup();
const env = encode(
new Map<string, unknown>([
["fmt", "packed"],
["attStmt", stmt],
["authData", authData.bytes],
])
);
return { env, expectedRpIdHash: authData.rpIdHash };
}
it("rejects missing alg", { timeout: 5000 }, async () => {
const stmt = new Map<string, unknown>([["sig", new Uint8Array(64)]]);
const { env, expectedRpIdHash } = buildBadStmt(stmt);
const parsed = parseAttestationObject(env);
await expect(
verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: new Uint8Array(32),
expectedRpIdHash,
})
).rejects.toThrow();
});
it("rejects missing sig", { timeout: 5000 }, async () => {
const stmt = new Map<string, unknown>([["alg", -7]]);
const { env, expectedRpIdHash } = buildBadStmt(stmt);
const parsed = parseAttestationObject(env);
await expect(
verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: new Uint8Array(32),
expectedRpIdHash,
})
).rejects.toThrow();
});
it("rejects non-array x5c", { timeout: 5000 }, async () => {
const stmt = new Map<string, unknown>([
["alg", -7],
["sig", new Uint8Array(64)],
["x5c", "not-array"],
]);
const { env, expectedRpIdHash } = buildBadStmt(stmt);
const parsed = parseAttestationObject(env);
await expect(
verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: new Uint8Array(32),
expectedRpIdHash,
})
).rejects.toThrow();
});
it("rejects x5c containing a non-bytes element", {
timeout: 5000,
}, async () => {
const stmt = new Map<string, unknown>([
["alg", -7],
["sig", new Uint8Array(64)],
["x5c", ["not-bytes"]],
]);
const { env, expectedRpIdHash } = buildBadStmt(stmt);
const parsed = parseAttestationObject(env);
await expect(
verifyPackedAttestation({
attestationObject: parsed,
clientDataHash: new Uint8Array(32),
expectedRpIdHash,
})
).rejects.toThrow();
});
});