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";12131415function derEcdsaToP1363(der: Uint8Array, coordSize: number): Uint8Array {16 if (der[0] !== 0x30) throw new Error("not a SEQUENCE");17 let off = 2;18 if (der[1]! & 0x80) off = 2 + (der[1]! & 0x7f);19 if (der[off] !== 0x02) throw new Error("missing r INTEGER");20 const rLen = der[off + 1]!;21 let r = der.slice(off + 2, off + 2 + rLen);22 off = off + 2 + rLen;23 if (der[off] !== 0x02) throw new Error("missing s INTEGER");24 const sLen = der[off + 1]!;25 let s = der.slice(off + 2, off + 2 + sLen);26 if (r.length > coordSize) r = r.slice(r.length - coordSize);27 if (s.length > coordSize) s = s.slice(s.length - coordSize);28 const out = new Uint8Array(coordSize * 2);29 out.set(r, coordSize - r.length);30 out.set(s, coordSize * 2 - s.length);31 return out;32}3334describe("passkey crypto", () => {35 it("generates importable ES256 passkey material", async () => {36 const generated = await generatePasskeyKeypair(-7);37 const privateKey = await crypto.subtle.importKey(38 "jwk",39 generated.privateKeyJwk,40 { name: "ECDSA", namedCurve: "P-256" },41 true,42 ["sign"]43 );44 const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", privateKey));45 const imported = await importPasskeyPrivateKey(pkcs8);4647 expect(imported.algorithm).toBe(-7);48 expect(imported.privateKeyJwk.kty).toBe("EC");49 expect(imported.publicKeyCose.length).toBeGreaterThan(32);50 expect(generated.publicKeySpki.length).toBeGreaterThan(64);51 });5253 it("generatePasskeyKeypair returns unique keypairs across calls", async () => {54 const first = await generatePasskeyKeypair(-7);55 const second = await generatePasskeyKeypair(-7);5657 expect(first.privateKeyJwk.d).not.toBe(second.privateKeyJwk.d);58 expect(first.publicKeySpki).not.toEqual(second.publicKeySpki);59 });6061 it("builds authenticator data and signs an assertion", async () => {62 const generated = await generatePasskeyKeypair(-7);63 const credentialId = crypto.getRandomValues(new Uint8Array(32));64 const registrationAuthData = await buildAuthenticatorData({65 rpId: "webauthn.io",66 flags: 0x4d,67 signCount: 0,68 credentialId,69 publicKeyCose: generated.publicKeyCose,70 });71 const assertionAuthData = await buildAuthenticatorData({72 rpId: "webauthn.io",73 flags: 0x05,74 signCount: 1,75 });76 const clientDataJSON = buildClientDataJSON(77 "webauthn.get",78 "ZmFrZS1jaGFsbGVuZ2U",79 "https://webauthn.io"80 );81 const clientDataHash = new Uint8Array(82 await crypto.subtle.digest("SHA-256", new Uint8Array(clientDataJSON))83 );84 const signature = await signPasskeyAssertion(85 generated.privateKeyJwk,86 -7,87 assertionAuthData,88 clientDataHash89 );9091 expect(registrationAuthData.length).toBeGreaterThan(80);92 expect(assertionAuthData.length).toBe(37);93 expect(signature.length).toBeGreaterThan(40);94 });9596 it("signPasskeyAssertion produces a signature verifiable with the corresponding public key", async () => {97 const generated = await generatePasskeyKeypair(-7);98 const authenticatorData = await buildAuthenticatorData({99 rpId: "webauthn.io",100 flags: 0x05,101 signCount: 1,102 });103 const clientDataJSON = buildClientDataJSON(104 "webauthn.get",105 "ZmFrZS1jaGFsbGVuZ2U",106 "https://webauthn.io"107 );108 const clientDataHash = new Uint8Array(109 await crypto.subtle.digest("SHA-256", clientDataJSON.buffer as ArrayBuffer)110 );111 const signature = await signPasskeyAssertion(112 generated.privateKeyJwk,113 -7,114 authenticatorData,115 clientDataHash116 );117 const publicKey = await crypto.subtle.importKey(118 "spki",119 generated.publicKeySpki.buffer as ArrayBuffer,120 { name: "ECDSA", namedCurve: "P-256" },121 false,122 ["verify"]123 );124125 126 127 const raw = derEcdsaToP1363(signature, 32);128 const verified = await crypto.subtle.verify(129 { name: "ECDSA", hash: "SHA-256" },130 publicKey,131 raw.buffer as ArrayBuffer,132 new Uint8Array([...authenticatorData, ...clientDataHash]).buffer as ArrayBuffer133 );134135 expect(verified).toBe(true);136 });137138 it("buildAuthenticatorData embeds the rpId hash, flags, and signCount", async () => {139 const authData = await buildAuthenticatorData({140 rpId: "webauthn.io",141 flags: 0x05,142 signCount: 7,143 });144 const expectedHash = new Uint8Array(145 await crypto.subtle.digest(146 "SHA-256",147 new TextEncoder().encode("webauthn.io").buffer as ArrayBuffer148 )149 );150151 expect(authData.slice(0, 32)).toEqual(expectedHash);152 expect(authData[32]).toBe(0x05);153 expect(new DataView(authData.buffer).getUint32(33, false)).toBe(7);154 });155156 it("createAttestationObject includes fmt:none and the supplied authData", async () => {157 const authData = await buildAuthenticatorData({158 rpId: "webauthn.io",159 flags: 0x41,160 signCount: 0,161 });162 const attestation = createAttestationObject(authData);163 const ascii = new TextDecoder().decode(attestation);164165 expect(ascii).toContain("fmt");166 expect(ascii).toContain("none");167 expect(ascii).toContain("authData");168 expect(attestation.includes(authData[0])).toBe(true);169 });170171 it("publicKeyJwkToCose encodes a P-256 public key into COSE bytes", async () => {172 const generated = await generatePasskeyKeypair(-7);173 const cose = publicKeyJwkToCose(generated.privateKeyJwk, -7);174175 expect(cose[0]).toBeGreaterThanOrEqual(0xa0);176 expect(cose.length).toBeGreaterThan(16);177 });178});