From 0d54c50e5f921cf168f309910b53b393df690e2d Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 8 Jun 2024 14:19:42 -0600 Subject: [PATCH] attempt to simply verify something signed with the same library -- doesn't work --- package-lock.json | 9 ++ package.json | 1 + src/libs/didPeer.ts | 273 +++++++++++++++++++++++++--------------- src/views/StartView.vue | 38 ++++-- 4 files changed, 213 insertions(+), 108 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40421ce..92cd992 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/vue-fontawesome": "^3.0.6", "@pvermeer/dexie-encrypted-addon": "^3.0.0", + "@simplewebauthn/browser": "^10.0.0", "@simplewebauthn/server": "^10.0.0", "@tweenjs/tween.js": "^21.1.1", "@types/js-yaml": "^4.0.9", @@ -8226,6 +8227,14 @@ "optional": true, "peer": true }, + "node_modules/@simplewebauthn/browser": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-10.0.0.tgz", + "integrity": "sha512-hG0JMZD+LiLUbpQcAjS4d+t4gbprE/dLYop/CkE01ugU/9sKXflxV5s0DRjdz3uNMFecatRfb4ZLG3XvF8m5zg==", + "dependencies": { + "@simplewebauthn/types": "^10.0.0" + } + }, "node_modules/@simplewebauthn/server": { "version": "10.0.0", "license": "MIT", diff --git a/package.json b/package.json index 1db5d89..7f206ac 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/vue-fontawesome": "^3.0.6", "@pvermeer/dexie-encrypted-addon": "^3.0.0", + "@simplewebauthn/browser": "^10.0.0", "@simplewebauthn/server": "^10.0.0", "@tweenjs/tween.js": "^21.1.1", "@types/js-yaml": "^4.0.9", diff --git a/src/libs/didPeer.ts b/src/libs/didPeer.ts index e9fcb8f..6672677 100644 --- a/src/libs/didPeer.ts +++ b/src/libs/didPeer.ts @@ -4,10 +4,18 @@ 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; @@ -61,11 +69,8 @@ export async function registerCredential( publicKey: publicKeyOptions, }); console.log("credential", credential); - console.log(credential?.id, " is the new Id"); - console.log( - Buffer.from(credential?.rawId).toString("base64"), - " is the base64 rawId", - ); + 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: { @@ -78,7 +83,6 @@ export async function registerCredential( clientExtensionResults: {}, type: "public-key", }, - //expectedChallenge: Buffer.from(challenge).toString("base64"), expectedChallenge: toBase64Url(challenge), expectedOrigin: window.location.origin, }; @@ -98,9 +102,6 @@ export async function registerCredential( ); console.log("attestationObject", attestationObject); - const credData = parseAuthData(attestationObject.authData); - console.log("new attempt at publicKey", credData); - const jwkObj = cborDecode( verification.registrationInfo?.credentialPublicKey as Uint8Array, ); @@ -129,7 +130,13 @@ export async function registerCredential( //const publicKeyBytes = extractPublicKeyCose(attestationObject.authData); //const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData); - return { rawId: credential?.rawId, publicKeyJwk, publicKeyBytes }; + return { + authData: attestationObject.authData, + credId: credential?.id, + rawId: credential?.rawId, + publicKeyJwk, + publicKeyBytes, + }; } // parse authData @@ -148,115 +155,181 @@ export function createPeerDid(publicKeyBytes: Uint8Array) { return "did:peer:0" + methodSpecificId; } -export async function createJwt( - payload: object, - issuerDid: string, - credentialId: ArrayBuffer, -) { - const signer = await webAuthnES256KSigner(credentialId); +export class PeerSetup { + public authenticatorData?: ArrayBuffer; + public clientDataJsonDecoded?: object; - // 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 }; + public async createJwt( + payload: object, + issuerDid: string, + credentialId: string, + ) { + const signer = await this.webAuthnES256KSigner(credentialId); - return createJWS(fullPayload, signer, header); -} + // 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 }; -async function webAuthnES256KSigner(credentialID: ArrayBuffer) { - return async (data: string | Uint8Array) => { - // also has clientDataJSON - const { signature } = await generateWebAuthnSignature(data, credentialID); + const jwt = createJWS(fullPayload, signer, header); + return jwt; + } - // 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"); + async webAuthnES256KSigner(credentialID: string) { + return async (data: string | Uint8Array) => { + const signature = await this.generateWebAuthnSignature( + data, + credentialID, + ); - // 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; + // 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"); - // 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, - ); + // 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; - const combSig64 = combinedSignature.toString("base64"); - console.log("combSig64", combSig64); - const combSig64Url = combSig64 - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); - console.log("combSig64Url", combSig64Url); - return combSig64Url; - }; -} + // 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, + ); -async function generateWebAuthnSignature( - dataToSign: string | Uint8Array, - credentialId: ArrayBuffer, -) { - if (!(dataToSign instanceof Uint8Array)) { - dataToSign = new TextEncoder().encode(dataToSign as string); + const combSig64 = combinedSignature.toString("base64"); + console.log("combSig64", combSig64); + const combSig64Url = combSig64 + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + console.log("combSig64Url", combSig64Url); + return combSig64Url; + }; } - const options = { - challenge: dataToSign, - allowCredentials: [{ id: credentialId, type: "public-key" }], - userVerification: "preferred", - }; + async generateWebAuthnSignature( + dataToSign: string | Uint8Array, // from Signer interface + credentialId: string, + ) { + if (dataToSign instanceof Uint8Array) { + dataToSign = new TextDecoder("utf-8").decode(dataToSign as Uint8Array); + } - const assertion = await navigator.credentials.get({ publicKey: options }); - console.log("assertion", assertion); + console.log("credentialId", credentialId); + const options = { + challenge: new TextEncoder().encode(dataToSign).buffer, + //allowCredentials: [{ id: credentialId, type: "public-key" }], + userVerification: "preferred", + }; - const authenticatorAssertionResponse = assertion?.response; - console.log( - "clientDataJSON decoded", - JSON.parse( + 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, ), - ), - ); - return { - signature: authenticatorAssertionResponse.signature, - clientDataJSON: authenticatorAssertionResponse.clientDataJSON, - authenticatorData: authenticatorAssertionResponse.authenticatorData, - }; + ); + 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, - issuerDid: string, // eslint-disable-line @typescript-eslint/no-unused-vars - publicKey: JWK, // eslint-disable-line @typescript-eslint/no-unused-vars + 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 }), }); @@ -266,7 +339,7 @@ export async function verifyJwt( 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 only verifies a peer DID, method 0, encoded base58btc.", ); } // this is basically hard-coded based on the results from the @aviarytech/did-peer resolver diff --git a/src/views/StartView.vue b/src/views/StartView.vue index f4dbfb4..6346da6 100644 --- a/src/views/StartView.vue +++ b/src/views/StartView.vue @@ -35,6 +35,7 @@ Only click "No" if you have a seed of 12 or 24 words generated elsewhere.

+