From cca9ec80092d60a295b0074895215b03fc98b1f7 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 12 Jul 2024 11:55:10 -0600 Subject: [PATCH 1/2] move low-level DID-related create & decode into separate folder --- README.md | 2 +- src/constants/app.ts | 3 + src/libs/crypto/index.ts | 55 +-------- src/libs/crypto/vc/index.ts | 106 ++++++++++++++++++ .../vc/passkeyDidPeer.ts} | 5 +- src/libs/crypto/{ => vc}/passkeyHelpers.ts | 0 src/libs/endorserServer.ts | 37 +++--- src/libs/util.ts | 7 +- src/views/ContactQRScanShowView.vue | 6 +- src/views/HomeView.vue | 36 ++++-- src/views/TestView.vue | 21 +++- 11 files changed, 181 insertions(+), 97 deletions(-) create mode 100644 src/libs/crypto/vc/index.ts rename src/libs/{didPeer.ts => crypto/vc/passkeyDidPeer.ts} (99%) rename src/libs/crypto/{ => vc}/passkeyHelpers.ts (100%) diff --git a/README.md b/README.md index c79de4e..2510962 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ npm run lint ``` # (Let's replace this with a .env.development or .env.staging file.) # The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there. -TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build +TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app PASSKEYS_ENABLED=yep npm run build ``` * Production diff --git a/src/constants/app.ts b/src/constants/app.ts index a1bcf12..f42f922 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -36,6 +36,9 @@ export const DEFAULT_PUSH_SERVER = export const IMAGE_TYPE_PROFILE = "profile"; +export const PASSKEYS_ENABLED = + !!import.meta.env.VITE_PASSKEYS_ENABLED || false; + /** * The possible values for "group" and "type" are in App.vue. * From the notiwind package diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index 169f271..862ed54 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -3,14 +3,13 @@ import { getRandomBytesSync } from "ethereum-cryptography/random"; import { entropyToMnemonic } from "ethereum-cryptography/bip39"; import { wordlist } from "ethereum-cryptography/bip39/wordlists/english"; import { HDNode } from "@ethersproject/hdnode"; -import * as didJwt from "did-jwt"; -import * as u8a from "uint8arrays"; import { - createEndorserJwt, + createEndorserJwtForDid, ENDORSER_JWT_URL_LOCATION, } from "@/libs/endorserServer"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; +import { decodeEndorserJwt } from "@/libs/crypto/vc"; export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'"; @@ -95,58 +94,12 @@ export const accessToken = async (did?: string) => { const nowEpoch = Math.floor(Date.now() / 1000); const endEpoch = nowEpoch + 60; // add one minute const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }; - return createEndorserJwt(did, tokenPayload); + return createEndorserJwtForDid(did, tokenPayload); } else { return ""; } }; -/** - * Copied out of did-jwt since it's deprecated in that library. - * - * The SimpleSigner returns a configured function for signing data. - * - * @example - * const signer = SimpleSigner(import.meta.env.PRIVATE_KEY) - * signer(data, (err, signature) => { - * ... - * }) - * - * @param {String} hexPrivateKey a hex encoded private key - * @return {Function} a configured signer function - */ -export function SimpleSigner(hexPrivateKey: string): didJwt.Signer { - const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true); - return async (data) => { - const signature = (await signer(data)) as string; - return fromJose(signature); - }; -} - -// from did-jwt/util; see SimpleSigner above -export function fromJose(signature: string): { - r: string; - s: string; - recoveryParam?: number; -} { - const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature); - if (signatureBytes.length < 64 || signatureBytes.length > 65) { - throw new TypeError( - `Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`, - ); - } - const r = bytesToHex(signatureBytes.slice(0, 32)); - const s = bytesToHex(signatureBytes.slice(32, 64)); - const recoveryParam = - signatureBytes.length === 65 ? signatureBytes[64] : undefined; - return { r, s, recoveryParam }; -} - -// from did-jwt/util; see SimpleSigner above -export function bytesToHex(b: Uint8Array): string { - return u8a.toString(b, "base16"); -} - /** @return results of uportJwtPayload: { iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } } @@ -163,7 +116,7 @@ export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => { } // JWT format: { header, payload, signature, data } - const jwt = didJwt.decodeJWT(jwtText); + const jwt = decodeEndorserJwt(jwtText); return jwt.payload; }; diff --git a/src/libs/crypto/vc/index.ts b/src/libs/crypto/vc/index.ts new file mode 100644 index 0000000..94355ef --- /dev/null +++ b/src/libs/crypto/vc/index.ts @@ -0,0 +1,106 @@ +/** + * Verifiable Credential & DID functions, specifically for EndorserSearch.org tools + */ + +import * as didJwt from "did-jwt"; +import { IIdentifier } from "@veramo/core"; +import * as u8a from "uint8arrays"; + +import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer"; +import {JWTDecoded} from "did-jwt/lib/JWT"; + +/** + * Meta info about a key + */ +export interface KeyMeta { + /** + * Decentralized ID for the key + */ + did: string; + /** + * Stringified IIDentifier object from Veramo + */ + identity?: string; + /** + * The Webauthn credential ID in hex, if this is from a passkey + */ + passkeyCredIdHex?: string; +} + +/** + * Tell whether a key is from a passkey + * @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey + */ +export function isFromPasskey(keyMeta?: KeyMeta): boolean { + return !!keyMeta?.passkeyCredIdHex; +} + +export async function createEndorserJwtForKey( + account: KeyMeta, + payload: object, +) { + if (account?.identity) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const identity: IIdentifier = JSON.parse(account.identity!); + const privateKeyHex = identity.keys[0].privateKeyHex; + const signer = await SimpleSigner(privateKeyHex as string); + return didJwt.createJWT(payload, { + issuer: account.did, + signer: signer, + }); + } else if (account?.passkeyCredIdHex) { + return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload); + } else { + throw new Error("No identity data found to sign for DID " + account.did); + } +} + +/** + * Copied out of did-jwt since it's deprecated in that library. + * + * The SimpleSigner returns a configured function for signing data. + * + * @example + * const signer = SimpleSigner(import.meta.env.PRIVATE_KEY) + * signer(data, (err, signature) => { + * ... + * }) + * + * @param {String} hexPrivateKey a hex encoded private key + * @return {Function} a configured signer function + */ +function SimpleSigner(hexPrivateKey: string): didJwt.Signer { + const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true); + return async (data) => { + const signature = (await signer(data)) as string; + return fromJose(signature); + }; +} + +// from did-jwt/util; see SimpleSigner above +function fromJose(signature: string): { + r: string; + s: string; + recoveryParam?: number; +} { + const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature); + if (signatureBytes.length < 64 || signatureBytes.length > 65) { + throw new TypeError( + `Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`, + ); + } + const r = bytesToHex(signatureBytes.slice(0, 32)); + const s = bytesToHex(signatureBytes.slice(32, 64)); + const recoveryParam = + signatureBytes.length === 65 ? signatureBytes[64] : undefined; + return {r, s, recoveryParam}; +} + +// from did-jwt/util; see SimpleSigner above +function bytesToHex(b: Uint8Array): string { + return u8a.toString(b, "base16"); +} + +export function decodeEndorserJwt(jwt: string): JWTDecoded { + return didJwt.decodeJWT(jwt); +} diff --git a/src/libs/didPeer.ts b/src/libs/crypto/vc/passkeyDidPeer.ts similarity index 99% rename from src/libs/didPeer.ts rename to src/libs/crypto/vc/passkeyDidPeer.ts index acc0fbf..6431123 100644 --- a/src/libs/didPeer.ts +++ b/src/libs/crypto/vc/passkeyDidPeer.ts @@ -21,7 +21,10 @@ import { } from "@simplewebauthn/types"; import { AppString } from "@/constants/app"; -import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; +import { + getWebCrypto, + unwrapEC2Signature, +} from "@/libs/crypto/vc/passkeyHelpers"; const PEER_DID_PREFIX = "did:peer:"; const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0"; diff --git a/src/libs/crypto/passkeyHelpers.ts b/src/libs/crypto/vc/passkeyHelpers.ts similarity index 100% rename from src/libs/crypto/passkeyHelpers.ts rename to src/libs/crypto/vc/passkeyHelpers.ts diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index f05e6de..ff4bbd6 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1,14 +1,13 @@ import { Axios, AxiosRequestConfig, AxiosResponse } from "axios"; -import * as didJwt from "did-jwt"; import { LRUCache } from "lru-cache"; import * as R from "ramda"; import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; import { Contact } from "@/db/tables/contacts"; -import { accessToken, SimpleSigner } from "@/libs/crypto"; +import { accessToken } from "@/libs/crypto"; import { NonsensitiveDexie } from "@/db/index"; -import { createDidPeerJwt } from "@/libs/didPeer"; import { getAccount, getIdentity } from "@/libs/util"; +import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc"; export const SCHEMA_ORG_CONTEXT = "https://schema.org"; // the object in RegisterAction claims @@ -692,7 +691,7 @@ export async function createAndSubmitClaim( }, }; - const vcJwt: string = await createEndorserJwt(issuerDid, vcPayload); + const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload); // Make the xhr request payload const payload = JSON.stringify({ jwtEncoded: vcJwt }); @@ -722,6 +721,14 @@ export async function createAndSubmitClaim( } } +export async function createEndorserJwtForDid( + issuerDid: string, + payload: object, +) { + const account = await getAccount(issuerDid); + return createEndorserJwtForKey(account as KeyMeta, payload); +} + /** * An AcceptAction is when someone accepts some contract or pledge. * @@ -937,25 +944,7 @@ export async function createEndorserJwtVcFromClaim( credentialSubject: claim, }, }; - return createEndorserJwt(issuerDid, vcPayload); -} - -export async function createEndorserJwt(issuerDid: string, payload: object) { - const account = await getAccount(issuerDid); - if (account?.identity) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const identity = JSON.parse(account.identity!); - const privateKeyHex = identity.keys[0].privateKeyHex; - const signer = await SimpleSigner(privateKeyHex); - return didJwt.createJWT(payload, { - issuer: issuerDid, - signer: signer, - }); - } else if (account?.passkeyCredIdHex) { - return createDidPeerJwt(issuerDid, account.passkeyCredIdHex, payload); - } else { - throw new Error("No identity data found to sign for DID " + issuerDid); - } + return createEndorserJwtForDid(issuerDid, vcPayload); } export async function register( @@ -980,7 +969,7 @@ export async function register( }, }; // Create a signature using private key of identity - const vcJwt = await createEndorserJwt(activeDid, vcPayload); + const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload); const url = apiServer + "/api/v2/claim"; const resp = await axios.post(url, { jwtEncoded: vcJwt }); diff --git a/src/libs/util.ts b/src/libs/util.ts index b92f6c7..f7de997 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -11,9 +11,10 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer"; -import { createPeerDid, registerCredential } from "@/libs/didPeer"; +import { createPeerDid, registerCredential } from "@/libs/crypto/vc/passkeyDidPeer"; import { Buffer } from "buffer"; +import {KeyMeta} from "@/libs/crypto/vc"; export const PRIVACY_MESSAGE = "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; @@ -196,9 +197,11 @@ export function findAllVisibleToDids( * **/ +export interface AccountKeyInfo extends Account, KeyMeta {} + export const getAccount = async ( activeDid: string, -): Promise => { +): Promise => { await accountsDB.open(); const account = (await accountsDB.accounts .where("did") diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index cbb518b..c14f350 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -84,7 +84,6 @@ import { useClipboard } from "@vueuse/core"; import QuickNav from "@/components/QuickNav.vue"; import { NotificationIface } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; -import { Account } from "@/db/tables/accounts"; import { Contact } from "@/db/tables/contacts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { @@ -93,8 +92,7 @@ import { nextDerivationPath, } from "@/libs/crypto"; import { - CONTACT_URL_PREFIX, - createEndorserJwt, + CONTACT_URL_PREFIX, createEndorserJwtForDid, ENDORSER_JWT_URL_LOCATION, isDid, register, @@ -161,7 +159,7 @@ export default class ContactQRScanShow extends Vue { }, }; - const vcJwt: string = await createEndorserJwt(identity.did, contactInfo); + const vcJwt: string = await createEndorserJwtForDid(identity.did, contactInfo); const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION; this.qrValue = viewPrefix + vcJwt; } diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index ff97ec3..53ac742 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -81,16 +81,29 @@ v-if="!activeDid" class="bg-amber-200 rounded-md text-center px-4 py-3 mb-4" > -

