diff --git a/src/libs/didPeer.ts b/src/libs/didPeer.ts index 14bff80..b6eb80f 100644 --- a/src/libs/didPeer.ts +++ b/src/libs/didPeer.ts @@ -1,20 +1,19 @@ import asn1 from "asn1-ber"; import { Buffer } from "buffer/"; import { decode as cborDecode } from "cbor-x"; -import { createJWS, JWTPayload } from "did-jwt"; +import { JWTPayload } from "did-jwt"; import { DIDResolutionResult } from "did-resolver"; import { sha256 } from "ethereum-cryptography/sha256.js"; import { bytesToMultibase } from "@veramo/utils"; import { startAuthentication, - startRegistration, WebAuthnAbortService, + startRegistration, } from "@simplewebauthn/browser"; import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, - VerifyRegistrationResponseOpts, } from "@simplewebauthn/server"; import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse"; import { @@ -23,7 +22,6 @@ import { PublicKeyCredentialRequestOptionsJSON, } from "@simplewebauthn/types"; -import { generateRandomBytes } from "@/libs/crypto"; import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; export interface JWK { @@ -54,13 +52,6 @@ export async function registerCredential(userId: Uint8Array) { // Don't prompt users for additional information about the authenticator // (Recommended for smoother UX) attestationType: "none", - // Prevent users from re-registering existing authenticators - // excludeCredentials: userPasskeys.map(passkey => ({ - // id: passkey.id, - // // Optional - // transports: passkey.transports, - // })), - // // See "Guiding use of authenticators via authenticatorSelection" below authenticatorSelection: { // Defaults residentKey: "preferred", @@ -69,24 +60,21 @@ export async function registerCredential(userId: Uint8Array) { 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); - console.log("attResp", attResp); const verification = await verifyRegistrationResponse({ response: attResp, expectedChallenge: options.challenge, expectedOrigin: window.location.origin, expectedRPID: window.location.hostname, }); - console.log("verification", verification); - const jwkObj = cborDecode( - verification.registrationInfo?.credentialPublicKey as Uint8Array, - ); - console.log("jwkObj from verification", jwkObj); - console.log( - "[1]==2 => kty EC", - "[3]==-7 => alg ES256", - "[-1]==1 => crv P-256", - ); + + // 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 { publicKeyJwk } = cborToKeys( verification.registrationInfo?.credentialPublicKey as Uint8Array, ); @@ -101,99 +89,13 @@ export async function registerCredential(userId: Uint8Array) { }; } -export async function registerCredential2(userId: Uint8Array) { - const challenge = generateRandomBytes(32); - 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(arrayToBase64Url(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: arrayToBase64Url(attestationResponse?.attestationObject), - clientDataJSON: arrayToBase64Url(attestationResponse?.clientDataJSON), - }, - clientExtensionResults: {}, - type: "public-key", - }, - expectedChallenge: arrayToBase64Url(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 { publicKeyJwk, publicKeyBuffer } = cborToKeys( - verification.registrationInfo?.credentialPublicKey as Uint8Array, - ); - - //const publicKeyBytes = extractPublicKeyCose(attestationObject.authData); - //const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData); - - return { - authData: attestationObject.authData, - credId: credential?.id, - rawId: credential?.rawId, - publicKeyJwk, - publicKeyBytes: publicKeyBuffer, - }; -} - -// 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", + "p256-pub", ); return "did:peer:0" + methodSpecificId; } @@ -205,11 +107,11 @@ export class PeerSetup { public clientDataJsonDecoded?: object; public clientDataJsonBase64Url?: Base64URLString; public signature?: Base64URLString; - public publicKeyJwk?: JWK; - - public async createJwt(fullPayload: object, credentialId: string) { - const header: JWTPayload = { typ: "JWT", alg: "ES256" }; + public async createJwtSimplewebauthn( + fullPayload: object, + credentialId: string, + ) { this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); // const payloadHash: Uint8Array = sha256(this.challenge); const options: PublicKeyCredentialRequestOptionsJSON = @@ -233,83 +135,65 @@ export class PeerSetup { "utf-8", ), ); - //console.log("simple authenticatorData for signing", this.authenticatorData); + // console.log("simple authenticatorData for signing", this.authenticatorData); this.signature = clientAuth.response.signature; - const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64"); - const payloadBase64 = clientAuth.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 = { + AuthenticationData: this.authenticatorDataBase64Url, + ClientDataJSON: this.clientDataJsonBase64Url, + }; + const dataInJwtString = JSON.stringify(dataInJwt); + const payloadBase64 = Buffer.from(dataInJwtString).toString("base64"); + const signature = clientAuth.response.signature; return headerBase64 + "." + payloadBase64 + "." + signature; } - public async createJwt2(fullPayload: object, credentialId: string) { - const header: JWTPayload = { typ: "JWT", alg: "ES256" }; - const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64"); - + public async createJwtNavigator(fullPayload: object, credentialId: string) { const dataToSignString = JSON.stringify(fullPayload); const dataToSignBuffer = Buffer.from(dataToSignString); - //console.log("lower credentialId", credentialId); + // console.log("lower credentialId", credentialId); this.challenge = new Uint8Array(dataToSignBuffer); const options = { publicKey: { challenge: this.challenge.buffer, rpID: window.location.hostname, - //allowCredentials: [{ id: credentialId, type: "public-key" }], userVerification: "preferred", - //extensions: fullPayload, }, }; - // console.log("lower authentication options", options); - // console.log("lower options in base64", { - // publicKey: { - // challenge: bufferToBase64URLString(options.publicKey.challenge), - // rpID: window.location.hostname, - // userVerification: "preferred", - // }, - // }); const credential = await navigator.credentials.get(options); - // console.log("lower credential get", credential); - // console.log("lower credential get in base64", { - // id: credential?.id, - // rawId: bufferToBase64URLString(credential?.rawId), - // response: { - // authenticatorData: bufferToBase64URLString( - // credential?.response.authenticatorData, - // ), - // clientDataJSON: bufferToBase64URLString( - // credential?.response.clientDataJSON, - // ), - // signature: bufferToBase64URLString(credential?.response.signature), - // }, - // type: credential?.type, - // }); - - const authenticatorAssertionResponse = credential?.response; - - this.authenticatorDataBase64Url = - authenticatorAssertionResponse.authenticatorData; + // console.log("nav credential get", credential); + + this.authenticatorDataBase64Url = bufferToBase64URLString( + credential?.response.authenticatorData, + ); this.authenticatorData = Buffer.from( this.authenticatorDataBase64Url as Base64URLString, "base64", ).buffer; - // console.log("lower authenticator data", this.authenticatorData); - this.clientDataJsonBase64Url = - authenticatorAssertionResponse.clientDataJSON; + this.clientDataJsonBase64Url = bufferToBase64URLString( + credential?.response.clientDataJSON, + ); this.clientDataJsonDecoded = JSON.parse( - new TextDecoder("utf-8").decode( - authenticatorAssertionResponse.clientDataJSON, - ), + new TextDecoder("utf-8").decode(credential?.response.clientDataJSON), ); - // console.log("lower clientDataJSON decoded", this.clientDataJsonDecoded); - const origSignature = Buffer.from( - authenticatorAssertionResponse.signature, - ).toString("base64"); - this.signature = origSignature + // 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(/=+$/, ""); @@ -319,122 +203,159 @@ export class PeerSetup { ClientDataJSON: this.clientDataJsonBase64Url, }; const dataInJwtString = JSON.stringify(dataInJwt); - const payloadBase64 = Buffer.from(dataInJwtString).toString("base64"); + const payloadBase64 = Buffer.from(dataInJwtString) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + + const origSignature = Buffer.from(credential?.response.signature) + .toString("base64") + this.signature = origSignature + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature; return jwt; } - // Attempted with JWS, but it will not match because it signs different content (header + payload) - //const signer = await this.webAuthnES256KSigner(credentialId); - //const jwt = createJWS(fullPayload, signer, { typ: "JWT", alg: "ES256" }); - 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("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; - }; - } + // 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; + // }; + // } +} - async generateWebAuthnSignature( - dataToSign: string | Uint8Array, // from Signer interface - credentialId: string, - ) { - if (!(dataToSign instanceof Uint8Array)) { - console.log("lower dataToSign & typeof ", typeof dataToSign, dataToSign); - dataToSign = new Uint8Array(base64URLStringToBuffer(dataToSign)); - } - console.log("lower credentialId", credentialId); - this.challenge = dataToSign; - const options = { - publicKey: { - challenge: this.challenge.buffer, - rpID: window.location.hostname, - //allowCredentials: [{ id: credentialId, type: "public-key" }], - userVerification: "preferred", - //extensions: fullPayload, +export async function verifyJwtSimplewebauthn( + jwt: string, + credId: Base64URLString, + rawId: Uint8Array, + authenticatorData: ArrayBuffer, + authenticatorDataBase64Url: Base64URLString, + challenge: Uint8Array, + clientDataJSON: object, + clientDataJsonBase64Url: Base64URLString, + publicKeyBytes: Uint8Array, + publicKeyJwk: JWK, + signature: Base64URLString, +) { + const authData = arrayToBase64Url(Buffer.from(authenticatorData)); + 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: arrayToBase64Url(rawId), + response: { + authenticatorData: authData, + clientDataJSON: arrayToBase64Url( + Buffer.from(JSON.stringify(clientDataJSON)), + ), + signature: signature, }, - }; - - // console.log("lower authentication options", options); - const assertion = await navigator.credentials.get(options); - // console.log("lower credential get", assertion); - - const authenticatorAssertionResponse = assertion?.response; + type: "public-key", + }, + }; + const verification = await verifyAuthenticationResponse(authOpts); + return verification.verified; +} - this.authenticatorDataBase64Url = - authenticatorAssertionResponse.authenticatorData; - this.authenticatorData = Buffer.from( - this.authenticatorDataBase64Url as Base64URLString, - "base64", - ).buffer; - // console.log("lower authenticator data", this.authenticatorData); +// I'd love to use this but it doesn't verify. +// Pequires: +// npm install @noble/curves +// ... and this import: +// import { p256 } from "@noble/curves/p256"; +export async function verifyJwtP256( + jwt: string, + credId: Base64URLString, + rawId: Uint8Array, + authenticatorData: ArrayBuffer, + authenticatorDataBase64Url: Base64URLString, + challenge: Uint8Array, + clientDataJSON: object, + clientDataJsonBase64Url: Base64URLString, + publicKeyBytes: Uint8Array, + publicKeyJwk: JWK, + signature: Base64URLString, +) { + const authDataFromBase = Buffer.from(authenticatorDataBase64Url, "base64"); + const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); + const sigBuffer = Buffer.from(signature, "base64"); + const finalSigBuffer = unwrapEC2Signature(sigBuffer); - this.clientDataJsonBase64Url = - authenticatorAssertionResponse.clientDataJSON; - this.clientDataJsonDecoded = JSON.parse( - new TextDecoder("utf-8").decode( - authenticatorAssertionResponse.clientDataJSON, - ), - ); - // console.log("lower clientDataJSON decoded", this.clientDataJsonDecoded); + // Hash the client data + const hash = sha256(clientDataFromBase); - this.signature = Buffer.from( - authenticatorAssertionResponse.signature, - ).toString("base64"); + // Construct the preimage + const preimage = Buffer.concat([authDataFromBase, hash]); - return this.signature; - } + const isValid = p256.verify( + finalSigBuffer, + new Uint8Array(preimage), + publicKeyBytes, + ); + return isValid; } -export async function verifyJwt( +export async function verifyJwtWebCrypto( jwt: string, credId: Base64URLString, rawId: Uint8Array, @@ -447,35 +368,6 @@ export async function verifyJwt( publicKeyJwk: JWK, signature: Base64URLString, ) { - // const authData = arrayToBase64Url(Buffer.from(authenticatorData)); - // 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: arrayToBase64Url(rawId), - // response: { - // authenticatorData: authData, - // clientDataJSON: arrayToBase64Url( - // Buffer.from(JSON.stringify(clientDataJSON)), - // ), - // signature: signature, - // }, - // type: "public-key", - // }, - // }; - // console.log("auth opts", authOpts); - // const verification = await verifyAuthenticationResponse(authOpts); - // console.log("auth verification", verification); - const authDataFromBase = Buffer.from(authenticatorDataBase64Url, "base64"); const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); const sigBuffer = Buffer.from(signature, "base64"); @@ -487,18 +379,6 @@ export async function verifyJwt( // Construct the preimage const preimage = Buffer.concat([authDataFromBase, hash]); - // console.log("finalSigBuffer", finalSigBuffer); - // console.log("preimage", preimage); - // console.log("publicKeyBytes", publicKeyBytes); - - // This uses p256 from @noble/curves/p256, which I would prefer but it's returning false. - // const isValid = p256.verify( - // finalSigBuffer, - // new Uint8Array(preimage), - // publicKeyBytes, - // ); - // console.log("isValid", isValid); - const WebCrypto = await getWebCrypto(); const verifyAlgorithm = { name: "ECDSA", @@ -515,17 +395,12 @@ export async function verifyJwt( false, ["verify"], ); - // console.log("verifyAlgorithm", verifyAlgorithm); - // console.log("publicKeyCryptoKey", publicKeyCryptoKey); - // console.log("finalSigBuffer", finalSigBuffer); - // console.log("preimage", preimage); const verified = await WebCrypto.subtle.verify( verifyAlgorithm, publicKeyCryptoKey, finalSigBuffer, preimage, ); - // console.log("verified", verified); return verified; } @@ -628,7 +503,6 @@ function base64URLStringToBuffer(base64URLString) { function cborToKeys(publicKeyBytes: Uint8Array) { const jwkObj = cborDecode(publicKeyBytes); - console.log("jwkObj from verification", jwkObj); if ( jwkObj[1] != 2 || // kty "EC" jwkObj[3] != -7 || // alg "ES256" @@ -663,7 +537,7 @@ async function pemToCryptoKey(pem: string) { for (let i = 0; i < binaryDerString.length; i++) { binaryDer[i] = binaryDerString.charCodeAt(i); } - console.log("binaryDer", binaryDer.buffer); + // console.log("binaryDer", binaryDer.buffer); return await window.crypto.subtle.importKey( "spki", binaryDer.buffer, diff --git a/src/views/TestView.vue b/src/views/TestView.vue index a6a6963..10e3a3e 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -172,24 +172,51 @@