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.
+