- To recognize giving, have someone register you: -

-
- - - - - - +
+

+ Choose how to see info from your contacts or share contributions: +

+
+ + + Give me all the options. + +
+
+
+

+ To recognize giving or collaborate, have someone register you: +

void; AppString = AppString; + PASSKEYS_ENABLED = PASSKEYS_ENABLED; activeDid = ""; allContacts: Array = []; diff --git a/src/views/TestView.vue b/src/views/TestView.vue index f8fc272..f717436 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -246,14 +246,15 @@ import { Component, Vue } from "vue-facing-decorator"; import QuickNav from "@/components/QuickNav.vue"; import { AppString, NotificationIface } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; +import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; +import * as vcLib from "@/libs/crypto/vc"; import { PeerSetup, verifyJwtP256, verifyJwtSimplewebauthn, verifyJwtWebCrypto, -} from "@/libs/didPeer"; -import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; -import { registerAndSavePasskey } from "@/libs/util"; +} from "@/libs/crypto/vc/passkeyDidPeer"; +import {AccountKeyInfo, getAccount, registerAndSavePasskey} from "@/libs/util"; const inputFileNameRef = ref(); @@ -360,6 +361,13 @@ export default class Help extends Vue { } public async createJwtSimplewebauthn() { + const account: AccountKeyInfo | undefined = await getAccount( + this.activeDid || "", + ); + if (!vcLib.isFromPasskey(account)) { + alert(`The DID ${this.activeDid} is not passkey-enabled.`); + return; + } this.peerSetup = new PeerSetup(); this.jwt = await this.peerSetup.createJwtSimplewebauthn( this.activeDid as string, @@ -370,6 +378,13 @@ export default class Help extends Vue { } public async createJwtNavigator() { + const account: AccountKeyInfo | undefined = await getAccount( + this.activeDid || "", + ); + if (!vcLib.isFromPasskey(account)) { + alert(`The DID ${this.activeDid} is not passkey-enabled.`); + return; + } this.peerSetup = new PeerSetup(); this.jwt = await this.peerSetup.createJwtNavigator( this.activeDid as string, -- 2.30.2 From 4bb311195b7d0ed2e12810033a09267a519829c1 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 12 Jul 2024 22:02:20 -0600 Subject: [PATCH 2/2] refine "vc" functions further, separating passkey-specific items from did-peer file --- src/libs/crypto/vc/didPeer.ts | 96 ++++++++++++++++++++++++++++ src/libs/crypto/vc/index.ts | 8 ++- src/libs/crypto/vc/passkeyDidPeer.ts | 90 +++----------------------- src/libs/util.ts | 3 +- 4 files changed, 112 insertions(+), 85 deletions(-) create mode 100644 src/libs/crypto/vc/didPeer.ts diff --git a/src/libs/crypto/vc/didPeer.ts b/src/libs/crypto/vc/didPeer.ts new file mode 100644 index 0000000..9c0fa73 --- /dev/null +++ b/src/libs/crypto/vc/didPeer.ts @@ -0,0 +1,96 @@ +import {Buffer} from "buffer/"; +import {decode as cborDecode} from "cbor-x"; +import {bytesToMultibase, multibaseToBytes} from "did-jwt"; + +import {getWebCrypto} from "@/libs/crypto/vc/passkeyHelpers"; + +const PEER_DID_PREFIX = "did:peer:"; +const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0"; + +/** + * + * + * similar code is in crowd-funder-for-time-pwa libs/crypto/vc/passkeyDidPeer.ts verifyJwtWebCrypto + * + * @returns {Promise} + */ +export async function verifyPeerSignature( + payloadBytes: Buffer, + issuerDid: string, + signatureBytes: Uint8Array, +): Promise { + const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); + + const WebCrypto = await getWebCrypto(); + const verifyAlgorithm = { + name: "ECDSA", + hash: { name: "SHA-256" }, + }; + const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk; + const keyAlgorithm = { + name: "ECDSA", + namedCurve: publicKeyJwk.crv, + }; + const publicKeyCryptoKey = await WebCrypto.subtle.importKey( + "jwk", + publicKeyJwk, + keyAlgorithm, + false, + ["verify"], + ); + const verified = await WebCrypto.subtle.verify( + verifyAlgorithm, + publicKeyCryptoKey, + signatureBytes, + payloadBytes, + ); + return verified; +} + +export function cborToKeys(publicKeyBytes: Uint8Array) { + const jwkObj = cborDecode(publicKeyBytes); + if ( + jwkObj[1] != 2 || // kty "EC" + jwkObj[3] != -7 || // alg "ES256" + jwkObj[-1] != 1 || // crv "P-256" + jwkObj[-2].length != 32 || // x + jwkObj[-3].length != 32 // y + ) { + throw new Error("Unable to extract key."); + } + const publicKeyJwk = { + alg: "ES256", + crv: "P-256", + kty: "EC", + x: arrayToBase64Url(jwkObj[-2]), + y: arrayToBase64Url(jwkObj[-3]), + }; + const publicKeyBuffer = Buffer.concat([ + Buffer.from(jwkObj[-2]), + Buffer.from(jwkObj[-3]), + ]); + return { publicKeyJwk, publicKeyBuffer }; +} + +export function toBase64Url(anythingB64: string) { + return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +export function arrayToBase64Url(anything: Uint8Array) { + return toBase64Url(Buffer.from(anything).toString("base64")); +} + +export function peerDidToPublicKeyBytes(did: string) { + return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length)); +} + +export function createPeerDid(publicKeyBytes: Uint8Array) { + // https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67 + //const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME }); + const methodSpecificId = bytesToMultibase( + publicKeyBytes, + "base58btc", + "p256-pub", + ); + return PEER_DID_MULTIBASE_PREFIX + methodSpecificId; +} \ No newline at end of file diff --git a/src/libs/crypto/vc/index.ts b/src/libs/crypto/vc/index.ts index 94355ef..7736708 100644 --- a/src/libs/crypto/vc/index.ts +++ b/src/libs/crypto/vc/index.ts @@ -1,13 +1,17 @@ /** * Verifiable Credential & DID functions, specifically for EndorserSearch.org tools + * + * The goal is to make this folder similar across projects, then move it to a library. + * Other projects: endorser-ch, image-api + * */ import * as didJwt from "did-jwt"; +import { JWTDecoded } from "did-jwt/lib/JWT"; import { IIdentifier } from "@veramo/core"; import * as u8a from "uint8arrays"; import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer"; -import {JWTDecoded} from "did-jwt/lib/JWT"; /** * Meta info about a key @@ -93,7 +97,7 @@ function fromJose(signature: string): { const s = bytesToHex(signatureBytes.slice(32, 64)); const recoveryParam = signatureBytes.length === 65 ? signatureBytes[64] : undefined; - return {r, s, recoveryParam}; + return { r, s, recoveryParam }; } // from did-jwt/util; see SimpleSigner above diff --git a/src/libs/crypto/vc/passkeyDidPeer.ts b/src/libs/crypto/vc/passkeyDidPeer.ts index 6431123..920d751 100644 --- a/src/libs/crypto/vc/passkeyDidPeer.ts +++ b/src/libs/crypto/vc/passkeyDidPeer.ts @@ -1,6 +1,5 @@ import { Buffer } from "buffer/"; -import { decode as cborDecode } from "cbor-x"; -import { bytesToMultibase, JWTPayload, multibaseToBytes } from "did-jwt"; +import { JWTPayload } from "did-jwt"; import { DIDResolutionResult } from "did-resolver"; import { sha256 } from "ethereum-cryptography/sha256.js"; import { @@ -21,13 +20,14 @@ import { } from "@simplewebauthn/types"; import { AppString } from "@/constants/app"; +import { unwrapEC2Signature } from "@/libs/crypto/vc/passkeyHelpers"; import { - getWebCrypto, - unwrapEC2Signature, -} from "@/libs/crypto/vc/passkeyHelpers"; + arrayToBase64Url, + cborToKeys, + peerDidToPublicKeyBytes, + verifyPeerSignature, +} from "@/libs/crypto/vc/didPeer"; -const PEER_DID_PREFIX = "did:peer:"; -const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0"; export interface JWK { kty: string; crv: string; @@ -35,14 +35,6 @@ export interface JWK { y: string; } -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(passkeyName?: string) { const options: PublicKeyCredentialCreationOptionsJSON = await generateRegistrationOptions({ @@ -95,21 +87,6 @@ export async function registerCredential(passkeyName?: string) { }; } -export function createPeerDid(publicKeyBytes: Uint8Array) { - // https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67 - //const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME }); - const methodSpecificId = bytesToMultibase( - publicKeyBytes, - "base58btc", - "p256-pub", - ); - return PEER_DID_MULTIBASE_PREFIX + methodSpecificId; -} - -function peerDidToPublicKeyBytes(did: string) { - return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length)); -} - export class PeerSetup { public authenticatorData?: ArrayBuffer; public challenge?: Uint8Array; @@ -422,33 +399,7 @@ export async function verifyJwtWebCrypto( // Construct the preimage const preimage = Buffer.concat([authDataFromBase, hash]); - - const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); - - const WebCrypto = await getWebCrypto(); - const verifyAlgorithm = { - name: "ECDSA", - hash: { name: "SHA-256" }, - }; - const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk; - const keyAlgorithm = { - name: "ECDSA", - namedCurve: publicKeyJwk.crv, - }; - const publicKeyCryptoKey = await WebCrypto.subtle.importKey( - "jwk", - publicKeyJwk, - keyAlgorithm, - false, - ["verify"], - ); - const verified = await WebCrypto.subtle.verify( - verifyAlgorithm, - publicKeyCryptoKey, - finalSigBuffer, - preimage, - ); - return verified; + return verifyPeerSignature(preimage, issuerDid, finalSigBuffer); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -554,31 +505,6 @@ function base64URLStringToArrayBuffer(base64URLString: string) { return buffer; } -function cborToKeys(publicKeyBytes: Uint8Array) { - const jwkObj = cborDecode(publicKeyBytes); - if ( - jwkObj[1] != 2 || // kty "EC" - jwkObj[3] != -7 || // alg "ES256" - jwkObj[-1] != 1 || // crv "P-256" - jwkObj[-2].length != 32 || // x - jwkObj[-3].length != 32 // y - ) { - throw new Error("Unable to extract key."); - } - const publicKeyJwk = { - alg: "ES256", - crv: "P-256", - kty: "EC", - x: arrayToBase64Url(jwkObj[-2]), - y: arrayToBase64Url(jwkObj[-3]), - }; - const publicKeyBuffer = Buffer.concat([ - Buffer.from(jwkObj[-2]), - Buffer.from(jwkObj[-3]), - ]); - return { publicKeyJwk, publicKeyBuffer }; -} - // eslint-disable-next-line @typescript-eslint/no-unused-vars async function pemToCryptoKey(pem: string) { const binaryDerString = atob( diff --git a/src/libs/util.ts b/src/libs/util.ts index f7de997..c5f3b39 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -11,10 +11,11 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer"; -import { createPeerDid, registerCredential } from "@/libs/crypto/vc/passkeyDidPeer"; +import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer"; import { Buffer } from "buffer"; import {KeyMeta} from "@/libs/crypto/vc"; +import {createPeerDid} from "@/libs/crypto/vc/didPeer"; export const PRIVACY_MESSAGE = "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; -- 2.30.2