import { Buffer } from "buffer/"; import { JWTPayload } from "did-jwt"; import { p256 } from "@noble/curves/p256"; import { startAuthentication, startRegistration, } from "@simplewebauthn/browser"; import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, VerifyAuthenticationResponseOpts, } from "@simplewebauthn/server"; import { Base64URLString, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, AuthenticatorAssertionResponse, } from "@simplewebauthn/types"; import { AppString } from "../../../constants/app"; import { unwrapEC2Signature } from "../../../libs/crypto/vc/passkeyHelpers"; import { arrayToBase64Url, cborToKeys, peerDidToPublicKeyBytes, verifyPeerSignature, } from "../../../libs/crypto/vc/didPeer"; import { logger } from "../../../utils/logger"; export interface JWK { kty: string; crv: string; x: string; y: string; } export async function registerCredential(passkeyName?: string) { const options: PublicKeyCredentialCreationOptionsJSON = await generateRegistrationOptions({ rpName: AppString.APP_NAME, rpID: window.location.hostname, userName: passkeyName || AppString.APP_NAME + " User", // Don't prompt users for additional information about the authenticator // (Recommended for smoother UX) attestationType: "none", authenticatorSelection: { // Defaults residentKey: "preferred", userVerification: "preferred", // Optional authenticatorAttachment: "platform", }, }); // someday, instead of simplwebauthn, we'll go direct: navigator.credentials.create with PublicKeyCredentialCreationOptions // with pubKeyCredParams: { type: "public-key", alg: -7 } const attResp = await startRegistration(options); const verification = await verifyRegistrationResponse({ response: attResp, expectedChallenge: options.challenge, expectedOrigin: window.location.origin, expectedRPID: window.location.hostname, }); // references for parsing auth data and getting the public key // https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11 // https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa // https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7 const credIdBase64Url = verification.registrationInfo?.credentialID as string; if (attResp.rawId !== credIdBase64Url) { logger.warn("Warning! The raw ID does not match the credential ID."); } const credIdHex = Buffer.from( base64URLStringToArrayBuffer(credIdBase64Url), ).toString("hex"); const { publicKeyJwk } = cborToKeys( verification.registrationInfo?.credentialPublicKey as Uint8Array, ); return { authData: verification.registrationInfo?.attestationObject, credIdHex: credIdHex, publicKeyJwk: publicKeyJwk, publicKeyBytes: verification.registrationInfo ?.credentialPublicKey as Uint8Array, }; } export class PeerSetup { public authenticatorData?: ArrayBuffer; public challenge?: Uint8Array; public clientDataJsonBase64Url?: Base64URLString; public signature?: Base64URLString; public async createJwtSimplewebauthn( issuerDid: string, payload: object, credIdHex: string, expMinutes: number = 1, ) { const credentialId = arrayBufferToBase64URLString( Buffer.from(credIdHex, "hex").buffer, ); const issuedAt = Math.floor(Date.now() / 1000); const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now const fullPayload = { ...payload, exp: expiryTime, iat: issuedAt, iss: issuerDid, }; this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); // const payloadHash: Uint8Array = sha256(this.challenge); const options: PublicKeyCredentialRequestOptionsJSON = await generateAuthenticationOptions({ challenge: this.challenge, rpID: window.location.hostname, allowCredentials: [{ id: credentialId }], }); // console.log("simple authentication options", options); const clientAuth = await startAuthentication(options); // console.log("simple credential get", clientAuth); const authenticatorDataBase64Url = clientAuth.response.authenticatorData; this.authenticatorData = Buffer.from( clientAuth.response.authenticatorData, "base64", ).buffer; this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON; // console.log("simple authenticatorData for signing", this.authenticatorData); this.signature = clientAuth.response.signature; // Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties const header: JWTPayload = { typ: "JWANT", alg: "ES256" }; const headerBase64 = Buffer.from(JSON.stringify(header)) .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); const dataInJwt = { AuthenticationDataB64URL: authenticatorDataBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url, exp: expiryTime, iat: issuedAt, iss: issuerDid, }; const dataInJwtString = JSON.stringify(dataInJwt); const payloadBase64 = Buffer.from(dataInJwtString) .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); const signature = clientAuth.response.signature; return headerBase64 + "." + payloadBase64 + "." + signature; } public async createJwtNavigator( issuerDid: string, payload: object, credIdHex: string, expMinutes: number = 1, ) { const issuedAt = Math.floor(Date.now() / 1000); const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now const fullPayload = { ...payload, exp: expiryTime, iat: issuedAt, iss: issuerDid, }; const dataToSignString = JSON.stringify(fullPayload); const dataToSignBuffer = Buffer.from(dataToSignString); const credentialId = Buffer.from(credIdHex, "hex"); // console.log("lower credentialId", credentialId); this.challenge = new Uint8Array(dataToSignBuffer); const options = { publicKey: { allowCredentials: [ { id: credentialId, type: "public-key" as const, }, ], challenge: this.challenge.buffer, rpID: window.location.hostname, userVerification: "preferred" as const, }, }; const credential = (await navigator.credentials.get( options, )) as PublicKeyCredential; // console.log("nav credential get", credential); const response = credential?.response as AuthenticatorAssertionResponse; this.authenticatorData = response?.authenticatorData; const authenticatorDataBase64Url = arrayBufferToBase64URLString( this.authenticatorData as ArrayBuffer, ); this.clientDataJsonBase64Url = arrayBufferToBase64URLString( response?.clientDataJSON, ); // Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties const header: JWTPayload = { typ: "JWANT", alg: "ES256" }; const headerBase64 = Buffer.from(JSON.stringify(header)) .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); const dataInJwt = { AuthenticationDataB64URL: authenticatorDataBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url, exp: expiryTime, iat: issuedAt, iss: issuerDid, }; const dataInJwtString = JSON.stringify(dataInJwt); const payloadBase64 = Buffer.from(dataInJwtString) .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); const origSignature = Buffer.from(response?.signature).toString("base64"); this.signature = origSignature .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature; return jwt; } // To use this, add the asn1-ber library and add this import: // import asn1 from "asn1-ber"; // // return a low-level signing function, similar to createJWS approach // async webAuthnES256KSigner(credentialID: string) { // return async (data: string | Uint8Array) => { // // get signature from WebAuthn // const signature = await this.generateWebAuthnSignature(data); // // // This converts from the browser ArrayBuffer to a Node.js Buffer, which is a requirement for the asn1 library. // const signatureBuffer = Buffer.from(signature); // console.log("lower signature inside signer", signature); // console.log("lower buffer signature inside signer", signatureBuffer); // console.log("lower base64 buffer signature inside signer", signatureBuffer.toString("base64")); // // Decode the DER-encoded signature to extract R and S values // const reader = new asn1.BerReader(signatureBuffer); // console.log("lower after reader"); // reader.readSequence(); // console.log("lower after read sequence"); // const r = reader.readString(asn1.Ber.Integer, true); // console.log("lower after r"); // const s = reader.readString(asn1.Ber.Integer, true); // console.log("lower after r & s"); // // // Ensure R and S are 32 bytes each // const rBuffer = Buffer.from(r); // const sBuffer = Buffer.from(s); // console.log("lower after rBuffer & sBuffer", rBuffer, sBuffer); // const rWithoutPrefix = rBuffer.length > 32 ? rBuffer.slice(1) : rBuffer; // const sWithoutPrefix = sBuffer.length > 32 ? sBuffer.slice(1) : sBuffer; // const rPadded = // rWithoutPrefix.length < 32 // ? Buffer.concat([Buffer.alloc(32 - rWithoutPrefix.length), rBuffer]) // : rWithoutPrefix; // const sPadded = // rWithoutPrefix.length < 32 // ? Buffer.concat([Buffer.alloc(32 - sWithoutPrefix.length), sBuffer]) // : sWithoutPrefix; // // // Concatenate R and S to form the 64-byte array (ECDSA signature format expected by JWT) // const combinedSignature = Buffer.concat([rPadded, sPadded]); // console.log( // "lower combinedSignature", // combinedSignature.length, // combinedSignature, // ); // // const combSig64 = combinedSignature.toString("base64"); // console.log("lower combSig64", combSig64); // const combSig64Url = combSig64 // .replace(/\+/g, "-") // .replace(/\//g, "_") // .replace(/=+$/, ""); // console.log("lower combSig64Url", combSig64Url); // return combSig64Url; // }; // } } export async function createDidPeerJwt( did: string, credIdHex: string, payload: object, ): Promise { const peerSetup = new PeerSetup(); const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex); return jwt; } // I'd love to use this but it doesn't verify. // Requires: // npm install @noble/curves // ... and this import: // import { p256 } from "@noble/curves/p256"; export async function verifyJwtP256( issuerDid: string, authenticatorData: ArrayBuffer, challenge: Uint8Array, signature: Base64URLString, ) { const authDataFromBase = Buffer.from(authenticatorData); const sigBuffer = Buffer.from(signature, "base64"); const finalSigBuffer = unwrapEC2Signature(sigBuffer); const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); // Use challenge in preimage construction const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]); const isValid = p256.verify( finalSigBuffer, new Uint8Array(preimage), publicKeyBytes, ); return isValid; } export async function verifyJwtSimplewebauthn( credIdHex: string, issuerDid: string, authenticatorData: ArrayBuffer, challenge: Uint8Array, clientDataJsonBase64Url: Base64URLString, signature: Base64URLString, ) { const authData = arrayToBase64Url(Buffer.from(authenticatorData)); const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); const credId = arrayBufferToBase64URLString( Buffer.from(credIdHex, "hex").buffer, ); const authOpts: VerifyAuthenticationResponseOpts = { authenticator: { credentialID: credId, credentialPublicKey: publicKeyBytes, counter: 0, }, expectedChallenge: arrayToBase64Url(challenge), expectedOrigin: window.location.origin, expectedRPID: window.location.hostname, response: { authenticatorAttachment: "platform", clientExtensionResults: {}, id: credId, rawId: credId, response: { authenticatorData: authData, clientDataJSON: clientDataJsonBase64Url, signature: signature, }, type: "public-key", }, }; const verification = await verifyAuthenticationResponse(authOpts); return verification.verified; } // similar code is in endorser-ch util-crypto.ts verifyPeerSignature export async function verifyJwtWebCrypto( issuerDid: string, authenticatorData: ArrayBuffer, challenge: Uint8Array, signature: Base64URLString, ) { const authDataFromBase = Buffer.from(authenticatorData); const sigBuffer = Buffer.from(signature, "base64"); const finalSigBuffer = unwrapEC2Signature(sigBuffer); // Use challenge in preimage construction const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]); return verifyPeerSignature(preimage, issuerDid, finalSigBuffer); } // Remove unused functions: // - peerDidToDidDocument // - COSEtoPEM // - base64urlDecodeArrayBuffer // - base64urlEncodeArrayBuffer // - pemToCryptoKey // Keep only the used functions: export function base64urlDecodeString(input: string) { return atob(input.replace(/-/g, "+").replace(/_/g, "/")); } export function base64urlEncodeString(input: string) { return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } // from @simplewebauthn/browser function arrayBufferToBase64URLString(buffer: ArrayBuffer) { const bytes = new Uint8Array(buffer); let str = ""; for (const charCode of bytes) { str += String.fromCharCode(charCode); } const base64String = btoa(str); return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } // from @simplewebauthn/browser function base64URLStringToArrayBuffer(base64URLString: string) { const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/"); const padLength = (4 - (base64.length % 4)) % 4; const padded = base64.padEnd(base64.length + padLength, "="); const binary = atob(padded); const buffer = new ArrayBuffer(binary.length); const bytes = new Uint8Array(buffer); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return buffer; }