From 37824b449406e4db54ad5966d128d504ccc90e32 Mon Sep 17 00:00:00 2001 From: Danny Browning Date: Mon, 29 Jan 2024 08:19:07 -0700 Subject: [PATCH] feat: Use threads for jws verification Exposes necessary functionality to use threads for jws verification. --- packages/dids/src/did.ts | 368 ++++++++++++++++++++++++++------------- 1 file changed, 244 insertions(+), 124 deletions(-) diff --git a/packages/dids/src/did.ts b/packages/dids/src/did.ts index 2d38132..e2760d9 100644 --- a/packages/dids/src/did.ts +++ b/packages/dids/src/did.ts @@ -3,9 +3,9 @@ import { createJWE, JWE, verifyJWS, resolveX25519Encrypters } from 'did-jwt' import { encodePayload, prepareCleartext, decodeCleartext } from 'dag-jose-utils' import { RPCClient } from 'rpc-utils' import { CID } from 'multiformats/cid' -import { CacaoBlock, Cacao, Verifiers } from '@didtools/cacao' +import { CacaoBlock, Cacao, Verifiers, VerifyOptions } from '@didtools/cacao' import { getEIP191Verifier } from '@didtools/pkh-ethereum' -import type { DagJWS } from '@didtools/codecs' +import type { DagJWS, GeneralJWS } from '@didtools/codecs' import type { DIDProvider, DIDProviderClient } from './types.js' import { fromDagJWS, @@ -19,7 +19,7 @@ import { } from './utils.js' // Eth Verifier default for CACAO -const verifiers = { ...getEIP191Verifier() } +export const verifiers = { ...getEIP191Verifier() } export type AuthenticateOptions = { provider?: DIDProvider @@ -138,7 +138,7 @@ export class DID { if (capability) { this._capability = capability this._parentId = this._capability.p.iss - if (this._parentId.startsWith('did:pkh:eip155:1:')) { + if (this._parentId?.startsWith('did:pkh:eip155:1:')) { // Lower case ethereum address for compatibility with Ceramic this._parentId = this._parentId.toLowerCase() } @@ -227,7 +227,7 @@ export class DID { this._client = new RPCClient(provider) } else if (this._client.connection !== provider) { throw new Error( - 'A different provider is already set, create a new DID instance to use another provider', + 'A different provider is already set, create a new DID instance to use another provider', ) } } @@ -276,26 +276,28 @@ export class DID { * @param options Optional parameters */ async createJWS>( - payload: T, - options: CreateJWSOptions = {}, + payload: T, + options: CreateJWSOptions = {}, ): Promise { + return createJWSUsing({ + capability: this._capability, + payload: payload, + options: options, + request: this.requestJWS.bind(this), + }) + } + + /** + * Request a JWS from this did's client + * @param options + * @param payload + */ + async requestJWS>( + options: CreateJWSOptions, + payload: T + ): Promise { if (this._client == null) throw new Error('No provider available') if (this._id == null) throw new Error('DID is not authenticated') - if (this._capability) { - const exp = this._capability.p.exp - if (exp && Date.parse(exp) < Date.now()) { - throw new Error('Capability is expired, cannot create a valid signature') - } - const cacaoBlock = await CacaoBlock.fromCacao(this._capability) - const capCID = CID.asCID(cacaoBlock.cid) - if (!capCID) { - throw new Error( - `Capability CID of the JWS cannot be set to the capability payload cid as they are incompatible`, - ) - } - options.protected = options.protected || {} - options.protected.cap = `ipfs://${capCID?.toString()}` - } const { jws } = await this._client.request('did_createJWS', { did: this._id, ...options, @@ -312,27 +314,15 @@ export class DID { * @param options Optional parameters */ async createDagJWS( - payload: Record, - options: CreateJWSOptions = {}, + payload: Record, + options: CreateJWSOptions = {}, ): Promise { - const { cid, linkedBlock } = await encodePayload(payload) - const payloadCid = encodeBase64Url(cid.bytes) - Object.assign(options, { linkedBlock: encodeBase64(linkedBlock) }) - const jws = await this.createJWS(payloadCid, options) - - const compatibleCID = CID.asCID(cid) - if (!compatibleCID) { - throw new Error( - 'CID of the JWS cannot be set to the encoded payload cid as they are incompatible', - ) - } - jws.link = compatibleCID - - if (this._capability) { - const cacaoBlock = await CacaoBlock.fromCacao(this._capability) - return { jws, linkedBlock, cacaoBlock: cacaoBlock.bytes } - } - return { jws, linkedBlock } + return createDagJWSUsing({ + capability: this._capability, + payload: payload, + options: options, + request: this.requestJWS.bind(this), + }) } /** @@ -344,83 +334,19 @@ export class DID { * @returns Information about the signed JWS */ async verifyJWS(jws: string | DagJWS, options: VerifyJWSOptions = {}): Promise { - options = Object.assign({ verifiers }, options) - if (typeof jws !== 'string') jws = fromDagJWS(jws) - const kid = base64urlToJSON(jws.split('.')[0]).kid as string - if (!kid) throw new Error('No "kid" found in jws') - const didResolutionResult = await this.resolve(kid) - const timecheckEnabled = !options.disableTimecheck - if (timecheckEnabled) { - const nextUpdate = didResolutionResult.didDocumentMetadata?.nextUpdate - if (nextUpdate) { - // This version of the DID document has been revoked. Check if the JWS - // was signed before the revocation happened. - const phaseOutMS = options.revocationPhaseOutSecs - ? options.revocationPhaseOutSecs * 1000 - : 0 - const revocationTime = new Date(nextUpdate).valueOf() + phaseOutMS - const isEarlier = options.atTime && options.atTime.getTime() < revocationTime - const isLater = !isEarlier - if (isLater) { - // Do not allow using a key _after_ it is being revoked - throw new Error(`invalid_jws: signature authored with a revoked DID version: ${kid}`) - } - } - // Key used before `updated` date - const updated = didResolutionResult.didDocumentMetadata?.updated - if (updated && options.atTime && options.atTime.getTime() < new Date(updated).valueOf()) { - throw new Error(`invalid_jws: signature authored before creation of DID version: ${kid}`) - } - } - - const signerDid = didResolutionResult.didDocument?.id - if ( - options.issuer && - options.capability && - issuerEquals(options.issuer, options.capability?.p.iss) && - signerDid === options.capability.p.aud - ) { - if (!options.verifiers) throw new Error('Registered verifiers needed for CACAO') - await Cacao.verify(options.capability, { - disableExpirationCheck: options.disableTimecheck, - atTime: options.atTime ? options.atTime : undefined, - revocationPhaseOutSecs: options.revocationPhaseOutSecs, - verifiers: options.verifiers ?? {}, - }) - } else if (options.issuer && options.issuer !== signerDid) { - const issuerUrl = didWithTime(options.issuer, options.atTime) - const issuerResolution = await this.resolve(issuerUrl) - const controllerProperty = issuerResolution.didDocument?.controller - const controllers = extractControllers(controllerProperty) - - if ( - options.capability?.s && - options.capability.p.aud === signerDid && - controllers.includes(options.capability.p.iss) - ) { - await Cacao.verify(options.capability, { - atTime: options.atTime ? options.atTime : undefined, - revocationPhaseOutSecs: options.revocationPhaseOutSecs, - verifiers: options.verifiers ?? {}, - }) - } else { - const signerIsController = signerDid ? controllers.includes(signerDid) : false - if (!signerIsController) { - throw new Error(`invalid_jws: not a valid verificationMethod for issuer: ${kid}`) + const opts: VerifyJWSParameters = { ...options } + return verifyJWSUsing({ + resolve: this.resolve.bind(this), + verifyCacao: (cacao: Cacao, verifyOpts: VerifyCacaoParameters) => { + const cacaoOpts: VerifyOptions = { + verifiers: options.verifiers || verifiers, + ...verifyOpts, } - } - } - - const publicKeys = didResolutionResult.didDocument?.verificationMethod || [] - // verifyJWS will throw an error if the signature is invalid - verifyJWS(jws, publicKeys) - let payload - try { - payload = base64urlToJSON(jws.split('.')[1]) - } catch (e) { - // If an error is thrown it means that the payload is a CID. - } - return { kid, payload, didResolutionResult } + return Cacao.verify(cacao, cacaoOpts) + }, + jws: jws, + options: opts, + }) } /** @@ -431,9 +357,9 @@ export class DID { * @param options Optional parameters */ async createJWE( - cleartext: Uint8Array, - recipients: Array, - options: CreateJWEOptions = {}, + cleartext: Uint8Array, + recipients: Array, + options: CreateJWEOptions = {}, ): Promise { const encrypters = await resolveX25519Encrypters(recipients, this._resolver) return createJWE(cleartext, encrypters, options.protectedHeader, options.aad) @@ -447,9 +373,9 @@ export class DID { * @param options Optional parameters */ async createDagJWE( - cleartext: Record, - recipients: Array, - options: CreateJWEOptions = {}, + cleartext: Record, + recipients: Array, + options: CreateJWEOptions = {}, ): Promise { const preparedCleartext = await prepareCleartext(cleartext) return this.createJWE(preparedCleartext, recipients, options) @@ -498,3 +424,197 @@ export class DID { return result } } + +export type VerifyJWSParameters = { + /** + * JS timestamp when the signature was allegedly made. `undefined` means _now_. + */ + atTime?: Date + + /** + * If true, timestamp checking is disabled. + */ + disableTimecheck?: boolean + + /** + * DID that issued the signature. + */ + issuer?: string + + /** + * Cacao OCAP to verify the JWS with. + */ + capability?: Cacao + + /** + * Number of seconds that a revoked key stays valid for after it was revoked + */ + revocationPhaseOutSecs?: number +} + +export type VerifyCacaoParameters = { + /** + * @param atTime - the point in time the capability is being verified for + */ + atTime?: Date + /** + * @param expPhaseOutSecs - Number of seconds that a capability stays valid for after it was expired + */ + revocationPhaseOutSecs?: number + /** + * @param clockSkewSecs - Number of seconds of clock tolerance when verifying iat, nbf, and exp + */ + clockSkewSecs?: number + + /** + * @param disableExpirationCheck - Do not verify expiration time + */ + disableExpirationCheck?: boolean +} + +export type CreateJWSUsingParameters> = { + capability?: Cacao + payload: T + options: CreateJWSOptions + request: (options: CreateJWSOptions, payload: T) => Promise +} + +export async function createJWSUsing>( + params: CreateJWSUsingParameters +): Promise { + if (params.capability) { + const exp = params.capability.p.exp + if (exp && Date.parse(exp) < Date.now()) { + throw new Error('Capability is expired, cannot create a valid signature') + } + const cacaoBlock = await CacaoBlock.fromCacao(params.capability) + const capCID = CID.asCID(cacaoBlock.cid) + if (!capCID) { + throw new Error( + `Capability CID of the JWS cannot be set to the capability payload cid as they are incompatible` + ) + } + params.options.protected = params.options.protected || {} + params.options.protected.cap = `ipfs://${capCID?.toString()}` + } + return await params.request(params.options, params.payload) +} + +export type CreateDagJWSUsingParameters = { + capability?: Cacao + payload: Record + options: CreateJWSOptions + request: (options: CreateJWSOptions, payload: string) => Promise +} + +export async function createDagJWSUsing( + params: CreateDagJWSUsingParameters +): Promise { + const { cid, linkedBlock } = await encodePayload(params.payload) + const payloadCid = encodeBase64Url(cid.bytes) + Object.assign(params.options, { linkedBlock: encodeBase64(linkedBlock) }) + const jws = await createJWSUsing({ + capability: params.capability, + payload: payloadCid, + options: params.options, + request: params.request, + }) + + const compatibleCID = CID.asCID(cid) + if (!compatibleCID) { + throw new Error( + 'CID of the JWS cannot be set to the encoded payload cid as they are incompatible' + ) + } + jws.link = compatibleCID + + if (params.capability) { + const cacaoBlock = await CacaoBlock.fromCacao(params.capability) + return { jws, linkedBlock, cacaoBlock: cacaoBlock.bytes } + } + return { jws, linkedBlock } +} + +export type VerifyJWSUsingParameters = { + resolve: (url: string) => Promise + verifyCacao: (cacao: Cacao, opts: VerifyCacaoParameters) => Promise + jws: string | DagJWS + options: VerifyJWSParameters +} + +export async function verifyJWSUsing(params: VerifyJWSUsingParameters): Promise { + let jws = params.jws + const options = params.options || {} + if (typeof jws !== 'string') jws = fromDagJWS(jws) + const kid = base64urlToJSON(jws.split('.')[0]).kid as string + if (!kid) throw new Error('No "kid" found in jws') + const didResolutionResult = await params.resolve(kid) + const timecheckEnabled = !options.disableTimecheck + if (timecheckEnabled) { + const nextUpdate = didResolutionResult.didDocumentMetadata?.nextUpdate + if (nextUpdate) { + // This version of the DID document has been revoked. Check if the JWS + // was signed before the revocation happened. + const phaseOutMS = options.revocationPhaseOutSecs ? options.revocationPhaseOutSecs * 1000 : 0 + const revocationTime = new Date(nextUpdate).valueOf() + phaseOutMS + const isEarlier = options.atTime && options.atTime.getTime() < revocationTime + const isLater = !isEarlier + if (isLater) { + // Do not allow using a key _after_ it is being revoked + throw new Error(`invalid_jws: signature authored with a revoked DID version: ${kid}`) + } + } + // Key used before `updated` date + const updated = didResolutionResult.didDocumentMetadata?.updated + if (updated && options.atTime && options.atTime.getTime() < new Date(updated).valueOf()) { + throw new Error(`invalid_jws: signature authored before creation of DID version: ${kid}`) + } + } + + const signerDid = didResolutionResult.didDocument?.id + if ( + options.issuer && + options.capability && + issuerEquals(options.issuer, options.capability?.p.iss) && + signerDid === options.capability.p.aud + ) { + await params.verifyCacao(options.capability, { + disableExpirationCheck: options.disableTimecheck, + atTime: options.atTime ? options.atTime : undefined, + revocationPhaseOutSecs: options.revocationPhaseOutSecs, + }) + } else if (options.issuer && options.issuer !== signerDid) { + const issuerUrl = didWithTime(options.issuer, options.atTime) + const issuerResolution = await params.resolve(issuerUrl) + const controllerProperty = issuerResolution.didDocument?.controller + const controllers = extractControllers(controllerProperty) + + if ( + options.issuer && + options.capability && + issuerEquals(options.issuer, options.capability?.p.iss) && + signerDid === options.capability.p.aud + ) { + await params.verifyCacao(options.capability, { + atTime: options.atTime ? options.atTime : undefined, + revocationPhaseOutSecs: options.revocationPhaseOutSecs, + }) + } else { + const signerIsController = signerDid ? controllers.includes(signerDid) : false + if (!signerIsController) { + throw new Error(`invalid_jws: not a valid verificationMethod for issuer: ${kid}`) + } + } + } + + const publicKeys = didResolutionResult.didDocument?.verificationMethod || [] + // verifyJWS will throw an error if the signature is invalid + verifyJWS(jws, publicKeys) + let payload + try { + payload = base64urlToJSON(jws.split('.')[1]) + } catch (e) { + // If an error is thrown it means that the payload is a CID. + } + return { kid, payload, didResolutionResult } +} \ No newline at end of file