add link directly into contact page to add a new contact via "contactJwt" query parameter

This commit is contained in:
2024-09-27 18:41:11 -06:00
parent 2942a02a4e
commit a271d9c206
13 changed files with 204 additions and 78 deletions

View File

@@ -0,0 +1,46 @@
/**
* This did:ethr resolver instructs the did-jwt machinery to use the
* EcdsaSecp256k1RecoveryMethod2020Uses verification method which adds the recovery bit to the
* signature to recover the DID's public key from a signature.
*
* This effectively hard codes the did:ethr DID resolver to use the address as the public key.
* @param did : string
* @returns {Promise<DIDResolutionResult>}
*
* Similar code resides in image-api
*/
export const didEthLocalResolver = async (did: string) => {
const didRegex = /^did:ethr:(0x[0-9a-fA-F]{40})$/;
const match = did.match(didRegex);
if (match) {
const address = match[1]; // Extract eth address: 0x...
const publicKeyHex = address; // Use the address directly as a public key placeholder
return {
didDocumentMetadata: {},
didResolutionMetadata: {
contentType: "application/did+ld+json",
},
didDocument: {
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/secp256k1recovery-2020/v2",
],
id: did,
verificationMethod: [
{
id: `${did}#controller`,
type: "EcdsaSec256k1RecoveryMethod2020",
controller: did,
blockchainAccountId: "eip155:1:" + publicKeyHex,
},
],
authentication: [`${did}#controller`],
assertionMethod: [`${did}#controller`],
},
};
}
throw new Error(`Unsupported DID format: ${did}`);
};

View File

@@ -6,14 +6,22 @@
*
*/
import { Buffer } from "buffer/";
import * as didJwt from "did-jwt";
import { JWTVerified } from "did-jwt";
import { JWTDecoded } from "did-jwt/lib/JWT";
import { Resolver } from "did-resolver";
import { IIdentifier } from "@veramo/core";
import * as u8a from "uint8arrays";
import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer";
import { didEthLocalResolver } from "./did-eth-local-resolver";
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
import { urlBase64ToUint8Array } from "./util";
export const ETHR_DID_PREFIX = "did:ethr:";
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
/**
* Meta info about a key
@@ -33,6 +41,8 @@ export interface KeyMeta {
passkeyCredIdHex?: string;
}
const resolver = new Resolver({ ethr: didEthLocalResolver });
/**
* 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
@@ -107,6 +117,78 @@ function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}
// We should be calling 'verify' in more places, showing warnings if it fails.
export function decodeEndorserJwt(jwt: string): JWTDecoded {
return didJwt.decodeJWT(jwt);
}
// return Promise of at least { issuer, payload, verified boolean }
// ... and also if successfully verified by did-jwt (not JWANT): data, doc, signature, signer
export async function decodeAndVerifyJwt(
jwt: string,
): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> {
const pieces = jwt.split(".");
console.log("WTF decodeAndVerifyJwt", typeof jwt, jwt, pieces);
const header = JSON.parse(base64urlDecodeString(pieces[0]));
const payload = JSON.parse(base64urlDecodeString(pieces[1]));
console.log("WTF decodeAndVerifyJwt after", header, payload);
const issuerDid = payload.iss;
if (!issuerDid) {
return Promise.reject({
clientError: {
message: `Missing "iss" field in JWT.`,
},
});
}
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
try {
const verified = await didJwt.verifyJWT(jwt, { resolver });
return verified;
} catch (e: unknown) {
return Promise.reject({
clientError: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
message: `JWT failed verification: ` + e.toString(),
code: JWT_VERIFY_FAILED_CODE,
},
});
}
}
if (issuerDid.startsWith(PEER_DID_PREFIX) && header.typ === "JWANT") {
const verified = await verifyPeerSignature(
Buffer.from(payload),
issuerDid,
urlBase64ToUint8Array(pieces[2]),
);
if (!verified) {
return Promise.reject({
clientError: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
message: `JWT failed verification: ` + e.toString(),
code: JWT_VERIFY_FAILED_CODE,
},
});
} else {
return { issuer: issuerDid, payload: payload, verified: true };
}
}
if (issuerDid.startsWith(PEER_DID_PREFIX)) {
return Promise.reject({
clientError: {
message: `JWT with a PEER DID currently only supported with typ == JWANT. Contact us us for JWT suport since it should be straightforward.`,
},
});
}
return Promise.reject({
clientError: {
message: `Unsupported DID method ${issuerDid}`,
code: UNSUPPORTED_DID_METHOD_CODE,
},
});
}

View File

@@ -470,8 +470,18 @@ ${pubKeyBuffer.toString("base64")}
return pem;
}
// tried the base64url library but got an error using their Buffer
export function base64urlDecodeString(input: string) {
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
}
// tried the base64url library but got an error using their Buffer
export function base64urlEncodeString(input: string) {
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlDecode(input: string) {
function base64urlDecodeArrayBuffer(input: string) {
input = input.replace(/-/g, "+").replace(/_/g, "/");
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
const str = atob(input + pad);
@@ -483,9 +493,9 @@ function base64urlDecode(input: string) {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlEncode(buffer: ArrayBuffer) {
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
const str = String.fromCharCode(...new Uint8Array(buffer));
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return base64urlEncodeString(str);
}
// from @simplewebauthn/browser

View File

@@ -0,0 +1,11 @@
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}