diff --git a/.changeset/dull-jokes-teach.md b/.changeset/dull-jokes-teach.md new file mode 100644 index 00000000..47616918 --- /dev/null +++ b/.changeset/dull-jokes-teach.md @@ -0,0 +1,9 @@ +--- +"@didtools/key-secp256k1": minor +"key-did-provider-ed25519": minor +"did-session": minor +"dids": minor +"jest-environment-ceramic": minor +--- + +Expose DIDs functionality to allow threaded signing and verification diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000..cf9b6847 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,28 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@didtools/cacao": "3.0.1", + "@didtools/codecs": "3.0.0", + "did-session": "3.0.2", + "dids": "5.0.2", + "integration": "0.0.1", + "jest-environment-ceramic": "0.18.0", + "key-did-provider-ed25519": "4.0.2", + "@didtools/key-secp256k1": "0.3.2", + "@didtools/key-webcrypto": "0.2.0", + "key-did-resolver": "4.0.0", + "@didtools/key-webauthn": "2.0.1", + "@didtools/multidid": "0.1.0", + "pkh-did-resolver": "2.0.0", + "@didtools/pkh-ethereum": "0.5.0", + "@didtools/pkh-solana": "0.2.0", + "@didtools/pkh-stacks": "0.2.0", + "@didtools/pkh-tezos": "0.3.0", + "@didtools/siwx": "2.0.0", + "website": "0.0.0" + }, + "changesets": [ + "dull-jokes-teach" + ] +} diff --git a/packages/did-session/CHANGELOG.md b/packages/did-session/CHANGELOG.md index e90212d2..b2794fa7 100644 --- a/packages/did-session/CHANGELOG.md +++ b/packages/did-session/CHANGELOG.md @@ -1,5 +1,18 @@ # did-session +## 3.1.0-next.0 + +### Minor Changes + +- Expose DIDs functionality to allow threaded signing and verification + +### Patch Changes + +- Updated dependencies + - key-did-provider-ed25519@4.1.0-next.0 + - dids@5.1.0-next.0 + - @didtools/key-webcrypto@0.2.0 + ## 3.0.2 ### Patch Changes diff --git a/packages/did-session/package.json b/packages/did-session/package.json index 4458b1ad..e9fb5032 100644 --- a/packages/did-session/package.json +++ b/packages/did-session/package.json @@ -1,6 +1,6 @@ { "name": "did-session", - "version": "3.0.2", + "version": "3.1.0-next.0", "description": "Manage user DIDs in a web environment", "author": "3Box Labs", "license": "(Apache-2.0 OR MIT)", diff --git a/packages/dids/CHANGELOG.md b/packages/dids/CHANGELOG.md index 97547b29..c06e037a 100644 --- a/packages/dids/CHANGELOG.md +++ b/packages/dids/CHANGELOG.md @@ -1,5 +1,11 @@ ## v2.4.0 (2021-07-12) +## 5.1.0-next.0 + +### Minor Changes + +- Expose DIDs functionality to allow threaded signing and verification + ## 5.0.2 ### Patch Changes diff --git a/packages/dids/package.json b/packages/dids/package.json index 65ca7ea3..94aa63d6 100644 --- a/packages/dids/package.json +++ b/packages/dids/package.json @@ -1,6 +1,6 @@ { "name": "dids", - "version": "5.0.2", + "version": "5.1.0-next.0", "description": "Typescript library for interacting with DIDs", "author": "Joel Thorstensson ", "license": "(Apache-2.0 OR MIT)", diff --git a/packages/dids/src/did.ts b/packages/dids/src/did.ts index 2d381324..e2760d97 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 diff --git a/packages/jest-environment-ceramic/CHANGELOG.md b/packages/jest-environment-ceramic/CHANGELOG.md index 1ed4b326..234cf1de 100644 --- a/packages/jest-environment-ceramic/CHANGELOG.md +++ b/packages/jest-environment-ceramic/CHANGELOG.md @@ -1,5 +1,11 @@ # jest-environment-ceramic +## 0.19.0-next.0 + +### Minor Changes + +- Expose DIDs functionality to allow threaded signing and verification + ## 0.18.0 ### Minor Changes diff --git a/packages/jest-environment-ceramic/package.json b/packages/jest-environment-ceramic/package.json index 0d72a689..bddbe21b 100644 --- a/packages/jest-environment-ceramic/package.json +++ b/packages/jest-environment-ceramic/package.json @@ -1,6 +1,6 @@ { "name": "jest-environment-ceramic", - "version": "0.18.0", + "version": "0.19.0-next.0", "description": "Ceramic environment for Jest", "author": "3Box Labs", "license": "(Apache-2.0 OR MIT)", diff --git a/packages/key-did-provider-ed25519/CHANGELOG.md b/packages/key-did-provider-ed25519/CHANGELOG.md index faee1088..d119fd47 100644 --- a/packages/key-did-provider-ed25519/CHANGELOG.md +++ b/packages/key-did-provider-ed25519/CHANGELOG.md @@ -1,5 +1,16 @@ # key-did-provider-ed25519 +## 4.1.0-next.0 + +### Minor Changes + +- Expose DIDs functionality to allow threaded signing and verification + +### Patch Changes + +- Updated dependencies + - dids@5.1.0-next.0 + ## 4.0.2 ### Patch Changes diff --git a/packages/key-did-provider-ed25519/package.json b/packages/key-did-provider-ed25519/package.json index de6aedf1..694dceef 100644 --- a/packages/key-did-provider-ed25519/package.json +++ b/packages/key-did-provider-ed25519/package.json @@ -1,6 +1,6 @@ { "name": "key-did-provider-ed25519", - "version": "4.0.2", + "version": "4.1.0-next.0", "author": "Joel Thorstensson", "license": "(Apache-2.0 OR MIT)", "type": "module", diff --git a/packages/key-did-provider-secp256k1/CHANGELOG.md b/packages/key-did-provider-secp256k1/CHANGELOG.md index 3d18f833..e77ab01e 100644 --- a/packages/key-did-provider-secp256k1/CHANGELOG.md +++ b/packages/key-did-provider-secp256k1/CHANGELOG.md @@ -1,5 +1,16 @@ # @didtools/key-secp256k1 +## 0.4.0-next.0 + +### Minor Changes + +- Expose DIDs functionality to allow threaded signing and verification + +### Patch Changes + +- Updated dependencies + - dids@5.1.0-next.0 + ## 0.3.2 ### Patch Changes diff --git a/packages/key-did-provider-secp256k1/package.json b/packages/key-did-provider-secp256k1/package.json index f609649e..4214caf7 100644 --- a/packages/key-did-provider-secp256k1/package.json +++ b/packages/key-did-provider-secp256k1/package.json @@ -1,6 +1,6 @@ { "name": "@didtools/key-secp256k1", - "version": "0.3.2", + "version": "0.4.0-next.0", "author": "Joel Thorstensson", "license": "(Apache-2.0 OR MIT)", "type": "module", diff --git a/packages/key-did-provider-webcrypto/package.json b/packages/key-did-provider-webcrypto/package.json index 09c0f05c..299bb4aa 100644 --- a/packages/key-did-provider-webcrypto/package.json +++ b/packages/key-did-provider-webcrypto/package.json @@ -46,7 +46,7 @@ "devDependencies": { "@types/varint": "^6.0.3", "did-jwt": "^7.4.5", - "dids": "workspace:^5.0.0", + "dids": "workspace:^5.1.0-next.0", "jest-environment-jsdom": "^29.7.0" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20fc88ca..bc30034b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -352,7 +352,7 @@ importers: specifier: ^7.4.5 version: 7.4.5 dids: - specifier: workspace:^5.0.0 + specifier: workspace:^5.1.0-next.0 version: link:../dids jest-environment-jsdom: specifier: ^29.7.0