1import { describe, expect, it } from "vitest";23import {4 buildAuthenticatorData,5 buildClientDataJSON,6 createAttestationObject,7 generatePasskeyKeypair,8 importPasskeyPrivateKey,9 publicKeyJwkToCose,10 signPasskeyAssertion,11} from "./passkey-crypto";1213describe("passkey crypto", () => {14 it("generates importable ES256 passkey material", async () => {15 const generated = await generatePasskeyKeypair(-7);16 const privateKey = await crypto.subtle.importKey(17 "jwk",18 generated.privateKeyJwk,19 { name: "ECDSA", namedCurve: "P-256" },20 true,21 ["sign"]22 );23 const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", privateKey));24 const imported = await importPasskeyPrivateKey(pkcs8);2526 expect(imported.algorithm).toBe(-7);27 expect(imported.privateKeyJwk.kty).toBe("EC");28 expect(imported.publicKeyCose.length).toBeGreaterThan(32);29 expect(generated.publicKeySpki.length).toBeGreaterThan(64);30 });3132 it("generatePasskeyKeypair returns unique keypairs across calls", async () => {33 const first = await generatePasskeyKeypair(-7);34 const second = await generatePasskeyKeypair(-7);3536 expect(first.privateKeyJwk.d).not.toBe(second.privateKeyJwk.d);37 expect(first.publicKeySpki).not.toEqual(second.publicKeySpki);38 });3940 it("builds authenticator data and signs an assertion", async () => {41 const generated = await generatePasskeyKeypair(-7);42 const credentialId = crypto.getRandomValues(new Uint8Array(32));43 const registrationAuthData = await buildAuthenticatorData({44 rpId: "webauthn.io",45 flags: 0x4d,46 signCount: 0,47 credentialId,48 publicKeyCose: generated.publicKeyCose,49 });50 const assertionAuthData = await buildAuthenticatorData({51 rpId: "webauthn.io",52 flags: 0x05,53 signCount: 1,54 });55 const clientDataJSON = buildClientDataJSON(56 "webauthn.get",57 "ZmFrZS1jaGFsbGVuZ2U",58 "https://webauthn.io"59 );60 const clientDataHash = new Uint8Array(61 await crypto.subtle.digest("SHA-256", new Uint8Array(clientDataJSON))62 );63 const signature = await signPasskeyAssertion(64 generated.privateKeyJwk,65 -7,66 assertionAuthData,67 clientDataHash68 );6970 expect(registrationAuthData.length).toBeGreaterThan(80);71 expect(assertionAuthData.length).toBe(37);72 expect(signature.length).toBeGreaterThan(40);73 });7475 it("signPasskeyAssertion produces a signature verifiable with the corresponding public key", async () => {76 const generated = await generatePasskeyKeypair(-7);77 const authenticatorData = await buildAuthenticatorData({78 rpId: "webauthn.io",79 flags: 0x05,80 signCount: 1,81 });82 const clientDataJSON = buildClientDataJSON(83 "webauthn.get",84 "ZmFrZS1jaGFsbGVuZ2U",85 "https://webauthn.io"86 );87 const clientDataHash = new Uint8Array(88 await crypto.subtle.digest("SHA-256", clientDataJSON.buffer as ArrayBuffer)89 );90 const signature = await signPasskeyAssertion(91 generated.privateKeyJwk,92 -7,93 authenticatorData,94 clientDataHash95 );96 const publicKey = await crypto.subtle.importKey(97 "spki",98 generated.publicKeySpki.buffer as ArrayBuffer,99 { name: "ECDSA", namedCurve: "P-256" },100 false,101 ["verify"]102 );103104 const verified = await crypto.subtle.verify(105 { name: "ECDSA", hash: "SHA-256" },106 publicKey,107 signature.buffer as ArrayBuffer,108 new Uint8Array([...authenticatorData, ...clientDataHash]).buffer as ArrayBuffer109 );110111 expect(verified).toBe(true);112 });113114 it("buildAuthenticatorData embeds the rpId hash, flags, and signCount", async () => {115 const authData = await buildAuthenticatorData({116 rpId: "webauthn.io",117 flags: 0x05,118 signCount: 7,119 });120 const expectedHash = new Uint8Array(121 await crypto.subtle.digest(122 "SHA-256",123 new TextEncoder().encode("webauthn.io").buffer as ArrayBuffer124 )125 );126127 expect(authData.slice(0, 32)).toEqual(expectedHash);128 expect(authData[32]).toBe(0x05);129 expect(new DataView(authData.buffer).getUint32(33, false)).toBe(7);130 });131132 it("createAttestationObject includes fmt:none and the supplied authData", async () => {133 const authData = await buildAuthenticatorData({134 rpId: "webauthn.io",135 flags: 0x41,136 signCount: 0,137 });138 const attestation = createAttestationObject(authData);139 const ascii = new TextDecoder().decode(attestation);140141 expect(ascii).toContain("fmt");142 expect(ascii).toContain("none");143 expect(ascii).toContain("authData");144 expect(attestation.includes(authData[0])).toBe(true);145 });146147 it("publicKeyJwkToCose encodes a P-256 public key into COSE bytes", async () => {148 const generated = await generatePasskeyKeypair(-7);149 const cose = publicKeyJwkToCose(generated.privateKeyJwk, -7);150151 expect(cose[0]).toBeGreaterThanOrEqual(0xa0);152 expect(cose.length).toBeGreaterThan(16);153 });154});