diff --git a/package-lock.json b/package-lock.json index 92cd992..d06eb6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6018,6 +6018,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/ed25519": { "version": "1.7.3", "funding": [ @@ -6030,8 +6041,9 @@ "optional": true }, "node_modules/@noble/hashes": { - "version": "1.3.2", - "license": "MIT", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "engines": { "node": ">= 16" }, @@ -8197,6 +8209,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", "optional": true, @@ -12718,6 +12741,17 @@ "node": ">=14.0.0" } }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ethers/node_modules/@types/node": { "version": "18.15.13", "license": "MIT" diff --git a/src/libs/didPeer.ts b/src/libs/didPeer.ts index 6cb4e9c..6ac1137 100644 --- a/src/libs/didPeer.ts +++ b/src/libs/didPeer.ts @@ -3,6 +3,7 @@ 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 { sha256 } from "ethereum-cryptography/sha256.js"; import { bytesToMultibase } from "@veramo/utils"; import { startAuthentication, @@ -22,6 +23,8 @@ import { PublicKeyCredentialRequestOptionsJSON, } from "@simplewebauthn/types"; +import { generateRandomBytes } from "@/libs/crypto"; + export interface JWK { kty: string; crv: string; @@ -33,15 +36,15 @@ export interface PublicKeyCredential { jwt: JWK; } -function toBase64Url(anything: Uint8Array) { - return Buffer.from(anything) - .toString("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); +function toBase64Url(anythingB64: string) { + return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function arrayToBase64Url(anything: Uint8Array) { + return toBase64Url(Buffer.from(anything).toString("base64")); } -export async function registerCredential() { +export async function registerCredential(userId: Uint8Array) { const options: PublicKeyCredentialCreationOptionsJSON = await generateRegistrationOptions({ rpName: "Time Safari", @@ -85,10 +88,8 @@ export async function registerCredential() { }; } -export async function registerCredential2( - userId: Uint8Array, - challenge: Uint8Array, -) { +export async function registerCredential2(userId: Uint8Array) { + const challenge = generateRandomBytes(32); const publicKeyOptions: PublicKeyCredentialCreationOptions = { challenge: challenge, rp: { @@ -120,20 +121,20 @@ export async function registerCredential2( console.log("credential", credential); console.log(credential?.id, " is the new ID base64-url-encoded"); - console.log(toBase64Url(credential?.rawId), " is the base64 rawId"); + 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: toBase64Url(attestationResponse?.attestationObject), - clientDataJSON: toBase64Url(attestationResponse?.clientDataJSON), + attestationObject: arrayToBase64Url(attestationResponse?.attestationObject), + clientDataJSON: arrayToBase64Url(attestationResponse?.clientDataJSON), }, clientExtensionResults: {}, type: "public-key", }, - expectedChallenge: toBase64Url(challenge), + expectedChallenge: arrayToBase64Url(challenge), expectedOrigin: window.location.origin, }; console.log("verfInput", verfInput); @@ -169,8 +170,8 @@ export async function registerCredential2( alg: "ES256", crv: "P-256", kty: "EC", - x: toBase64Url(jwkObj[-2]), - y: toBase64Url(jwkObj[-3]), + x: arrayToBase64Url(jwkObj[-2]), + y: arrayToBase64Url(jwkObj[-3]), }; const publicKeyBytes = Buffer.concat([ Buffer.from(jwkObj[-2]), @@ -207,12 +208,65 @@ export function createPeerDid(publicKeyBytes: Uint8Array) { export class PeerSetup { public authenticatorData?: ArrayBuffer; + public challenge?: Uint8Array; public clientDataJsonDecoded?: object; + public signature?: Base64URLString; public async createJwt( payload: object, issuerDid: string, 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)), + ); + const options: PublicKeyCredentialRequestOptionsJSON = + await generateAuthenticationOptions({ + challenge: payloadHash, + 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); + + const clientAuth = await startAuthentication(options); + console.log("custom clientAuth", clientAuth); + + this.authenticatorData = Buffer.from( + clientAuth.response.authenticatorData, + "base64", + ).buffer; + this.challenge = payloadHash; + this.clientDataJsonDecoded = JSON.parse( + Buffer.from(clientAuth.response.clientDataJSON, "base64").toString( + "utf-8", + ), + ); + this.signature = clientAuth.response.signature; + + const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64"); + const payloadBase64 = clientAuth.response.clientDataJSON; + const signature = clientAuth.response.signature; + + return headerBase64 + "." + payloadBase64 + "." + signature; + } + + public async createJwt2( + payload: object, + issuerDid: string, + credentialId: string, ) { const signer = await this.webAuthnES256KSigner(credentialId); @@ -319,58 +373,63 @@ export async function verifyJwt( credId: Base64URLString, rawId: Uint8Array, authenticatorData: ArrayBuffer, + challenge: Uint8Array, 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 = { - authenticator: { - credentialID: credId, - credentialPublicKey: publicKey, - counter: 0, - }, - expectedChallenge: options.challenge, - expectedOrigin: window.location.origin, - expectedRPID: window.location.hostname, - response: clientAuth, - }; - console.log("verfOpts", verfOpts); - const verificationFromClient = await verifyAuthenticationResponse(verfOpts); - console.log("client auth verification", verificationFromClient); - - const authData = toBase64Url(Buffer.from(authenticatorData)); + // 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 = { + // authenticator: { + // credentialID: credId, + // credentialPublicKey: publicKey, + // counter: 0, + // }, + // expectedChallenge: options.challenge, + // expectedOrigin: window.location.origin, + // expectedRPID: window.location.hostname, + // response: clientAuth, + // }; + // 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: publicKey, counter: 0, }, - expectedChallenge: options.challenge, + expectedChallenge: arrayToBase64Url(challenge), expectedOrigin: window.location.origin, expectedRPID: window.location.hostname, response: { authenticatorAttachment: "platform", clientExtensionResults: {}, id: credId, - rawId: toBase64Url(rawId), + rawId: arrayToBase64Url(rawId), response: { authenticatorData: authData, - clientDataJSON: clientAuth.response.clientDataJSON, - signature: clientAuth.response.signature, + clientDataJSON: arrayToBase64Url( + Buffer.from(JSON.stringify(clientDataJSON)), + ), + signature: signature, }, type: "public-key", }, diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 5f92271..a5cae98 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -222,6 +222,7 @@ export default class Help extends Vue { authenticatorData?: ArrayBuffer; credId?: Base64URLString; jwt?: string; + payload = { a: 1 }; peerSetup?: PeerSetup; publicKeyJwk?: JWK; publicKeyBytes?: Uint8Array; @@ -264,10 +265,8 @@ export default class Help extends Vue { const encodedUserId = Buffer.from(this.userId).toString("base64"); console.log("encodedUserId", encodedUserId); - const challenge = generateRandomBytes(32); const cred = await registerCredential( - this.userId as Uint8Array, - challenge as Uint8Array, + this.userId as Uint8Array ); console.log("public key", cred); this.publicKeyJwk = cred.publicKeyJwk; @@ -280,9 +279,12 @@ export default class Help extends Vue { public async create() { const did = createPeerDid(this.publicKeyBytes as Uint8Array); console.log("did", did); - const payload = { a: 1 }; this.peerSetup = new PeerSetup(); - const rawJwt = await this.peerSetup.createJwt(payload, did, this.credId as string); + const rawJwt = await this.peerSetup.createJwt( + this.payload, + did, + this.credId as string, + ); console.log("raw jwt", rawJwt); this.jwt = rawJwt .replace(/\+/g, "-") @@ -296,15 +298,15 @@ export default class Help extends Vue { alert("Create a JWT first."); return; } - const signature = this.jwt.split(".")[2]; const decoded = await verifyJwt( this.jwt, this.credId as Base64URLString, this.rawId as Uint8Array, this.peerSetup.authenticatorData as ArrayBuffer, + this.peerSetup.challenge as Uint8Array, this.peerSetup.clientDataJsonDecoded, this.publicKeyBytes as Uint8Array, - signature, + this.peerSetup.signature as Base64URLString, ); console.log("decoded", decoded); }