add expiration inside JWANT & refactor getHeaders to move toward supporting did:peer

This commit is contained in:
2024-07-09 17:56:48 -06:00
parent 42fde503e3
commit 45f0a14661
17 changed files with 163 additions and 276 deletions

View File

@@ -6,7 +6,8 @@ import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays";
import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { createEndorserJwt, ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
@@ -88,23 +89,35 @@ export const generateSeed = (): string => {
* @param {IIdentifier} identifier
* @return {*}
*/
export const accessToken = async (identifier: IIdentifier) => {
const did: string = identifier.did;
const privateKeyHex: string = identifier.keys[0].privateKeyHex as string;
export const accessToken = async (
identifier: IIdentifier | undefined,
did?: string,
) => {
if (did) {
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);
} else {
// deprecated
// must have identifier
const did = identifier?.did;
const privateKeyHex: string = identifier?.keys[0].privateKeyHex as string;
const signer = SimpleSigner(privateKeyHex);
const signer = SimpleSigner(privateKeyHex);
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt: string = await didJwt.createJWT(tokenPayload, {
alg,
issuer: did,
signer,
});
return jwt;
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt: string = await didJwt.createJWT(tokenPayload, {
alg,
issuer: did || "no DID set",
signer,
});
return jwt;
}
};
export const sign = async (privateKeyHex: string) => {

View File

@@ -88,7 +88,7 @@ export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
);
return toResolve;
}
export class MissingWebCrypto extends Error {
class MissingWebCrypto extends Error {
constructor() {
const message = "An instance of the Crypto API could not be located";
super(message);
@@ -96,7 +96,7 @@ export class MissingWebCrypto extends Error {
}
}
// Make it possible to stub return values during testing
export const _getWebCryptoInternals = {
const _getWebCryptoInternals = {
stubThisGlobalThisCrypto: () => globalThis.crypto,
// Make it possible to reset the `webCrypto` at the top of the file
setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => {

View File

@@ -117,13 +117,17 @@ export class PeerSetup {
issuerDid: string,
payload: object,
credIdHex: string,
expMinutes: number = 1,
) {
const credentialId = arrayBufferToBase64URLString(
Buffer.from(credIdHex, "hex").buffer,
);
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = {
...payload,
iat: Math.floor(Date.now() / 1000),
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
@@ -159,7 +163,8 @@ export class PeerSetup {
const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
iat: Math.floor(Date.now() / 1000),
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
const dataInJwtString = JSON.stringify(dataInJwt);
@@ -178,10 +183,14 @@ export class PeerSetup {
issuerDid: string,
payload: object,
credIdHex: string,
expMinutes: number = 1,
) {
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = {
...payload,
iat: Math.floor(Date.now() / 1000),
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
const dataToSignString = JSON.stringify(fullPayload);
@@ -227,7 +236,8 @@ export class PeerSetup {
const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
iat: Math.floor(Date.now() / 1000),
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
const dataInJwtString = JSON.stringify(dataInJwt);
@@ -308,6 +318,16 @@ export class PeerSetup {
// }
}
export async function createDidPeerJwt(
did: string,
credIdHex: string,
payload: object,
): Promise<string> {
const peerSetup = new PeerSetup();
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex);
return jwt;
}
// I'd love to use this but it doesn't verify.
// Requires:
// npm install @noble/curves
@@ -380,6 +400,7 @@ export async function verifyJwtSimplewebauthn(
return verification.verified;
}
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
export async function verifyJwtWebCrypto(
credId: Base64URLString,
issuerDid: string,

View File

@@ -2,7 +2,6 @@ import {
Axios,
AxiosRequestConfig,
AxiosResponse,
RawAxiosRequestHeaders,
} from "axios";
import * as didJwt from "did-jwt";
import { LRUCache } from "lru-cache";
@@ -13,7 +12,8 @@ import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
import { Contact } from "@/db/tables/contacts";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { NonsensitiveDexie } from "@/db/index";
import { getIdentity } from "@/libs/util";
import { createDidPeerJwt } from "@/libs/didPeer";
import { getAccount, getIdentity } from "@/libs/util";
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims
@@ -160,7 +160,7 @@ export interface OfferVerifiableCredential {
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential {
"@context": SCHEMA_ORG_CONTEXT;
"@context": "https://schema.org";
"@type": "PlanAction";
name: string;
agent?: { identifier: string };
@@ -453,28 +453,30 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
}
async function getHeaders(identity: IIdentifier | null) {
const headers: RawAxiosRequestHeaders = {
export async function getHeaders(did?: string) {
const headers: { "Content-Type": string; Authorization?: string } = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
if (did) {
const token = await accessToken(undefined, did);
headers["Authorization"] = "Bearer " + token;
} else {
// it's often OK to request without auth; we assume necessary checks are done earlier
}
return headers;
}
/**
* @param handleId nullable, in which case "undefined" will be returned
* @param identity nullable, in which case no private info will be returned
* @param requesterDid optional, in which case no private info will be returned
* @param axios
* @param apiServer
*/
export async function getPlanFromCache(
handleId: string | null,
identity: IIdentifier | null,
axios: Axios,
apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> {
if (!handleId) {
return undefined;
@@ -485,7 +487,7 @@ export async function getPlanFromCache(
apiServer +
"/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId);
const headers = await getHeaders(identity);
const headers = await getHeaders(requesterDid);
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) {
@@ -944,18 +946,34 @@ export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
};
};
export async function createEndorserJwt(did: string, payload: object) {
const account = await getAccount(did);
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: did,
signer: signer,
});
} else if (account.passkeyCredIdHex) {
return createDidPeerJwt(did, account.passkeyCredIdHex, payload);
} else {
throw new Error("No identity data found to sign for DID " + did);
}
}
export async function register(
activeDid: string,
apiServer: string,
axios: Axios,
contact: Contact,
) {
const identity = await getIdentity(activeDid);
const vcClaim: RegisterVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "RegisterAction",
agent: { identifier: identity.did },
agent: { identifier: activeDid },
object: SERVICE_ID,
participant: { identifier: contact.did },
};
@@ -968,26 +986,10 @@ export async function register(
},
};
// Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) {
return { error: "Private key not found." };
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
const vcJwt = await createEndorserJwt(activeDid, vcPayload);
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const headers = await getHeaders(identity);
const resp = await axios.post(url, payload, { headers });
const resp = await axios.post(url, { jwtEncoded: vcJwt });
if (resp.data?.success?.handleId) {
return { success: true };
} else if (resp.data?.success?.embeddedRecordError) {
@@ -1017,7 +1019,7 @@ export async function setVisibilityUtil(
const url =
apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe");
const identity = await getIdentity(activeDid);
const headers = await getHeaders(identity);
const headers = await getHeaders(identity.did);
const payload = JSON.stringify({ did: contact.did });
try {
@@ -1052,10 +1054,10 @@ export async function setVisibilityUtil(
export async function fetchEndorserRateLimits(
apiServer: string,
axios: Axios,
identity: IIdentifier,
did: string,
) {
const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(identity);
const headers = await getHeaders(did);
return await axios.get(url, { headers } as AxiosRequestConfig);
}
@@ -1070,9 +1072,9 @@ export async function fetchEndorserRateLimits(
export async function fetchImageRateLimits(
apiServer: string,
axios: Axios,
identity: IIdentifier,
did: string,
) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
const headers = await getHeaders(identity);
const headers = await getHeaders(did);
return await axios.get(url, { headers } as AxiosRequestConfig);
}

View File

@@ -196,12 +196,17 @@ export function findAllVisibleToDids(
*
**/
export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
export const getAccount = async (activeDid: string): Promise<Account> => {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
return account;
};
export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
const account = await getAccount(activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {