1const textEncoder = new TextEncoder();23function toArrayBuffer(data: Uint8Array): ArrayBuffer {4 const out = new Uint8Array(data.byteLength);5 out.set(data);6 return out.buffer;7}89function encodeLength(majorType: number, value: number): number[] {10 if (value < 24) return [(majorType << 5) | value];11 if (value < 0x100) return [(majorType << 5) | 24, value];12 if (value < 0x1_0000) return [(majorType << 5) | 25, value >> 8, value & 0xff];13 if (value < 0x1_0000_0000) {14 return [15 (majorType << 5) | 26,16 (value >>> 24) & 0xff,17 (value >>> 16) & 0xff,18 (value >>> 8) & 0xff,19 value & 0xff,20 ];21 }2223 throw new Error("CBOR length too large");24}2526function encodeInteger(value: number): Uint8Array {27 if (value >= 0) {28 return Uint8Array.from(encodeLength(0, value));29 }30 return Uint8Array.from(encodeLength(1, -1 - value));31}3233function encodeText(value: string): Uint8Array {34 const bytes = textEncoder.encode(value);35 return concatBytes(Uint8Array.from(encodeLength(3, bytes.length)), bytes);36}3738function encodeBytes(value: Uint8Array): Uint8Array {39 return concatBytes(Uint8Array.from(encodeLength(2, value.length)), value);40}4142function encodeMap(entries: Array<[number | string, unknown]>): Uint8Array {43 const parts: Uint8Array[] = [Uint8Array.from(encodeLength(5, entries.length))];44 for (const [key, value] of entries) {45 parts.push(typeof key === "number" ? encodeInteger(key) : encodeText(key));46 parts.push(encodeCbor(value));47 }48 return concatBytes(...parts);49}5051export function encodeCbor(value: unknown): Uint8Array {52 if (value instanceof Uint8Array) return encodeBytes(value);53 if (typeof value === "string") return encodeText(value);54 if (typeof value === "number" && Number.isInteger(value)) return encodeInteger(value);55 if (value instanceof Map) return encodeMap(Array.from(value.entries()));56 if (Array.isArray(value)) {57 return concatBytes(58 Uint8Array.from(encodeLength(4, value.length)),59 ...value.map((item) => encodeCbor(item))60 );61 }62 if (value && typeof value === "object") {63 return encodeMap(Object.entries(value as Record<string, unknown>));64 }65 if (value === false) return Uint8Array.of(0xf4);66 if (value === true) return Uint8Array.of(0xf5);67 if (value === null) return Uint8Array.of(0xf6);6869 throw new Error("Unsupported CBOR value");70}7172export function concatBytes(...parts: Uint8Array[]): Uint8Array {73 const total = parts.reduce((sum, part) => sum + part.length, 0);74 const out = new Uint8Array(total);75 let offset = 0;76 for (const part of parts) {77 out.set(part, offset);78 offset += part.length;79 }80 return out;81}8283export function base64urlEncode(data: Uint8Array): string {84 let binary = "";85 for (const byte of data) binary += String.fromCharCode(byte);86 return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");87}8889export function base64urlDecode(input: string): Uint8Array {90 const padded = input.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(input.length / 4) * 4, "=");91 const binary = atob(padded);92 const out = new Uint8Array(binary.length);93 for (let i = 0; i < binary.length; i += 1) {94 out[i] = binary.charCodeAt(i);95 }96 return out;97}9899export function pickPasskeyAlgorithm(100 pubKeyCredParams: Array<{ alg: number }>101): -7 | -8 | null {102 const offered = new Set(pubKeyCredParams.map((param) => param.alg));103 if (offered.has(-8)) return -8;104 if (offered.has(-7)) return -7;105 return null;106}107108export function p256RawToCose(raw: Uint8Array): Uint8Array {109 if (raw.length !== 65 || raw[0] !== 0x04) {110 throw new Error("Invalid P-256 public key");111 }112 const x = raw.slice(1, 33);113 const y = raw.slice(33, 65);114 return encodeMap([115 [1, 2],116 [3, -7],117 [-1, 1],118 [-2, x],119 [-3, y],120 ]);121}122123export function ed25519RawToCose(raw: Uint8Array): Uint8Array {124 if (raw.length !== 32) {125 throw new Error("Invalid Ed25519 public key");126 }127 return encodeMap([128 [1, 1],129 [3, -8],130 [-1, 6],131 [-2, raw],132 ]);133}134135export function publicKeyJwkToCose(jwk: JsonWebKey, algorithm: -7 | -8): Uint8Array {136 if (algorithm === -7) {137 if (!jwk.x || !jwk.y) throw new Error("Missing EC coordinates");138 return encodeMap([139 [1, 2],140 [3, -7],141 [-1, 1],142 [-2, base64urlDecode(jwk.x)],143 [-3, base64urlDecode(jwk.y)],144 ]);145 }146147 if (!jwk.x) throw new Error("Missing Ed25519 coordinate");148 return encodeMap([149 [1, 1],150 [3, -8],151 [-1, 6],152 [-2, base64urlDecode(jwk.x)],153 ]);154}155156157158159160161162163164export async function generatePasskeyKeypair(165 algorithm: -7 | -8166): Promise<{167 publicKeyCose: Uint8Array;168 publicKeySpki: Uint8Array;169 privateKeyJwk: JsonWebKey;170}> {171 if (algorithm === -7) {172 const keyPair = await crypto.subtle.generateKey(173 { name: "ECDSA", namedCurve: "P-256" },174 true, 175 ["sign", "verify"]176 );177 const privateKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);178 const publicKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey);179 const publicKeySpki = await crypto.subtle.exportKey("spki", keyPair.publicKey);180 return {181 privateKeyJwk,182 publicKeyCose: p256RawToCose(new Uint8Array(publicKeyRaw)),183 publicKeySpki: new Uint8Array(publicKeySpki),184 };185 }186187 const keyPair = await crypto.subtle.generateKey(188 { name: "Ed25519" },189 true, 190 ["sign", "verify"]191 );192 const privateKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);193 const publicKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey);194 const publicKeySpki = await crypto.subtle.exportKey("spki", keyPair.publicKey);195 return {196 privateKeyJwk,197 publicKeyCose: ed25519RawToCose(new Uint8Array(publicKeyRaw)),198 publicKeySpki: new Uint8Array(publicKeySpki),199 };200}201202async function importPkcs8AsAlgorithm(203 pkcs8: Uint8Array,204 algorithm: -7 | -8205): Promise<{ privateKeyJwk: JsonWebKey; publicKeyCose: Uint8Array; algorithm: -7 | -8 }> {206 const imported =207 algorithm === -7208 ? await crypto.subtle.importKey(209 "pkcs8",210 toArrayBuffer(pkcs8),211 { name: "ECDSA", namedCurve: "P-256" },212 true,213 ["sign"]214 )215 : await crypto.subtle.importKey(216 "pkcs8",217 toArrayBuffer(pkcs8),218 { name: "Ed25519" },219 true,220 ["sign"]221 );222223 const privateKeyJwk = await crypto.subtle.exportKey("jwk", imported);224 return {225 privateKeyJwk,226 publicKeyCose: publicKeyJwkToCose(privateKeyJwk, algorithm),227 algorithm,228 };229}230231export async function importPasskeyPrivateKey(232 pkcs8: Uint8Array233): Promise<{ privateKeyJwk: JsonWebKey; publicKeyCose: Uint8Array; algorithm: -7 | -8 }> {234 try {235 return await importPkcs8AsAlgorithm(pkcs8, -8);236 } catch {237 return importPkcs8AsAlgorithm(pkcs8, -7);238 }239}240241242243244245246247function p1363ToDer(p1363: Uint8Array): Uint8Array {248 const half = p1363.length / 2;249 const r = trimAndPadAsn1Integer(p1363.slice(0, half));250 const s = trimAndPadAsn1Integer(p1363.slice(half));251 const body = concatBytes(252 Uint8Array.of(0x02, r.length),253 r,254 Uint8Array.of(0x02, s.length),255 s256 );257 return concatBytes(Uint8Array.of(0x30, body.length), body);258}259260function trimAndPadAsn1Integer(integer: Uint8Array): Uint8Array {261 let start = 0;262 while (start < integer.length - 1 && integer[start] === 0) start += 1;263 const trimmed = integer.slice(start);264 265 266 if ((trimmed[0]! & 0x80) !== 0) {267 return concatBytes(Uint8Array.of(0x00), trimmed);268 }269 return trimmed;270}271272export async function signPasskeyAssertion(273 privateKeyJwk: JsonWebKey,274 algorithm: -7 | -8,275 authenticatorData: Uint8Array,276 clientDataHash: Uint8Array277): Promise<Uint8Array> {278 const signInput = concatBytes(authenticatorData, clientDataHash);279280 if (algorithm === -7) {281 const key = await crypto.subtle.importKey(282 "jwk",283 privateKeyJwk,284 { name: "ECDSA", namedCurve: "P-256" },285 false,286 ["sign"]287 );288 const raw = await crypto.subtle.sign(289 { name: "ECDSA", hash: "SHA-256" },290 key,291 toArrayBuffer(signInput)292 );293 return p1363ToDer(new Uint8Array(raw));294 }295296 const key = await crypto.subtle.importKey(297 "jwk",298 privateKeyJwk,299 { name: "Ed25519" },300 false,301 ["sign"]302 );303 const signature = await crypto.subtle.sign("Ed25519", key, toArrayBuffer(signInput));304 return new Uint8Array(signature);305}306307export async function buildAuthenticatorData(opts: {308 rpId: string;309 flags: number;310 signCount: number;311 credentialId?: Uint8Array;312 publicKeyCose?: Uint8Array;313}): Promise<Uint8Array> {314 const rpIdHash = new Uint8Array(315 await crypto.subtle.digest("SHA-256", toArrayBuffer(textEncoder.encode(opts.rpId)))316 );317 const signCount = new Uint8Array(4);318 new DataView(signCount.buffer).setUint32(0, opts.signCount, false);319320 const base = concatBytes(rpIdHash, Uint8Array.of(opts.flags), signCount);321 if (!opts.credentialId || !opts.publicKeyCose) {322 return base;323 }324325 const aaguid = new Uint8Array(16);326 const credLength = new Uint8Array(2);327 new DataView(credLength.buffer).setUint16(0, opts.credentialId.length, false);328329 return concatBytes(base, aaguid, credLength, opts.credentialId, opts.publicKeyCose);330}331332export function buildClientDataJSON(333 type: "webauthn.create" | "webauthn.get",334 challenge: string,335 origin: string336): Uint8Array {337 return textEncoder.encode(338 JSON.stringify({339 type,340 challenge,341 origin,342 crossOrigin: false,343 })344 );345}346347export function createAttestationObject(authData: Uint8Array): Uint8Array {348 return encodeMap([349 ["fmt", "none"],350 ["attStmt", new Map()],351 ["authData", authData],352 ]);353}354355export async function sha256(data: Uint8Array): Promise<Uint8Array> {356 return new Uint8Array(await crypto.subtle.digest("SHA-256", toArrayBuffer(data)));357}358359360361362363export function generateCredentialId(): {364 bytes: Uint8Array;365 base64url: string;366} {367 const bytes = crypto.getRandomValues(new Uint8Array(32));368 return { bytes, base64url: base64urlEncode(bytes) };369}370