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}155156export async function generatePasskeyKeypair(157 algorithm: -7 | -8158): Promise<{159 publicKeyCose: Uint8Array;160 publicKeySpki: Uint8Array;161 privateKeyJwk: JsonWebKey;162}> {163 if (algorithm === -7) {164 const keyPair = await crypto.subtle.generateKey(165 { name: "ECDSA", namedCurve: "P-256" },166 true,167 ["sign", "verify"]168 );169 const privateKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);170 const publicKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey);171 const publicKeySpki = await crypto.subtle.exportKey("spki", keyPair.publicKey);172 return {173 privateKeyJwk,174 publicKeyCose: p256RawToCose(new Uint8Array(publicKeyRaw)),175 publicKeySpki: new Uint8Array(publicKeySpki),176 };177 }178179 const keyPair = await crypto.subtle.generateKey(180 { name: "Ed25519" },181 true,182 ["sign", "verify"]183 );184 const privateKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);185 const publicKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey);186 const publicKeySpki = await crypto.subtle.exportKey("spki", keyPair.publicKey);187 return {188 privateKeyJwk,189 publicKeyCose: ed25519RawToCose(new Uint8Array(publicKeyRaw)),190 publicKeySpki: new Uint8Array(publicKeySpki),191 };192}193194async function importPkcs8AsAlgorithm(195 pkcs8: Uint8Array,196 algorithm: -7 | -8197): Promise<{ privateKeyJwk: JsonWebKey; publicKeyCose: Uint8Array; algorithm: -7 | -8 }> {198 const imported =199 algorithm === -7200 ? await crypto.subtle.importKey(201 "pkcs8",202 toArrayBuffer(pkcs8),203 { name: "ECDSA", namedCurve: "P-256" },204 true,205 ["sign"]206 )207 : await crypto.subtle.importKey(208 "pkcs8",209 toArrayBuffer(pkcs8),210 { name: "Ed25519" },211 true,212 ["sign"]213 );214215 const privateKeyJwk = await crypto.subtle.exportKey("jwk", imported);216 return {217 privateKeyJwk,218 publicKeyCose: publicKeyJwkToCose(privateKeyJwk, algorithm),219 algorithm,220 };221}222223export async function importPasskeyPrivateKey(224 pkcs8: Uint8Array225): Promise<{ privateKeyJwk: JsonWebKey; publicKeyCose: Uint8Array; algorithm: -7 | -8 }> {226 try {227 return await importPkcs8AsAlgorithm(pkcs8, -8);228 } catch {229 return importPkcs8AsAlgorithm(pkcs8, -7);230 }231}232233234235236237238239function p1363ToDer(p1363: Uint8Array): Uint8Array {240 const half = p1363.length / 2;241 const r = trimAndPadAsn1Integer(p1363.slice(0, half));242 const s = trimAndPadAsn1Integer(p1363.slice(half));243 const body = concatBytes(244 Uint8Array.of(0x02, r.length),245 r,246 Uint8Array.of(0x02, s.length),247 s248 );249 return concatBytes(Uint8Array.of(0x30, body.length), body);250}251252function trimAndPadAsn1Integer(integer: Uint8Array): Uint8Array {253 let start = 0;254 while (start < integer.length - 1 && integer[start] === 0) start += 1;255 const trimmed = integer.slice(start);256 257 258 if ((trimmed[0]! & 0x80) !== 0) {259 return concatBytes(Uint8Array.of(0x00), trimmed);260 }261 return trimmed;262}263264export async function signPasskeyAssertion(265 privateKeyJwk: JsonWebKey,266 algorithm: -7 | -8,267 authenticatorData: Uint8Array,268 clientDataHash: Uint8Array269): Promise<Uint8Array> {270 const signInput = concatBytes(authenticatorData, clientDataHash);271272 if (algorithm === -7) {273 const key = await crypto.subtle.importKey(274 "jwk",275 privateKeyJwk,276 { name: "ECDSA", namedCurve: "P-256" },277 false,278 ["sign"]279 );280 const raw = await crypto.subtle.sign(281 { name: "ECDSA", hash: "SHA-256" },282 key,283 toArrayBuffer(signInput)284 );285 return p1363ToDer(new Uint8Array(raw));286 }287288 const key = await crypto.subtle.importKey(289 "jwk",290 privateKeyJwk,291 { name: "Ed25519" },292 false,293 ["sign"]294 );295 const signature = await crypto.subtle.sign("Ed25519", key, toArrayBuffer(signInput));296 return new Uint8Array(signature);297}298299export async function buildAuthenticatorData(opts: {300 rpId: string;301 flags: number;302 signCount: number;303 credentialId?: Uint8Array;304 publicKeyCose?: Uint8Array;305}): Promise<Uint8Array> {306 const rpIdHash = new Uint8Array(307 await crypto.subtle.digest("SHA-256", toArrayBuffer(textEncoder.encode(opts.rpId)))308 );309 const signCount = new Uint8Array(4);310 new DataView(signCount.buffer).setUint32(0, opts.signCount, false);311312 const base = concatBytes(rpIdHash, Uint8Array.of(opts.flags), signCount);313 if (!opts.credentialId || !opts.publicKeyCose) {314 return base;315 }316317 const aaguid = new Uint8Array(16);318 const credLength = new Uint8Array(2);319 new DataView(credLength.buffer).setUint16(0, opts.credentialId.length, false);320321 return concatBytes(base, aaguid, credLength, opts.credentialId, opts.publicKeyCose);322}323324export function buildClientDataJSON(325 type: "webauthn.create" | "webauthn.get",326 challenge: string,327 origin: string328): Uint8Array {329 return textEncoder.encode(330 JSON.stringify({331 type,332 challenge,333 origin,334 crossOrigin: false,335 })336 );337}338339export function createAttestationObject(authData: Uint8Array): Uint8Array {340 return encodeMap([341 ["fmt", "none"],342 ["attStmt", new Map()],343 ["authData", authData],344 ]);345}346347export async function sha256(data: Uint8Array): Promise<Uint8Array> {348 return new Uint8Array(await crypto.subtle.digest("SHA-256", toArrayBuffer(data)));349}350351352353354355export function generateCredentialId(): {356 bytes: Uint8Array;357 base64url: string;358} {359 const bytes = crypto.getRandomValues(new Uint8Array(32));360 return { bytes, base64url: base64urlEncode(bytes) };361}362