import { describe, expect, it } from "vitest"; import { bytesToArrayBuffer, bytesToBase64Url, utf8ToBytes, } from "../../src/crypto/encoding"; import { verifyAttestation } from "../../src/webauthn/attestation"; function bytesValue(value: Uint8Array): number[] { if (value.length < 24) return [0x40 | value.length, ...value]; if (value.length < 256) return [0x58, value.length, ...value]; return [0x59, value.length >>> 8, value.length & 0xff, ...value]; } function textValue(value: string): number[] { const encoded = utf8ToBytes(value); return [0x60 | encoded.length, ...encoded]; } function noneAttestation( authenticatorData: Uint8Array, format = "none", ): Uint8Array { return Uint8Array.from([ 0xa3, ...textValue("fmt"), ...textValue(format), ...textValue("authData"), ...bytesValue(authenticatorData), ...textValue("attStmt"), 0xa0, ]); } async function registrationData(): Promise<{ attestationObject: string; clientDataJSON: string; challenge: string; }> { const challenge = bytesToBase64Url(Uint8Array.of(1, 2, 3, 4)); const client = utf8ToBytes( JSON.stringify({ type: "webauthn.create", challenge, origin: "https://example.test", crossOrigin: false, }), ); const rpHash = new Uint8Array( await crypto.subtle.digest( "SHA-256", bytesToArrayBuffer(utf8ToBytes("example.test")), ), ); const cose = Uint8Array.from([ 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, ...new Uint8Array(32).fill(1), 0x22, 0x58, 0x20, ...new Uint8Array(32).fill(2), ]); const authData = Uint8Array.from([ ...rpHash, 0x41, 0, 0, 0, 0, ...new Uint8Array(16), 0, 1, 7, ...cose, ]); return { attestationObject: bytesToBase64Url(noneAttestation(authData)), clientDataJSON: bytesToBase64Url(client), challenge, }; } describe("WebAuthn attestation verification", () => { it("verifies a registration with none attestation without claiming trust", async () => { const data = await registrationData(); const result = await verifyAttestation({ ...data, expectedChallenge: data.challenge, expectedOrigin: "https://example.test", expectedRpId: "example.test", }); expect(result).toMatchObject({ verified: true, format: "none", attestationType: "none", trustEstablished: false, }); }); it("rejects mismatched ceremony state", async () => { const data = await registrationData(); const result = await verifyAttestation({ ...data, expectedChallenge: "different", expectedOrigin: "https://evil.test", expectedRpId: "evil.test", }); expect(result.verified).toBe(false); expect( result.checks .filter((check) => check.status === "fail") .map((check) => check.name), ).toEqual(expect.arrayContaining(["Challenge", "Origin", "RP ID hash"])); }); });