From bbf1c17e6263811a676341b32f0368243e3029f2 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 14 Jun 2024 21:19:03 -0600 Subject: [PATCH] add signing by non-simplewebauthn code --- src/libs/didPeer.ts | 361 +++++++++++++++++++++-------------------- src/views/TestView.vue | 55 ++++++- 2 files changed, 234 insertions(+), 182 deletions(-) diff --git a/src/libs/didPeer.ts b/src/libs/didPeer.ts index 3f4ee27..14bff80 100644 --- a/src/libs/didPeer.ts +++ b/src/libs/didPeer.ts @@ -7,7 +7,7 @@ import { sha256 } from "ethereum-cryptography/sha256.js"; import { bytesToMultibase } from "@veramo/utils"; import { startAuthentication, - startRegistration, + startRegistration, WebAuthnAbortService, } from "@simplewebauthn/browser"; import { generateAuthenticationOptions, @@ -207,55 +207,33 @@ export class PeerSetup { public signature?: Base64URLString; public publicKeyJwk?: JWK; - public async createJwt( - payload: object, - issuerDid: string, - credentialId: string, - ) { + public async createJwt(fullPayload: object, credentialId: string) { const header: JWTPayload = { typ: "JWT", alg: "ES256" }; - // from createJWT in did-jwt/src/JWT.ts - const timestamps: Partial = { - iat: Math.floor(Date.now() / 1000), - exp: undefined, - }; - const fullPayload = { ...timestamps, ...payload, iss: issuerDid }; - - const payloadHash: Uint8Array = sha256( - Buffer.from(JSON.stringify(fullPayload)), - ); + this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); + // const payloadHash: Uint8Array = sha256(this.challenge); const options: PublicKeyCredentialRequestOptionsJSON = await generateAuthenticationOptions({ - challenge: payloadHash, + challenge: this.challenge, rpID: window.location.hostname, - // Require users to use a previously-registered authenticator - // allowCredentials: userPasskeys.map(passkey => ({ - // id: passkey.id, - // transports: passkey.transports, - // })), }); - console.log("custom authentication options", options); + // console.log("simple authentication options", options); const clientAuth = await startAuthentication(options); - console.log("custom clientAuth", clientAuth); + // console.log("simple credential get", clientAuth); this.authenticatorDataBase64Url = clientAuth.response.authenticatorData; this.authenticatorData = Buffer.from( clientAuth.response.authenticatorData, "base64", ).buffer; - this.challenge = payloadHash; this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON; this.clientDataJsonDecoded = JSON.parse( Buffer.from(clientAuth.response.clientDataJSON, "base64").toString( "utf-8", ), ); - console.log("authenticatorData for signing", this.authenticatorData); - console.log( - "clientDataJSON for signing", - Buffer.from(clientAuth.response.clientDataJSON, "base64"), - ); + //console.log("simple authenticatorData for signing", this.authenticatorData); this.signature = clientAuth.response.signature; const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64"); @@ -265,25 +243,90 @@ export class PeerSetup { return headerBase64 + "." + payloadBase64 + "." + signature; } - public async createJwt2( - payload: object, - issuerDid: string, - credentialId: string, - ) { - const signer = await this.webAuthnES256KSigner(credentialId); + public async createJwt2(fullPayload: object, credentialId: string) { + const header: JWTPayload = { typ: "JWT", alg: "ES256" }; + const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64"); - // 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 dataToSignString = JSON.stringify(fullPayload); + const dataToSignBuffer = Buffer.from(dataToSignString); + + //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, + }, }; - const fullPayload = { ...timestamps, ...payload, iss: issuerDid }; - const jwt = createJWS(fullPayload, signer, header); + // 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; + this.authenticatorData = Buffer.from( + this.authenticatorDataBase64Url as Base64URLString, + "base64", + ).buffer; + // console.log("lower authenticator data", this.authenticatorData); + + this.clientDataJsonBase64Url = + authenticatorAssertionResponse.clientDataJSON; + this.clientDataJsonDecoded = JSON.parse( + new TextDecoder("utf-8").decode( + authenticatorAssertionResponse.clientDataJSON, + ), + ); + // console.log("lower clientDataJSON decoded", this.clientDataJsonDecoded); + + const origSignature = Buffer.from( + authenticatorAssertionResponse.signature, + ).toString("base64"); + this.signature = origSignature + .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 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( @@ -293,22 +336,23 @@ export class PeerSetup { // 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); + 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("after reader"); + console.log("lower after reader"); reader.readSequence(); - console.log("after read sequence"); + console.log("lower after read sequence"); const r = reader.readString(asn1.Ber.Integer, true); - console.log("after r"); + console.log("lower after r"); const s = reader.readString(asn1.Ber.Integer, true); - console.log("after r & s"); + 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("after rBuffer & sBuffer", rBuffer, sBuffer); + 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 = @@ -323,18 +367,18 @@ export class PeerSetup { // 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", + "lower combinedSignature", combinedSignature.length, combinedSignature, ); const combSig64 = combinedSignature.toString("base64"); - console.log("combSig64", combSig64); + console.log("lower combSig64", combSig64); const combSig64Url = combSig64 .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); - console.log("combSig64Url", combSig64Url); + console.log("lower combSig64Url", combSig64Url); return combSig64Url; }; } @@ -343,30 +387,50 @@ export class PeerSetup { dataToSign: string | Uint8Array, // from Signer interface credentialId: string, ) { - if (dataToSign instanceof Uint8Array) { - dataToSign = new TextDecoder("utf-8").decode(dataToSign as Uint8Array); + if (!(dataToSign instanceof Uint8Array)) { + console.log("lower dataToSign & typeof ", typeof dataToSign, dataToSign); + dataToSign = new Uint8Array(base64URLStringToBuffer(dataToSign)); } - - console.log("credentialId", credentialId); + console.log("lower credentialId", credentialId); + this.challenge = dataToSign; const options = { - challenge: new TextEncoder().encode(dataToSign).buffer, - //allowCredentials: [{ id: credentialId, type: "public-key" }], - userVerification: "preferred", + publicKey: { + challenge: this.challenge.buffer, + rpID: window.location.hostname, + //allowCredentials: [{ id: credentialId, type: "public-key" }], + userVerification: "preferred", + //extensions: fullPayload, + }, }; - const assertion = await navigator.credentials.get({ publicKey: options }); - console.log("assertion", assertion); + // console.log("lower authentication options", options); + const assertion = await navigator.credentials.get(options); + // console.log("lower credential get", assertion); const authenticatorAssertionResponse = assertion?.response; + + this.authenticatorDataBase64Url = + authenticatorAssertionResponse.authenticatorData; + this.authenticatorData = Buffer.from( + this.authenticatorDataBase64Url as Base64URLString, + "base64", + ).buffer; + // console.log("lower authenticator data", this.authenticatorData); + + this.clientDataJsonBase64Url = + authenticatorAssertionResponse.clientDataJSON; 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; + // console.log("lower clientDataJSON decoded", this.clientDataJsonDecoded); + + this.signature = Buffer.from( + authenticatorAssertionResponse.signature, + ).toString("base64"); + + return this.signature; } } @@ -383,109 +447,34 @@ export async function verifyJwt( publicKeyJwk: JWK, signature: Base64URLString, ) { - // Here's a combined auth & verify process, based on some of the inputs. - // - // 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 = { + // const authData = arrayToBase64Url(Buffer.from(authenticatorData)); + // const authOpts: VerifyAuthenticationResponseOpts = { // authenticator: { // credentialID: credId, // credentialPublicKey: publicKeyBytes, // counter: 0, // }, - // expectedChallenge: options.challenge, + // expectedChallenge: arrayToBase64Url(challenge), // expectedOrigin: window.location.origin, // expectedRPID: window.location.hostname, - // response: clientAuth, + // 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("verfOpts", verfOpts); - // const verificationFromClient = await verifyAuthenticationResponse(verfOpts); - // console.log("client auth verification", verificationFromClient); - - 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); - - // It doesn't work to use the did-jwt verifyJWT with did-resolver Resolver - // const decoded = verifyJWT(jwt, { - // resolver: new Resolver({ peer: peerDidToDidDocument }), - // }); - // return decoded; - - // const [headerB64, concatenatedPayloadB64, signatureB64] = jwt.split("."); - // - // const header = JSON.parse(atob(headerB64)); - // const jsonPayload = JSON.parse(atob(concatenatedPayloadB64)); - // const [jsonPayloadB64, otherDataHashB64] = concatenatedPayloadB64.split("."); - // - // const otherDataHash = base64urlDecode(otherDataHashB64); - // const signature = base64urlDecode(signatureB64); - // - // const dataToVerify = new TextEncoder().encode(`${headerB64}.${concatenatedPayloadB64}`); - // const dataHash = await sha256(dataToVerify); - // - // const authenticatorData = base64urlDecode(jsonPayload.authenticatorData); - // const clientDataJSON = jsonPayload.clientDataJSON; - - ///////// - - // const clientBuffer = clientDataJsonBase64Url - // .replace(/-/g, "+") - // .replace(/_/g, "/"); - // const clientData = new Uint8Array(Buffer.from(clientBuffer, "base64")); - // const clientDataHash = sha256(clientData); - // - // const verifyData = new Uint8Array([ - // ...new Uint8Array(authenticatorData), - // ...new Uint8Array(clientDataHash), - // ]); - // console.log("verifyData", verifyData); - // const sigBase64Raw = signature.replace(/-/g, "+").replace(/_/g, "/"); - // - // const sigHex = new Uint8Array(Buffer.from(sigBase64Raw, "base64")); //Buffer.from(sigBase64Raw, "base64").toString("hex"); - // const msgHex = verifyData; //new Buffer(verifyData).toString("hex"); - // const pubHex = publicKeyBytes; //new Buffer(publicKeyBytes).toString("hex"); - // console.log("sig msg pub", sigHex, msgHex, pubHex); - // const isValid = p256.verify(sigHex, msgHex, pubHex); - - ///////// + // 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"); @@ -498,9 +487,9 @@ 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); + // 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( @@ -510,8 +499,6 @@ export async function verifyJwt( // ); // console.log("isValid", isValid); - ///////// - const WebCrypto = await getWebCrypto(); const verifyAlgorithm = { name: "ECDSA", @@ -521,10 +508,6 @@ export async function verifyJwt( name: "ECDSA", namedCurve: publicKeyJwk.crv, }; - // const publicKeyCryptoKey = await importKey({ - // publicKeyJwk, - // algorithm: keyAlgorithm, - // }); const publicKeyCryptoKey = await WebCrypto.subtle.importKey( "jwk", publicKeyJwk, @@ -532,17 +515,17 @@ export async function verifyJwt( false, ["verify"], ); - console.log("verifyAlgorithm", verifyAlgorithm); - console.log("publicKeyCryptoKey", publicKeyCryptoKey); - console.log("finalSigBuffer", finalSigBuffer); - console.log("preimage", preimage); + // 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); + // console.log("verified", verified); return verified; } @@ -552,7 +535,8 @@ async function peerDidToDidDocument(did: string): Promise { "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 + // this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types + // (another reference is the @aviarytech/did-peer resolver) const id = did.split(":")[2]; const multibase = id.slice(1); const encnumbasis = multibase.slice(1); @@ -617,6 +601,31 @@ function base64urlEncode(buffer: ArrayBuffer) { return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } +// from @simplewebauthn/browser +function bufferToBase64URLString(buffer) { + 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 base64URLStringToBuffer(base64URLString) { + 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; +} + function cborToKeys(publicKeyBytes: Uint8Array) { const jwkObj = cborDecode(publicKeyBytes); console.log("jwkObj from verification", jwkObj); diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 82f51bd..a6a6963 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -210,6 +210,7 @@ import { verifyJwt, } from "@/libs/didPeer"; import { Buffer } from "buffer"; +import {JWTPayload} from "did-jwt"; const inputFileNameRef = ref(); @@ -222,8 +223,14 @@ export default class Help extends Vue { authenticatorData?: ArrayBuffer; credId?: Base64URLString; jwt?: string; - payload = { a: 1 }; + jwt2?: string; + payload = { + "@context": "https://schema.org", + type: "GiveAction", + description: "pizza", + }; peerSetup?: PeerSetup; + peerSetup2?: PeerSetup; publicKeyJwk?: JWK; publicKeyBytes?: Uint8Array; rawId?: Uint8Array; @@ -277,19 +284,40 @@ export default class Help extends Vue { public async create() { console.log("Starting a create"); const did = createPeerDid(this.publicKeyBytes as Uint8Array); - console.log("did", did); + // console.log("did", did); + + // from createJWT in did-jwt/src/JWT.ts + const timestamps: Partial = { + iat: Math.floor(Date.now() / 1000), + exp: undefined, + }; + const fullPayload = { ...timestamps, ...this.payload, did }; + this.peerSetup = new PeerSetup(); const rawJwt = await this.peerSetup.createJwt( - this.payload, - did, + fullPayload, this.credId as string, ); - console.log("raw jwt", rawJwt); + //console.log("simple raw jwt", rawJwt); this.jwt = rawJwt .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); - console.log("jwt4url", this.jwt); + console.log("simple jwt4url", this.jwt); + // console.log("simple peerSetup", this.peerSetup); + + this.peerSetup2 = new PeerSetup(); + const rawJwt2 = await this.peerSetup2.createJwt2( + fullPayload, + this.credId as string, + ); + // console.log("lower raw jwt2", rawJwt2); + this.jwt2 = rawJwt2 + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + console.log("lower jwt4url2", this.jwt2); + // console.log("lower peerSetup2", this.peerSetup2); } public async verify() { @@ -311,6 +339,21 @@ export default class Help extends Vue { this.peerSetup.signature as Base64URLString, ); console.log("decoded", decoded); + + const decoded2 = await verifyJwt( + this.jwt2 as string, + this.credId as Base64URLString, + this.rawId as Uint8Array, + this.peerSetup2.authenticatorData as ArrayBuffer, + this.peerSetup2.authenticatorDataBase64Url as Base64URLString, + this.peerSetup2.challenge as Uint8Array, + this.peerSetup2.clientDataJsonDecoded, + this.peerSetup2.clientDataJsonBase64Url as Base64URLString, + this.publicKeyBytes as Uint8Array, + this.publicKeyJwk as JWK, + this.peerSetup2.signature as Base64URLString, + ); + console.log("decoded2", decoded2); } }