import asn1 from "asn1-ber"; import { Buffer } from "buffer/"; import { decode as cborDecode } from "cbor-x"; import { createJWS, JWTPayload, verifyJWT } from "did-jwt"; import { DIDResolutionResult, Resolver } from "did-resolver"; import { bytesToMultibase } from "@veramo/utils"; import { startAuthentication } from "@simplewebauthn/browser"; import { generateAuthenticationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, VerifyRegistrationResponseOpts, } from "@simplewebauthn/server"; import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse"; import { Base64URLString, PublicKeyCredentialRequestOptionsJSON, } from "@simplewebauthn/types"; export interface JWK { kty: string; crv: string; x: string; y: string; } export interface PublicKeyCredential { rawId: Uint8Array; jwt: JWK; } function toBase64Url(anything: Uint8Array) { return Buffer.from(anything) .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); } export async function registerCredential( userId: Uint8Array, challenge: Uint8Array, ) { const publicKeyOptions: PublicKeyCredentialCreationOptions = { challenge: challenge, rp: { name: "Time Safari", id: window.location.hostname, }, user: { id: userId, name: "Current-User", displayName: "Current User", }, pubKeyCredParams: [ { type: "public-key", alg: -7, // ES256 algorithm }, ], authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "preferred", }, timeout: 60000, attestation: "direct", }; const credential = await navigator.credentials.create({ publicKey: publicKeyOptions, }); console.log("credential", credential); console.log(credential?.id, " is the new ID base64-url-encoded"); console.log(toBase64Url(credential?.rawId), " is the base64 rawId"); const attestationResponse = credential?.response; const verfInput: VerifyRegistrationResponseOpts = { response: { id: credential?.id as string, rawId: credential?.id as string, //Buffer.from(credential?.rawId).toString("base64"), response: { attestationObject: toBase64Url(attestationResponse?.attestationObject), clientDataJSON: toBase64Url(attestationResponse?.clientDataJSON), }, clientExtensionResults: {}, type: "public-key", }, expectedChallenge: toBase64Url(challenge), expectedOrigin: window.location.origin, }; console.log("verfInput", verfInput); const verification = await verifyRegistrationResponse(verfInput); console.log("verification", verification); // Parse the attestation response to get the public key const clientDataJSON = attestationResponse.clientDataJSON; console.log("clientDataJSON raw", clientDataJSON); console.log( "clientDataJSON dec", new TextDecoder("utf-8").decode(clientDataJSON), ); const attestationObject = cborDecode( new Uint8Array(attestationResponse.attestationObject), ); console.log("attestationObject", attestationObject); const jwkObj = cborDecode( verification.registrationInfo?.credentialPublicKey as Uint8Array, ); console.log("jwkObj from verification", jwkObj); if ( jwkObj[1] != 2 || // kty "EC" jwkObj[3] != -7 || // alg "ES256" jwkObj[-1] != 1 || // crv "P-256" jwkObj[-2].length != 32 || // x jwkObj[-3].length != 32 // y ) { throw new Error("Unable to extract key."); } const publicKeyJwk = { alg: "ES256", crv: "P-256", kty: "EC", x: toBase64Url(jwkObj[-2]), y: toBase64Url(jwkObj[-3]), }; const publicKeyBytes = Buffer.concat([ Buffer.from(jwkObj[-2]), Buffer.from(jwkObj[-3]), ]); //const publicKeyBytes = extractPublicKeyCose(attestationObject.authData); //const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData); return { authData: attestationObject.authData, credId: credential?.id, rawId: credential?.rawId, publicKeyJwk, publicKeyBytes, }; } // parse authData // here's one: https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11 // from https://chatgpt.com/c/0ce72fda-bc5d-42ff-a748-6022f6e39fa0 // from https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa export function createPeerDid(publicKeyBytes: Uint8Array) { // https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67 //const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME }); const methodSpecificId = bytesToMultibase( publicKeyBytes, "base58btc", "secp256k1-pub", ); return "did:peer:0" + methodSpecificId; } export class PeerSetup { public authenticatorData?: ArrayBuffer; public clientDataJsonDecoded?: object; public async createJwt( payload: object, issuerDid: string, credentialId: string, ) { const signer = await this.webAuthnES256KSigner(credentialId); // from createJWT in did-jwt/src/JWT.ts const header: JWTPayload = { typ: "JWT", alg: "ES256K" }; const timestamps: Partial = { iat: Math.floor(Date.now() / 1000), exp: undefined, }; const fullPayload = { ...timestamps, ...payload, iss: issuerDid }; const jwt = createJWS(fullPayload, signer, header); return jwt; } async webAuthnES256KSigner(credentialID: string) { return async (data: string | Uint8Array) => { const signature = await this.generateWebAuthnSignature( data, credentialID, ); // 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("signature inside signer", signature); console.log("buffer signature inside signer", signatureBuffer); // Decode the DER-encoded signature to extract R and S values const reader = new asn1.BerReader(signatureBuffer); console.log("after reader"); reader.readSequence(); console.log("after read sequence"); const r = reader.readString(asn1.Ber.Integer, true); console.log("after r"); const s = reader.readString(asn1.Ber.Integer, true); console.log("after r & s"); // Ensure R and S are 32 bytes each const rBuffer = Buffer.from(r); const sBuffer = Buffer.from(s); console.log("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( "combinedSignature", combinedSignature.length, combinedSignature, ); const combSig64 = combinedSignature.toString("base64"); console.log("combSig64", combSig64); const combSig64Url = combSig64 .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); console.log("combSig64Url", combSig64Url); return combSig64Url; }; } async generateWebAuthnSignature( dataToSign: string | Uint8Array, // from Signer interface credentialId: string, ) { if (dataToSign instanceof Uint8Array) { dataToSign = new TextDecoder("utf-8").decode(dataToSign as Uint8Array); } console.log("credentialId", credentialId); const options = { challenge: new TextEncoder().encode(dataToSign).buffer, //allowCredentials: [{ id: credentialId, type: "public-key" }], userVerification: "preferred", }; const assertion = await navigator.credentials.get({ publicKey: options }); console.log("assertion", assertion); const authenticatorAssertionResponse = assertion?.response; this.clientDataJsonDecoded = JSON.parse( new TextDecoder("utf-8").decode( authenticatorAssertionResponse.clientDataJSON, ), ); console.log("clientDataJSON decoded", this.clientDataJsonDecoded); this.authenticatorData = authenticatorAssertionResponse.authenticatorData; console.log("authenticator data", this.authenticatorData); return authenticatorAssertionResponse.signature; } } export async function verifyJwt( jwt: string, credId: Base64URLString, rawId: Uint8Array, authenticatorData: ArrayBuffer, clientDataJSON: object, publicKey: Uint8Array, signature: Base64URLString, ) { const options: PublicKeyCredentialRequestOptionsJSON = await generateAuthenticationOptions({ rpID: window.location.hostname, // Require users to use a previously-registered authenticator // allowCredentials: userPasskeys.map(passkey => ({ // id: passkey.id, // transports: passkey.transports, // })), }); console.log("authentication options", options); const clientAuth = await startAuthentication(options); console.log("clientAuth", clientAuth); const verfOpts: VerifyAuthenticationResponseOpts = { response: clientAuth, authenticator: { credentialID: credId, credentialPublicKey: publicKey, counter: 0, }, expectedChallenge: () => true, // options.challenge doesn't work expectedOrigin: window.location.origin, expectedRPID: window.location.hostname, }; console.log("verfOpts", verfOpts); const verificationFromClient = await verifyAuthenticationResponse(verfOpts); console.log("client auth verification", verificationFromClient); const authData = toBase64Url(Buffer.from(authenticatorData)); const bufferizedJson = toBase64Url( new TextEncoder().encode(JSON.stringify(clientDataJSON)), ); const authOpts: VerifyAuthenticationResponseOpts = { response: { id: credId, rawId: toBase64Url(rawId), response: { authenticatorData: authData, clientDataJSON: bufferizedJson, signature: signature, }, clientExtensionResults: {}, type: "public-key", }, expectedChallenge: () => true, // options.challenge doesn't work expectedOrigin: window.location.origin, expectedRPID: window.location.hostname, authenticator: { credentialID: credId, credentialPublicKey: publicKey, counter: 0, }, }; const verification = await verifyAuthenticationResponse(authOpts); console.log("auth verification", verification); const decoded = verifyJWT(jwt, { resolver: new Resolver({ peer: peerDidToDidDocument }), }); return decoded; } async function peerDidToDidDocument(did: string): Promise { if (!did.startsWith("did:peer:0z")) { throw new Error( "This only verifies a peer DID, method 0, encoded base58btc.", ); } // this is basically hard-coded based on the results from the @aviarytech/did-peer resolver const id = did.split(":")[2]; const multibase = id.slice(1); const encnumbasis = multibase.slice(1); const didDocument = { "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1", ], assertionMethod: [did + "#" + encnumbasis], authentication: [did + "#" + encnumbasis], capabilityDelegation: [did + "#" + encnumbasis], capabilityInvocation: [did + "#" + encnumbasis], id: did, keyAgreement: undefined, service: undefined, verificationMethod: [ { controller: did, id: did + "#" + encnumbasis, publicKeyMultibase: multibase, type: "EcdsaSecp256k1VerificationKey2019", }, ], }; return { didDocument, didDocumentMetadata: {}, didResolutionMetadata: { contentType: "application/did+ld+json" }, }; }