diff --git a/.changeset/config.json b/.changeset/config.json index b081cc5a..e6f6880c 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,7 +4,7 @@ "commit": false, "fixed": [], "linked": [], - "access": "restricted", + "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": ["integration", "website"] diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000..f0401c08 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,27 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@didtools/cacao": "2.1.0", + "@didtools/codecs": "1.0.1", + "did-session": "2.1.1", + "dids": "4.0.4", + "integration": "0.0.1", + "jest-environment-ceramic": "0.17.0", + "key-did-provider-ed25519": "3.0.2", + "@didtools/key-secp256k1": "0.2.1", + "@didtools/key-webcrypto": "0.1.1", + "key-did-resolver": "3.0.0", + "@didtools/multidid": "0.0.1", + "pkh-did-resolver": "1.2.0", + "@didtools/pkh-ethereum": "0.4.1", + "@didtools/pkh-solana": "0.1.1", + "@didtools/pkh-stacks": "0.1.0", + "@didtools/pkh-tezos": "0.2.2", + "@didtools/siwx": "1.0.0", + "website": "0.0.0" + }, + "changesets": [ + "khaki-eyes-smell" + ] +} diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 7934fa9a..e2c871c8 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -20,7 +20,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 cache: 'pnpm' - name: Install dependencies and build diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 74bdd6f7..00222874 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -24,7 +24,7 @@ jobs: - name: Use Node ${{ matrix.node }} uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} + node-version: ${{ matrix.node }} cache: 'pnpm' - name: Install dependencies and build diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index adbaa8b4..4fe3bdb7 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -20,7 +20,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 cache: 'pnpm' - name: Install dependencies and build diff --git a/packages/codecs/package.json b/packages/codecs/package.json index ad8b9f74..c4141e96 100644 --- a/packages/codecs/package.json +++ b/packages/codecs/package.json @@ -13,7 +13,7 @@ "dist" ], "engines": { - "node": ">=14.14" + "node": ">=20.8" }, "sideEffects": false, "scripts": { diff --git a/packages/did-session/CHANGELOG.md b/packages/did-session/CHANGELOG.md index e90212d2..6b759220 100644 --- a/packages/did-session/CHANGELOG.md +++ b/packages/did-session/CHANGELOG.md @@ -28,3 +28,17 @@ - key-did-resolver@4.0.0 - dids@5.0.0 - @didtools/key-webcrypto@0.2.0 +||||||| parent of 702edc4 (chore: Release threadable version) +======= +# did-session + +## 2.1.2-next.0 + +### Patch Changes + +- Use worker threads +- Updated dependencies + - dids@4.0.5-next.0 + - key-did-provider-ed25519@3.0.3-next.0 + - @didtools/key-webcrypto@0.1.1 +>>>>>>> 702edc4 (chore: Release threadable version) diff --git a/packages/dids/src/did.ts b/packages/dids/src/did.ts index 2d381324..c24b28ec 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 @@ -268,6 +268,25 @@ export class DID { return this._id } + /** + * 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') + const { jws } = await this._client.request('did_createJWS', { + did: this._id, + ...options, + payload, + }) + return jws + } + /** * Create a JWS encoded signature over the given payload. * Will be signed by the currently authenticated DID. @@ -279,29 +298,12 @@ export class DID { payload: T, options: CreateJWSOptions = {}, ): 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, - payload, + return createJWSUsing({ + capability: this._capability, + payload: payload, + options: options, + request: this.requestJWS.bind(this), }) - return jws } /** @@ -315,24 +317,12 @@ export class DID { 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}`) + const opts: VerifyJWSParameters = { ...options } + return verifyJWSUsing({ + resolve: this.resolve.bind(this), + verifyCacao: (cacao: Cacao, verifyOpts: VerifyCacaoParameters) => { + const cacaoOpts: VerifyOptions = { + verifiers: options.verifiers || verifiers, + ...verifyOpts, } - } - // 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 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, + }) } /** @@ -498,3 +424,196 @@ 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 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.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 } +} + +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 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) +} diff --git a/packages/key-did-provider-secp256k1/CHANGELOG.md b/packages/key-did-provider-secp256k1/CHANGELOG.md index 3d18f833..e4b39222 100644 --- a/packages/key-did-provider-secp256k1/CHANGELOG.md +++ b/packages/key-did-provider-secp256k1/CHANGELOG.md @@ -23,3 +23,14 @@ - Updated dependencies - dids@5.0.0 +||||||| parent of 702edc4 (chore: Release threadable version) +======= +# @didtools/key-secp256k1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - dids@4.0.5-next.0 +>>>>>>> 702edc4 (chore: Release threadable version) diff --git a/packages/pkh-solana/CHANGELOG.md b/packages/pkh-solana/CHANGELOG.md index ebe34fc1..a116035e 100644 --- a/packages/pkh-solana/CHANGELOG.md +++ b/packages/pkh-solana/CHANGELOG.md @@ -10,3 +10,14 @@ - Updated dependencies - @didtools/cacao@3.0.0 +||||||| parent of 702edc4 (chore: Release threadable version) +======= +# @didtools/pkh-solana + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @didtools/cacao@2.1.1-next.0 +>>>>>>> 702edc4 (chore: Release threadable version) diff --git a/packages/pkh-stacks/CHANGELOG.md b/packages/pkh-stacks/CHANGELOG.md index c0472569..e9f6210a 100644 --- a/packages/pkh-stacks/CHANGELOG.md +++ b/packages/pkh-stacks/CHANGELOG.md @@ -10,3 +10,14 @@ - Updated dependencies - @didtools/cacao@3.0.0 +||||||| parent of 702edc4 (chore: Release threadable version) +======= +# @didtools/pkh-stacks + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @didtools/cacao@2.1.1-next.0 +>>>>>>> 702edc4 (chore: Release threadable version) diff --git a/packages/pkh-stacks/package.json b/packages/pkh-stacks/package.json index 78e3c8d9..2a67617e 100644 --- a/packages/pkh-stacks/package.json +++ b/packages/pkh-stacks/package.json @@ -46,7 +46,7 @@ "typescript": "^5.3.2" }, "dependencies": { - "@didtools/cacao": "workspace:^3.0.0", + "@didtools/cacao": "workspace:^", "@stablelib/random": "^1.0.2", "@stacks/common": "^6.10.0", "@stacks/encryption": "^6.10.0", diff --git a/packages/pkh-tezos/CHANGELOG.md b/packages/pkh-tezos/CHANGELOG.md index 452b5e5e..a93e2371 100644 --- a/packages/pkh-tezos/CHANGELOG.md +++ b/packages/pkh-tezos/CHANGELOG.md @@ -10,3 +10,14 @@ - Updated dependencies - @didtools/cacao@3.0.0 +||||||| parent of 702edc4 (chore: Release threadable version) +======= +# @didtools/pkh-tezos + +## 0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @didtools/cacao@2.1.1-next.0 +>>>>>>> 702edc4 (chore: Release threadable version) diff --git a/packages/pkh-tezos/package.json b/packages/pkh-tezos/package.json index 3a6c7d68..718a8c74 100644 --- a/packages/pkh-tezos/package.json +++ b/packages/pkh-tezos/package.json @@ -49,7 +49,7 @@ "typescript": "^5.3.2" }, "dependencies": { - "@didtools/cacao": "workspace:^3.0.0", + "@didtools/cacao": "workspace:^", "@noble/curves": "^1.2.0", "@noble/hashes": "^1.3.2", "@stablelib/random": "^1.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20fc88ca..6217a058 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -496,7 +496,7 @@ importers: packages/pkh-stacks: dependencies: '@didtools/cacao': - specifier: workspace:^3.0.0 + specifier: workspace:^ version: link:../cacao '@stablelib/random': specifier: ^1.0.2 @@ -524,7 +524,7 @@ importers: packages/pkh-tezos: dependencies: '@didtools/cacao': - specifier: workspace:^3.0.0 + specifier: workspace:^ version: link:../cacao '@noble/curves': specifier: ^1.2.0