From e85ad529df7292d0526e6e36f6bde3670ef44f5c Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 24 May 2024 16:30:33 -0600 Subject: [PATCH] add prompt for a passkey and a start at creating a JWT out of it --- package-lock.json | 125 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/libs/didPeer.ts | 130 ++++++++++++++++++++++++++++++++++++++++ src/views/StartView.vue | 12 +++- 4 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 src/libs/didPeer.ts diff --git a/package-lock.json b/package-lock.json index 4e2bab3..58f38d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@vueuse/core": "^10.9.0", "@zxing/text-encoding": "^0.9.0", "axios": "^1.6.8", + "cbor-x": "^1.5.9", "class-transformer": "^0.5.1", "dexie": "^3.2.7", "dexie-export-import": "^4.1.1", @@ -2412,6 +2413,78 @@ "node": ">=8.9" } }, + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@dicebear/adventurer": { "version": "5.4.1", "license": "MIT", @@ -10936,6 +11009,35 @@ "@types/node": "*" } }, + "node_modules/cbor-extract": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.1.1" + }, + "bin": { + "download-cbor-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", + "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", + "@cbor-extract/cbor-extract-linux-x64": "2.2.0", + "@cbor-extract/cbor-extract-win32-x64": "2.2.0" + } + }, + "node_modules/cbor-x": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.9.tgz", + "integrity": "sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==", + "optionalDependencies": { + "cbor-extract": "^2.2.0" + } + }, "node_modules/chalk": { "version": "2.4.2", "devOptional": true, @@ -17826,6 +17928,29 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/node-int64": { "version": "0.4.0", "license": "MIT", diff --git a/package.json b/package.json index 1d6f317..227de5b 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@vueuse/core": "^10.9.0", "@zxing/text-encoding": "^0.9.0", "axios": "^1.6.8", + "cbor-x": "^1.5.9", "class-transformer": "^0.5.1", "dexie": "^3.2.7", "dexie-export-import": "^4.1.1", diff --git a/src/libs/didPeer.ts b/src/libs/didPeer.ts new file mode 100644 index 0000000..71c054f --- /dev/null +++ b/src/libs/didPeer.ts @@ -0,0 +1,130 @@ +import { decode as cborDecode } from "cbor-x"; +import { createJWS, JWTPayload } from "did-jwt"; + +export async function registerCredential() { + const publicKeyOptions: PublicKeyCredentialCreationOptions = { + challenge: new Uint8Array(32), // Random challenge + rp: { + name: "Time Safari", + id: window.location.hostname, + }, + user: { + id: new Uint8Array(16), // User ID + name: "user@example.com", + displayName: "Example 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); + const attestationResponse = credential?.response; + + // Parse the attestation response to get the public key + const clientDataJSON = attestationResponse.clientDataJSON; + console.log("clientDataJSON", clientDataJSON); + const attestationObject = cborDecode( + new Uint8Array(attestationResponse.attestationObject), + ); + + const authData = new Uint8Array(attestationObject.authData); + const publicKey = extractPublicKey(authData); + + return publicKey; +} + +// @ts-expect-error just because it doesn't like the "any" +function extractPublicKey(authData) { + // Extract the public key from authData using appropriate parsing + // This involves extracting the COSE key format and converting it to JWK + // For simplicity, we'll assume the public key is at a certain position in authData + const publicKeyCose = authData.slice(authData.length - 77); // Example position + const publicKeyJwk = coseToJwk(publicKeyCose); + return publicKeyJwk; +} + +// @ts-expect-error just because it doesn't like the "any" +function coseToJwk(coseKey) { + // Convert COSE key format to JWK + // This is simplified and needs appropriate parsing and conversion logic + return { + kty: "EC", + crv: "P-256", + x: btoa(coseKey.slice(2, 34)), + y: btoa(coseKey.slice(34, 66)), + }; +} + +async function generateWebAuthnSignature( + dataToSign: string | Uint8Array, + credentialID: ArrayBuffer, +) { + if (!(dataToSign instanceof Uint8Array)) { + dataToSign = new TextEncoder().encode(dataToSign as string); + } + const challenge = dataToSign; + + const options = { + challenge: challenge, + allowCredentials: [{ id: credentialID, type: "public-key" }], + userVerification: "preferred", + }; + + const assertion = await navigator.credentials.get({ publicKey: options }); + + const authenticatorAssertionResponse = assertion?.response; + return { + signature: authenticatorAssertionResponse.signature, + clientDataJSON: authenticatorAssertionResponse.clientDataJSON, + authenticatorData: authenticatorAssertionResponse.authenticatorData, + }; +} + +async function webAuthnES256KSigner(credentialID: ArrayBuffer) { + return async (data: string | Uint8Array) => { + // also has clientDataJSON + const { signature, authenticatorData } = await generateWebAuthnSignature( + data, + credentialID, + ); + + // Combine the WebAuthn components into a single signature format as required by did-jwt + const combinedSignature = Buffer.concat([ + Buffer.from(authenticatorData), + Buffer.from(signature), + ]); + + return combinedSignature.toString("base64"); + }; +} + +export async function createJwt( + payload: object, + issuerDid: string, + credentialId: ArrayBuffer, +) { + const signer = await 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 }; + + return createJWS(fullPayload, signer, header); +} diff --git a/src/views/StartView.vue b/src/views/StartView.vue index 04558e3..7da2040 100644 --- a/src/views/StartView.vue +++ b/src/views/StartView.vue @@ -64,6 +64,7 @@