diff --git a/.nxignore b/.nxignore new file mode 100644 index 00000000..d01d4b12 --- /dev/null +++ b/.nxignore @@ -0,0 +1 @@ +apps/w3c-cli \ No newline at end of file diff --git a/apps/w3c-cli/README.md b/apps/w3c-cli/README.md index 6fc82ec6..ae75da31 100644 --- a/apps/w3c-cli/README.md +++ b/apps/w3c-cli/README.md @@ -1,5 +1,13 @@ # TrustVC W3C CLI +> **⚠️ DEPRECATED PACKAGE** +> +> This CLI package has been deprecated and is no longer maintained. The CLI was built using legacy BBS+ cryptosuites that have been superseded by modern W3C Data Integrity cryptosuites (ECDSA-SD-2023, BBS-2023). + +--- + +## Legacy Documentation + `w3c-cli` is a command-line interface tool designed to demonstrate how to interact with Verifiable Credentials (VCs) and Decentralized Identifiers (DIDs) using functions from the following repositories: - [`w3c-issuer`](https://github.com/TrustVC/w3c/tree/main/packages/w3c-issuer) diff --git a/apps/w3c-cli/package.json b/apps/w3c-cli/package.json index c82b54e4..3c82dce9 100644 --- a/apps/w3c-cli/package.json +++ b/apps/w3c-cli/package.json @@ -1,7 +1,8 @@ { "name": "@trustvc/w3c-cli", - "version": "1.2.18", - "description": "CLI for TrustVC W3C", + "version": "1.3.0-alpha.12", + "description": "⚠️ DEPRECATED: CLI for TrustVC W3C - This package is no longer maintained.", + "deprecated": "This CLI package is deprecated due to incompatibility with modern cryptosuites.", "main": "dist/main.js", "types": "dist/main.d.ts", "bin": { @@ -34,10 +35,10 @@ }, "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/w3c-context": "^1.2.13", - "@trustvc/w3c-credential-status": "^1.2.13", - "@trustvc/w3c-vc": "^1.2.17", - "@trustvc/w3c-issuer": "^1.2.4", + "@trustvc/w3c-context": "^1.3.0-alpha.11", + "@trustvc/w3c-credential-status": "^1.3.0-alpha.11", + "@trustvc/w3c-vc": "^1.3.0-alpha.12", + "@trustvc/w3c-issuer": "^1.3.0-alpha.9", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "inquirer": "^10.0.1", diff --git a/apps/w3c-cli/project.json b/apps/w3c-cli/project.json.deprecated similarity index 64% rename from apps/w3c-cli/project.json rename to apps/w3c-cli/project.json.deprecated index 06213e99..9ea5e32e 100644 --- a/apps/w3c-cli/project.json +++ b/apps/w3c-cli/project.json.deprecated @@ -1,3 +1,7 @@ +# DEPRECATED CLI PROJECT CONFIGURATION +# This file has been renamed from project.json to exclude the deprecated CLI from NX builds and tests +# The @trustvc/w3c-cli package is deprecated due to incompatibility with modern cryptosuites + { "name": "@trustvc/w3c-cli", "$schema": "../../node_modules/nx/schemas/project-schema.json", diff --git a/apps/w3c-cli/tests/main.test.ts b/apps/w3c-cli/tests/main.test.ts index bf61fbe7..f7bc21e4 100644 --- a/apps/w3c-cli/tests/main.test.ts +++ b/apps/w3c-cli/tests/main.test.ts @@ -291,6 +291,39 @@ describe('w3c-cli', () => { true, ); + // Mock the issueDID function to return expected structure + issueDIDSpy.mockResolvedValue({ + wellKnownDid: { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/bls12381-2020/v1', + ], + assertionMethod: ['did:web:example.com#keys-1'], + authentication: ['did:web:example.com#keys-1'], + capabilityDelegation: ['did:web:example.com#keys-1'], + capabilityInvocation: ['did:web:example.com#keys-1'], + id: 'did:web:example.com', + verificationMethod: [ + { + controller: 'did:web:example.com', + id: 'did:web:example.com#keys-1', + publicKeyBase58: + 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ', + type: w3cIssuer.VerificationType.Bls12381G2Key2020, + }, + ], + }, + didKeyPairs: { + controller: 'did:web:example.com', + id: 'did:web:example.com#keys-1', + privateKeyBase58: '4LDU56PUhA9ZEutnR1qCWQnUhtLtpLu2EHSq4h1o7vtF', + publicKeyBase58: + 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ', + seedBase58: 'GWP69tmSWJjqC1RoJ27FehcVqkVyeYAz6h5ABwoNSNdS', + type: w3cIssuer.VerificationType.Bls12381G2Key2020, + }, + }); + await didHandler(); expect(issueDIDSpy).toHaveBeenCalledWith( @@ -312,7 +345,7 @@ describe('w3c-cli', () => { id: 'did:web:example.com#keys-1', publicKeyBase58: 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ', - type: 'Bls12381G2Key2020', + type: w3cIssuer.VerificationType.Bls12381G2Key2020, }, ], }); @@ -323,7 +356,7 @@ describe('w3c-cli', () => { publicKeyBase58: 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ', seedBase58: 'GWP69tmSWJjqC1RoJ27FehcVqkVyeYAz6h5ABwoNSNdS', - type: 'Bls12381G2Key2020', + type: w3cIssuer.VerificationType.Bls12381G2Key2020, }); }); diff --git a/nx.json b/nx.json index 789be7fb..df43802a 100644 --- a/nx.json +++ b/nx.json @@ -50,8 +50,7 @@ "@trustvc/w3c-context", "@trustvc/w3c-credential-status", "@trustvc/w3c-issuer", - "@trustvc/w3c-vc", - "@trustvc/w3c-cli" + "@trustvc/w3c-vc" ], "changelog": { "projectChangelogs": { diff --git a/package-lock.json b/package-lock.json index e781c678..f30c7a93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "express": "^4.19.2", "inquirer": "^10.1.2", "jsonld": "^6.0.0", - "jsonld-signatures": "7.0.0", "multiformats": "^9.9.0", "pako": "^2.1.0", "tslib": "^2.3.0", @@ -116,14 +115,15 @@ }, "apps/w3c-cli": { "name": "@trustvc/w3c-cli", - "version": "1.2.17", + "version": "1.3.0-alpha.12", + "deprecated": "This CLI package is deprecated due to incompatibility with modern cryptosuites.", "license": "Apache-2.0", "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/w3c-context": "^1.2.12", - "@trustvc/w3c-credential-status": "^1.2.12", - "@trustvc/w3c-issuer": "^1.2.3", - "@trustvc/w3c-vc": "^1.2.16", + "@trustvc/w3c-context": "^1.3.0-alpha.11", + "@trustvc/w3c-credential-status": "^1.3.0-alpha.11", + "@trustvc/w3c-issuer": "^1.3.0-alpha.9", + "@trustvc/w3c-vc": "^1.3.0-alpha.12", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "inquirer": "^10.0.1", @@ -136,6 +136,134 @@ "node": ">=18.x" } }, + "apps/w3c-cli/node_modules/@trustvc/w3c-vc": { + "version": "1.3.0-alpha.17", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-vc/-/w3c-vc-1.3.0-alpha.17.tgz", + "integrity": "sha512-aQXiu8xQ3fIN262h1KCoWkWRH0AeeTzN/68NbDqv3DdbA/iAnQoerwoSd//7oFoithr0yySCbcViulqlMZ8kHg==", + "license": "Apache-2.0", + "dependencies": { + "@digitalbazaar/bbs-2023-cryptosuite": "^2.0.1", + "@digitalbazaar/bls12-381-multikey": "^2.1.0", + "@digitalbazaar/data-integrity": "^2.5.0", + "@digitalbazaar/ecdsa-multikey": "^1.8.0", + "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.4.1", + "@mattrglobal/jsonld-signatures-bbs": "^1.2.0", + "@trustvc/w3c-credential-status": "^1.3.0-alpha.13", + "@trustvc/w3c-issuer": "^1.3.0-alpha.10", + "base64url-universal": "^2.0.0", + "cbor": "^9.0.2", + "did-resolver": "^4.1.0", + "jsonld": "^6.0.0", + "jsonld-signatures": "^11.5.0", + "jsonld-signatures-v7": "npm:jsonld-signatures@7.0.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "jsonld": "^6.0.0" + } + }, + "apps/w3c-cli/node_modules/jsonld-signatures": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-11.5.0.tgz", + "integrity": "sha512-Kdto+e8uvY/5u3HYkmAbpy52bplWX9uqS8fmqdCv6oxnCFwCTM0hMt6r4rWqlhw5/aHoCHJIRxwYb4QKGC69Jw==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/security-context": "^1.0.0", + "jsonld": "^8.0.0", + "rdf-canonize": "^4.0.1", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "apps/w3c-cli/node_modules/jsonld-signatures/node_modules/jsonld": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-8.3.3.tgz", + "integrity": "sha512-9YcilrF+dLfg9NTEof/mJLMtbdX1RJ8dbWtJgE00cMOIohb1lIyJl710vFiTaiHTl6ZYODJuBd32xFvUhmv3kg==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/http-client": "^3.4.1", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "apps/w3c-cli/node_modules/jsonld-signatures/node_modules/jsonld/node_modules/rdf-canonize": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", + "integrity": "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=12" + } + }, + "apps/w3c-cli/node_modules/jsonld-signatures/node_modules/rdf-canonize": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-4.0.1.tgz", + "integrity": "sha512-B5ynHt4sasbUafzrvYI2GFARgeFcD8Sx9yXPbg7gEyT2EH76rlCv84kyO6tnxzVbxUN/uJDbK1S/MXh+DsnuTA==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "apps/w3c-cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "apps/w3c-cli/node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/w3c-cli/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/w3c-cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/@adraffy/ens-normalize": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", @@ -2749,6 +2877,45 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@digitalbazaar/bbs-2023-cryptosuite": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@digitalbazaar/bbs-2023-cryptosuite/-/bbs-2023-cryptosuite-2.0.1.tgz", + "integrity": "sha512-Uw7aDSuCehLUsiSsTi2ob1hQ8AgVq+jV3OgvPsOzy/AmIh2yG9kg2tNCv6PngWPyOm3VCzolwh+5MzWySASiww==", + "dependencies": { + "@digitalbazaar/bls12-381-multikey": "^2.0.0", + "@digitalbazaar/di-sd-primitives": "^3.0.4", + "base64url-universal": "^2.0.0", + "cborg": "^4.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/bbs-signatures": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/bbs-signatures/-/bbs-signatures-3.0.0.tgz", + "integrity": "sha512-mQMCMnCWAraVSswJg1kJK/qmUrb3jMoWB9c8kOmztsWfnMZJcyYAcavuF8jgrVZ5cl/ZRNMK61ZbIvkqd6BE6g==", + "dependencies": { + "@noble/curves": "^1.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/bbs-signatures/node_modules/@noble/curves": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz", + "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@digitalbazaar/bitstring": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@digitalbazaar/bitstring/-/bitstring-3.1.0.tgz", @@ -2761,11 +2928,157 @@ "node": ">=16" } }, + "node_modules/@digitalbazaar/bls12-381-multikey": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/bls12-381-multikey/-/bls12-381-multikey-2.1.0.tgz", + "integrity": "sha512-JelU85fNhvHl2/mqRdmrtrE2ZQJ0//+UwI0l/YFmvsOr6YN2GuKPzdkfXjpm7f3UvnBqz5f8QKFTb9mVa7mVVg==", + "dependencies": { + "@digitalbazaar/bbs-signatures": "^3.0.0", + "@noble/curves": "^1.3.0", + "base58-universal": "^2.0.0", + "base64url-universal": "^2.0.0", + "cborg": "^4.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/bls12-381-multikey/node_modules/@noble/curves": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz", + "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@digitalbazaar/credentials-context": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@digitalbazaar/credentials-context/-/credentials-context-3.1.0.tgz", "integrity": "sha512-VKflyFbcid6xH3FJMrd8U9DeCjbYXa2aO3l8yy7MzP6O/0v0EZTV56hMfJT5fN6TSVtQmShv2R3oPCFqFbc2Tg==" }, + "node_modules/@digitalbazaar/data-integrity": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/data-integrity/-/data-integrity-2.5.0.tgz", + "integrity": "sha512-ohIieLfgtPQU9BYfj0eKNiz55/ZDOk5YSE9FN/Hn0eXzI8WQzLkzRvC8pvBnzuzXDgCsjPdSqYvzok5PoClMBQ==", + "dependencies": { + "base58-universal": "^2.0.0", + "base64url-universal": "^2.0.0", + "jsonld-signatures": "^11.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/data-integrity/node_modules/jsonld-signatures": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-11.5.0.tgz", + "integrity": "sha512-Kdto+e8uvY/5u3HYkmAbpy52bplWX9uqS8fmqdCv6oxnCFwCTM0hMt6r4rWqlhw5/aHoCHJIRxwYb4QKGC69Jw==", + "dependencies": { + "@digitalbazaar/security-context": "^1.0.0", + "jsonld": "^8.0.0", + "rdf-canonize": "^4.0.1", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/data-integrity/node_modules/rdf-canonize": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-4.0.1.tgz", + "integrity": "sha512-B5ynHt4sasbUafzrvYI2GFARgeFcD8Sx9yXPbg7gEyT2EH76rlCv84kyO6tnxzVbxUN/uJDbK1S/MXh+DsnuTA==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/data-integrity/node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@digitalbazaar/data-integrity/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@digitalbazaar/di-sd-primitives": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/di-sd-primitives/-/di-sd-primitives-3.1.0.tgz", + "integrity": "sha512-qi8H5yz4R6UmNWC6t99MSxFexniWZHME39Q77ev03afkgLMRMQSVwNGybrgeIK1VOKn0uF4YQj8quhkNlg5baQ==", + "dependencies": { + "base64url-universal": "^2.0.0", + "jsonld": "^8.3.2", + "klona": "^2.0.6", + "rdf-canonize": "^4.0.1", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/di-sd-primitives/node_modules/rdf-canonize": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-4.0.1.tgz", + "integrity": "sha512-B5ynHt4sasbUafzrvYI2GFARgeFcD8Sx9yXPbg7gEyT2EH76rlCv84kyO6tnxzVbxUN/uJDbK1S/MXh+DsnuTA==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/ecdsa-multikey": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/ecdsa-multikey/-/ecdsa-multikey-1.8.0.tgz", + "integrity": "sha512-Xo4oBCb0bJv6PYNBrLBZfR/jA2uNd9Bi+YTnadFtxTbhMQrSN0nTw3OnTBOOC7zxtL3t4N00ZweQM4zhsV6gVQ==", + "dependencies": { + "base58-universal": "^2.0.0", + "base64url-universal": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/ecdsa-sd-2023-cryptosuite": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@digitalbazaar/ecdsa-sd-2023-cryptosuite/-/ecdsa-sd-2023-cryptosuite-3.4.1.tgz", + "integrity": "sha512-PzKQneakxUUS/kzDgUZ0ZcZKuHhMhAelW2Bp/rryHEV43VeI3meoJkuQ0s7sfMlD1OWkKWrNClMr1znwBaQiLQ==", + "dependencies": { + "@digitalbazaar/di-sd-primitives": "^3.0.4", + "@digitalbazaar/ecdsa-multikey": "^1.1.3", + "base58-universal": "^2.0.0", + "base64url-universal": "^2.0.0", + "cborg": "^4.0.5", + "klona": "^2.0.6" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@digitalbazaar/http-client": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", @@ -4761,9 +5074,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "engines": { "node": "^14.21.3 || >=16" }, @@ -12393,6 +12706,14 @@ "node": ">=0.10.0" } }, + "node_modules/base58-universal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base58-universal/-/base58-universal-2.0.0.tgz", + "integrity": "sha512-BgkgF8zVLOAygszG4W8NkLm7iXrw80VYAOcedrzANrIhS14+4W6zVqjyGTFUBM/FpqkHUt8aAYd4DbBBfn3zKg==", + "engines": { + "node": ">=14" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -12838,6 +13159,26 @@ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, + "node_modules/cbor": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", + "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "license": "MIT", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/cborg": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.12.tgz", + "integrity": "sha512-z126yLoavS75cdTuiKu61RC3Y3trqtDAgQRa5Q0dpHn1RmqhIedptWXKnk0lQ5yo/GmcV9myvIkzFgZ8GnqSog==", + "bin": { + "cborg": "lib/bin.js" + } + }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -19492,6 +19833,23 @@ "node": ">=10" } }, + "node_modules/jsonld-signatures-v7": { + "name": "jsonld-signatures", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-7.0.0.tgz", + "integrity": "sha512-J/nA+llcYYjErPHG9WFpXvR82TOg5fbHk/7rXbx4Ts854DPReaKAAd0hAZ+s5/P2WIIAZPIHCqA+iz1QrOqeiQ==", + "dependencies": { + "base64url": "^3.0.1", + "crypto-ld": "^3.7.0", + "jsonld": "^4.0.1", + "node-forge": "^0.10.0", + "security-context": "^4.0.0", + "serialize-error": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsonld/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -19630,6 +19988,14 @@ "node": ">=6" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -21456,6 +21822,15 @@ "node": ">=4" } }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "license": "MIT", + "engines": { + "node": ">=12.19" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -32587,13 +32962,13 @@ }, "packages/w3c": { "name": "@trustvc/w3c", - "version": "1.2.16", + "version": "1.3.0-alpha.17", "license": "Apache-2.0", "dependencies": { - "@trustvc/w3c-context": "^1.2.12", - "@trustvc/w3c-credential-status": "^1.2.12", - "@trustvc/w3c-issuer": "^1.2.3", - "@trustvc/w3c-vc": "^1.2.16" + "@trustvc/w3c-context": "^1.3.0-alpha.12", + "@trustvc/w3c-credential-status": "^1.3.0-alpha.13", + "@trustvc/w3c-issuer": "^1.3.0-alpha.10", + "@trustvc/w3c-vc": "^1.3.0-alpha.17" }, "engines": { "node": ">=18.x" @@ -32601,23 +32976,103 @@ }, "packages/w3c-context": { "name": "@trustvc/w3c-context", - "version": "1.2.12", + "version": "1.3.0-alpha.12", "license": "Apache-2.0", "dependencies": { "did-resolver": "^4.1.0", - "jsonld-signatures": "^7.0.0" + "jsonld-signatures": "^11.5.0" }, "engines": { "node": ">=18.x" } }, + "packages/w3c-context/node_modules/jsonld": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-8.3.3.tgz", + "integrity": "sha512-9YcilrF+dLfg9NTEof/mJLMtbdX1RJ8dbWtJgE00cMOIohb1lIyJl710vFiTaiHTl6ZYODJuBd32xFvUhmv3kg==", + "dependencies": { + "@digitalbazaar/http-client": "^3.4.1", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "packages/w3c-context/node_modules/jsonld-signatures": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-11.5.0.tgz", + "integrity": "sha512-Kdto+e8uvY/5u3HYkmAbpy52bplWX9uqS8fmqdCv6oxnCFwCTM0hMt6r4rWqlhw5/aHoCHJIRxwYb4QKGC69Jw==", + "dependencies": { + "@digitalbazaar/security-context": "^1.0.0", + "jsonld": "^8.0.0", + "rdf-canonize": "^4.0.1", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/w3c-context/node_modules/jsonld-signatures/node_modules/rdf-canonize": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-4.0.1.tgz", + "integrity": "sha512-B5ynHt4sasbUafzrvYI2GFARgeFcD8Sx9yXPbg7gEyT2EH76rlCv84kyO6tnxzVbxUN/uJDbK1S/MXh+DsnuTA==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "packages/w3c-context/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "packages/w3c-context/node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/w3c-context/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/w3c-context/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "packages/w3c-credential-status": { "name": "@trustvc/w3c-credential-status", - "version": "1.2.12", + "version": "1.3.0-alpha.13", "license": "Apache-2.0", "dependencies": { - "@trustvc/w3c-context": "^1.2.12", - "@trustvc/w3c-issuer": "^1.2.3", + "@trustvc/w3c-context": "^1.3.0-alpha.12", + "@trustvc/w3c-issuer": "^1.3.0-alpha.10", "base64url-universal": "^2.0.0", "pako": "^2.1.0" }, @@ -32627,9 +33082,11 @@ }, "packages/w3c-issuer": { "name": "@trustvc/w3c-issuer", - "version": "1.2.3", + "version": "1.3.0-alpha.10", "license": "Apache-2.0", "dependencies": { + "@digitalbazaar/bls12-381-multikey": "^2.1.0", + "@digitalbazaar/ecdsa-multikey": "^1.8.0", "@mattrglobal/bls12381-key-pair": "^1.2.1", "bip39": "^3.1.0", "did-resolver": "^4.1.0", @@ -32642,15 +33099,23 @@ }, "packages/w3c-vc": { "name": "@trustvc/w3c-vc", - "version": "1.2.16", + "version": "1.2.17", "license": "Apache-2.0", "dependencies": { + "@digitalbazaar/bbs-2023-cryptosuite": "^2.0.1", + "@digitalbazaar/bls12-381-multikey": "^2.1.0", + "@digitalbazaar/data-integrity": "^2.5.0", + "@digitalbazaar/ecdsa-multikey": "^1.8.0", + "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.4.1", "@mattrglobal/jsonld-signatures-bbs": "^1.2.0", - "@trustvc/w3c-credential-status": "^1.2.12", - "@trustvc/w3c-issuer": "^1.2.3", + "@trustvc/w3c-credential-status": "^1.3.0-alpha.13", + "@trustvc/w3c-issuer": "^1.3.0-alpha.10", + "base64url-universal": "^2.0.0", + "cbor": "^9.0.2", "did-resolver": "^4.1.0", "jsonld": "^6.0.0", - "jsonld-signatures": "7.0.0", + "jsonld-signatures": "^11.5.0", + "jsonld-signatures-v7": "npm:jsonld-signatures@7.0.0", "uuid": "^10.0.0" }, "devDependencies": { @@ -32662,6 +33127,225 @@ "peerDependencies": { "jsonld": "^6.0.0" } + }, + "packages/w3c-vc/node_modules/jsonld-signatures": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-11.5.0.tgz", + "integrity": "sha512-Kdto+e8uvY/5u3HYkmAbpy52bplWX9uqS8fmqdCv6oxnCFwCTM0hMt6r4rWqlhw5/aHoCHJIRxwYb4QKGC69Jw==", + "dependencies": { + "@digitalbazaar/security-context": "^1.0.0", + "jsonld": "^8.0.0", + "rdf-canonize": "^4.0.1", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/w3c-vc/node_modules/jsonld-signatures/node_modules/jsonld": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-8.3.3.tgz", + "integrity": "sha512-9YcilrF+dLfg9NTEof/mJLMtbdX1RJ8dbWtJgE00cMOIohb1lIyJl710vFiTaiHTl6ZYODJuBd32xFvUhmv3kg==", + "dependencies": { + "@digitalbazaar/http-client": "^3.4.1", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "packages/w3c-vc/node_modules/jsonld-signatures/node_modules/jsonld/node_modules/rdf-canonize": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", + "integrity": "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=12" + } + }, + "packages/w3c-vc/node_modules/jsonld-signatures/node_modules/rdf-canonize": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-4.0.1.tgz", + "integrity": "sha512-B5ynHt4sasbUafzrvYI2GFARgeFcD8Sx9yXPbg7gEyT2EH76rlCv84kyO6tnxzVbxUN/uJDbK1S/MXh+DsnuTA==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "packages/w3c-vc/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "packages/w3c-vc/node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/w3c-vc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/w3c-vc/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "packages/w3c/node_modules/@trustvc/w3c-vc": { + "version": "1.3.0-alpha.17", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-vc/-/w3c-vc-1.3.0-alpha.17.tgz", + "integrity": "sha512-aQXiu8xQ3fIN262h1KCoWkWRH0AeeTzN/68NbDqv3DdbA/iAnQoerwoSd//7oFoithr0yySCbcViulqlMZ8kHg==", + "license": "Apache-2.0", + "dependencies": { + "@digitalbazaar/bbs-2023-cryptosuite": "^2.0.1", + "@digitalbazaar/bls12-381-multikey": "^2.1.0", + "@digitalbazaar/data-integrity": "^2.5.0", + "@digitalbazaar/ecdsa-multikey": "^1.8.0", + "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.4.1", + "@mattrglobal/jsonld-signatures-bbs": "^1.2.0", + "@trustvc/w3c-credential-status": "^1.3.0-alpha.13", + "@trustvc/w3c-issuer": "^1.3.0-alpha.10", + "base64url-universal": "^2.0.0", + "cbor": "^9.0.2", + "did-resolver": "^4.1.0", + "jsonld": "^6.0.0", + "jsonld-signatures": "^11.5.0", + "jsonld-signatures-v7": "npm:jsonld-signatures@7.0.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "jsonld": "^6.0.0" + } + }, + "packages/w3c/node_modules/jsonld-signatures": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-11.5.0.tgz", + "integrity": "sha512-Kdto+e8uvY/5u3HYkmAbpy52bplWX9uqS8fmqdCv6oxnCFwCTM0hMt6r4rWqlhw5/aHoCHJIRxwYb4QKGC69Jw==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/security-context": "^1.0.0", + "jsonld": "^8.0.0", + "rdf-canonize": "^4.0.1", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/w3c/node_modules/jsonld-signatures/node_modules/jsonld": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-8.3.3.tgz", + "integrity": "sha512-9YcilrF+dLfg9NTEof/mJLMtbdX1RJ8dbWtJgE00cMOIohb1lIyJl710vFiTaiHTl6ZYODJuBd32xFvUhmv3kg==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/http-client": "^3.4.1", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "packages/w3c/node_modules/jsonld-signatures/node_modules/jsonld/node_modules/rdf-canonize": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", + "integrity": "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=12" + } + }, + "packages/w3c/node_modules/jsonld-signatures/node_modules/rdf-canonize": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-4.0.1.tgz", + "integrity": "sha512-B5ynHt4sasbUafzrvYI2GFARgeFcD8Sx9yXPbg7gEyT2EH76rlCv84kyO6tnxzVbxUN/uJDbK1S/MXh+DsnuTA==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "packages/w3c/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "packages/w3c/node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/w3c/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/w3c/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/package.json b/package.json index ac5d3e51..b4fa99d3 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,6 @@ "express": "^4.19.2", "inquirer": "^10.1.2", "jsonld": "^6.0.0", - "jsonld-signatures": "7.0.0", "multiformats": "^9.9.0", "pako": "^2.1.0", "tslib": "^2.3.0", diff --git a/packages/declaration.d.ts b/packages/declaration.d.ts new file mode 100644 index 00000000..255d5e5d --- /dev/null +++ b/packages/declaration.d.ts @@ -0,0 +1,8 @@ +declare module '@digitalbazaar/bls12-381-multikey'; +declare module '@digitalbazaar/bbs-2023-cryptosuite'; +declare module '@digitalbazaar/ecdsa-multikey'; +declare module '@digitalbazaar/ecdsa-sd-2023-cryptosuite'; +declare module '@digitalbazaar/data-integrity'; +declare module 'jsonld'; +declare module 'jsonld-signatures'; +declare module 'jsonld-signatures-v7'; diff --git a/packages/w3c-context/README.md b/packages/w3c-context/README.md index 4fd04065..b8ca512b 100644 --- a/packages/w3c-context/README.md +++ b/packages/w3c-context/README.md @@ -1,9 +1,177 @@ # TrustVC W3C Context -A package to cache the commonly served JSON context schema in the `@context` within VCs. + +A comprehensive package that caches commonly used JSON-LD context schemas for W3C Verifiable Credentials and Data Integrity proofs. This package provides a document loader that resolves context URLs locally, improving performance and reliability for verifiable credential operations. + +## Features + +- **Local Context Caching**: Pre-cached JSON-LD contexts for faster resolution +- **W3C Standards Support**: Full support for W3C Verifiable Credentials and Data Integrity specifications +- **Cryptosuite Support**: Includes contexts for multiple cryptographic suites: + - BBS+ signatures (bbs-v1) + - JWS 2020 (jws-2020) + - BLS12-381 keys +- **DID Resolution**: Built-in support for DID document resolution +- **Custom Context Support**: Ability to add additional contexts at runtime +- **TypeScript Support**: Full TypeScript definitions included ## Installation -To install the package, use: ```sh npm install @trustvc/w3c-context ``` + +## Usage + +### Basic Document Loader + +```typescript +import { getDocumentLoader } from '@trustvc/w3c-context'; + +// Get a document loader with all cached contexts +const documentLoader = await getDocumentLoader(); + +// Use with jsonld-signatures or other JSON-LD libraries +const result = await documentLoader('https://w3id.org/security/data-integrity/v2'); +``` + +### With Additional Contexts + +```typescript +import { getDocumentLoader } from '@trustvc/w3c-context'; + +// Add custom contexts +const additionalContexts = { + 'https://example.com/my-context': { + "@context": { + "MyProperty": "https://example.com/vocab#MyProperty" + } + } +}; + +const documentLoader = await getDocumentLoader(additionalContexts); +``` + +### Available Context URLs + +The package includes the following pre-cached contexts: + +#### Core W3C Contexts +- `https://w3id.org/security/data-integrity/v2` - Data Integrity v2 +- `https://www.w3.org/ns/did/v1` - DID Core v1 +- `https://www.w3.org/2018/credentials/v1` - Verifiable Credentials v1 +- `https://www.w3.org/ns/credentials/v2` - Verifiable Credentials v2 +- `https://w3id.org/vc/status-list/2021/v1` - Status List 2021 v1 + +#### Cryptographic Suite Contexts +- `https://w3id.org/security/bbs/v1` - BBS+ v1 +- `https://w3id.org/security/suites/bls12381-2020/v1` - BLS12-381 2020 +- `https://w3id.org/security/suites/jws-2020/v1` - JWS 2020 + +#### TrustVC Business Contexts +- `https://trustvc.io/context/transferable-records-context.json` - Transferable Records +- `https://trustvc.io/context/render-method-context.json` - Render Methods +- `https://trustvc.io/context/attachments-context.json` - Attachments +- `https://trustvc.io/context/qrcode-context.json` - QR Code +- `https://trustvc.io/context/bill-of-lading.json` - Bill of Lading +- `https://trustvc.io/context/bill-of-lading-carrier.json` - Bill of Lading Carrier +- `https://trustvc.io/context/coo.json` - Certificate of Origin +- `https://trustvc.io/context/invoice.json` - Invoice +- `https://trustvc.io/context/promissory-note.json` - Promissory Note +- `https://trustvc.io/context/warehouse-receipt.json` - Warehouse Receipt + +### Context Constants + +You can import URL constants for type safety: + +```typescript +import { + DATA_INTEGRITY_V2_URL, + MULTIKEY_V1_URL, + VC_V1_URL +} from '@trustvc/w3c-context'; + +console.log(DATA_INTEGRITY_V2_URL); // https://w3id.org/security/data-integrity/v2 +console.log(MULTIKEY_V1_URL); // https://w3id.org/security/multikey/v1 +``` + +### Using Modern Cryptosuites + +For BBS-2023 and ECDSA-SD-2023, use the Data Integrity context: + +```typescript +import { DATA_INTEGRITY_V2_URL, VC_V1_URL } from '@trustvc/w3c-context'; + +// Create a credential with BBS-2023 or ECDSA-SD-2023 +const credential = { + "@context": [ + VC_V1_URL, + DATA_INTEGRITY_V2_URL // Supports BBS-2023, ECDSA-SD-2023 + ], + "type": ["VerifiableCredential"], + "credentialSubject": { + // ... credential data + }, + "proof": { + "type": "DataIntegrityProof", + "cryptosuite": "bbs-2023", // or "ecdsa-sd-2023" + // ... proof data + } +}; +``` + +### DID Resolution + +The document loader automatically resolves DID URLs: + +```typescript +const documentLoader = await getDocumentLoader(); + +// Resolves DID document +const didDoc = await documentLoader('did:web:example.com'); + +// Resolves specific verification method +const verificationMethod = await documentLoader('did:web:example.com#key-1'); +``` + +## API Reference + +### `getDocumentLoader(additionalContexts?)` + +Creates a document loader function that resolves JSON-LD contexts. + +**Parameters:** +- `additionalContexts` (optional): Record - Additional contexts to include + +**Returns:** Promise - A document loader function + +### Context Categories + +The package organizes contexts into several categories: + +- `contexts` - Core W3C and cryptographic contexts +- `trContexts` - Transferable records contexts +- `renderContexts` - Rendering method contexts +- `attachmentsContexts` - Attachment contexts +- `qrCodeContexts` - QR code contexts +- `templateContexts` - Business document templates + +## Development + +```sh +# Install dependencies +npm install + +# Build the package +npm run build + +# Run tests +npm test +``` + +## License + +MIT License - see LICENSE file for details. + +## Contributing + +Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository. diff --git a/packages/w3c-context/package.json b/packages/w3c-context/package.json index 4ee7916d..f4bb459b 100644 --- a/packages/w3c-context/package.json +++ b/packages/w3c-context/package.json @@ -1,6 +1,6 @@ { "name": "@trustvc/w3c-context", - "version": "1.2.13", + "version": "1.3.0-alpha.12", "description": "", "main": "dist/index.js", "module": "dist/esm/index.js", @@ -34,7 +34,7 @@ }, "dependencies": { "did-resolver": "^4.1.0", - "jsonld-signatures": "^7.0.0" + "jsonld-signatures": "^11.5.0" }, "repository": { "type": "git", diff --git a/packages/w3c-context/src/context/data-integrity-v2.json b/packages/w3c-context/src/context/data-integrity-v2.json new file mode 100644 index 00000000..dfd8f35f --- /dev/null +++ b/packages/w3c-context/src/context/data-integrity-v2.json @@ -0,0 +1,81 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + "@protected": true, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "DataIntegrityProof": { + "@id": "https://w3id.org/security#DataIntegrityProof", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "nonce": "https://w3id.org/security#nonce", + "previousProof": { + "@id": "https://w3id.org/security#previousProof", + "@type": "@id" + }, + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "cryptosuite": { + "@id": "https://w3id.org/security#cryptosuite", + "@type": "https://w3id.org/security#cryptosuiteString" + }, + "proofValue": { + "@id": "https://w3id.org/security#proofValue", + "@type": "https://w3id.org/security#multibase" + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +} diff --git a/packages/w3c-context/src/context/multikey-v1.json b/packages/w3c-context/src/context/multikey-v1.json new file mode 100644 index 00000000..43ac7f0c --- /dev/null +++ b/packages/w3c-context/src/context/multikey-v1.json @@ -0,0 +1,35 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + "@protected": true, + "Multikey": { + "@id": "https://w3id.org/security#Multikey", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "revoked": { + "@id": "https://w3id.org/security#revoked", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "publicKeyMultibase": { + "@id": "https://w3id.org/security#publicKeyMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "secretKeyMultibase": { + "@id": "https://w3id.org/security#secretKeyMultibase", + "@type": "https://w3id.org/security#multibase" + } + } + } + } +} diff --git a/packages/w3c-context/src/context/render-method-context-v2.json b/packages/w3c-context/src/context/render-method-context-v2.json new file mode 100644 index 00000000..af1e2670 --- /dev/null +++ b/packages/w3c-context/src/context/render-method-context-v2.json @@ -0,0 +1,13 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + "xsd": "http://www.w3.org/2001/XMLSchema#", + "EMBEDDED_RENDERER": { + "@id": "https://example.org/terms#EmbeddedRenderer", + "@context": { + "templateName": "https://example.org/terms#templateName" + } + } + } +} diff --git a/packages/w3c-context/src/context/status-list-2021-v1.json b/packages/w3c-context/src/context/status-list-2021-v1.json new file mode 100644 index 00000000..f3d2824d --- /dev/null +++ b/packages/w3c-context/src/context/status-list-2021-v1.json @@ -0,0 +1,39 @@ +{ + "@context": { + "@protected": true, + "StatusList2021Credential": { + "@id": "https://w3id.org/vc/status-list#StatusList2021Credential", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "description": "http://schema.org/description", + "name": "http://schema.org/name" + } + }, + "StatusList2021": { + "@id": "https://w3id.org/vc/status-list#StatusList2021", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "statusPurpose": "https://w3id.org/vc/status-list#statusPurpose", + "encodedList": "https://w3id.org/vc/status-list#encodedList" + } + }, + "StatusList2021Entry": { + "@id": "https://w3id.org/vc/status-list#StatusList2021Entry", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "statusPurpose": "https://w3id.org/vc/status-list#statusPurpose", + "statusListIndex": "https://w3id.org/vc/status-list#statusListIndex", + "statusListCredential": { + "@id": "https://w3id.org/vc/status-list#statusListCredential", + "@type": "@id" + } + } + } + } +} diff --git a/packages/w3c-context/src/lib/index.ts b/packages/w3c-context/src/lib/index.ts index 6c895c94..54ae1570 100644 --- a/packages/w3c-context/src/lib/index.ts +++ b/packages/w3c-context/src/lib/index.ts @@ -9,26 +9,33 @@ import bolcContext from '../context/bill-of-lading-carrier.json'; import cooContext from '../context/coo.json'; import credentialsV1 from '../context/credentials-v1.json'; import credentialsV2 from '../context/credentials-v2.json'; +import dataIntegrityV2 from '../context/data-integrity-v2.json'; import didV1 from '../context/did-v1.json'; import invoiceContext from '../context/invoice.json'; import jwsV1 from '../context/jws-2020-v1.json'; +import multikeyV1 from '../context/multikey-v1.json'; import promissoryNoteContext from '../context/promissory-note.json'; import qrCodeContext from '../context/qrcode-context.json'; import renderContext from '../context/render-method-context.json'; +import renderContextV2 from '../context/render-method-context-v2.json'; +import statusList2021V1 from '../context/status-list-2021-v1.json'; import trContext from '../context/transferable-records-context.json'; import warehouseReceiptContext from '../context/warehouse-receipt.json'; import { Document } from './types'; +export const DATA_INTEGRITY_V2_URL = 'https://w3id.org/security/data-integrity/v2'; export const DID_V1_URL = 'https://www.w3.org/ns/did/v1'; export const VC_V1_URL = 'https://www.w3.org/2018/credentials/v1'; export const VC_V2_URL = 'https://www.w3.org/ns/credentials/v2'; export const BBS_V1_URL = 'https://w3id.org/security/bbs/v1'; export const BLS12381_2020_V1_URL = 'https://w3id.org/security/suites/bls12381-2020/v1'; export const JWS_V1_URL = 'https://w3id.org/security/suites/jws-2020/v1'; +export const MULTIKEY_V1_URL = 'https://w3id.org/security/multikey/v1'; export const STATUS_LIST_2021_CREDENTIAL_URL = 'https://w3id.org/vc/status-list/2021/v1'; export const TR_CONTEXT_URL = 'https://trustvc.io/context/transferable-records-context.json'; export const RENDER_CONTEXT_URL = 'https://trustvc.io/context/render-method-context.json'; +export const RENDER_CONTEXT_V2_URL = 'https://trustvc.io/context/render-method-context-v2.json'; export const ATTACHMENTS_CONTEXT_URL = 'https://trustvc.io/context/attachments-context.json'; export const QRCODE_CONTEXT_URL = 'https://trustvc.io/context/qrcode-context.json'; @@ -40,12 +47,15 @@ export const PROMISSORY_NOTE_CONTEXT_URL = 'https://trustvc.io/context/promissor export const WAREHOUSE_RECEIPT_CONTEXT_URL = 'https://trustvc.io/context/warehouse-receipt.json'; export const contexts: { [key: string]: Document } = { + [DATA_INTEGRITY_V2_URL]: dataIntegrityV2, [DID_V1_URL]: didV1, [VC_V1_URL]: credentialsV1, [VC_V2_URL]: credentialsV2, [BBS_V1_URL]: bbsV1, [BLS12381_2020_V1_URL]: bbsV1, [JWS_V1_URL]: jwsV1, + [MULTIKEY_V1_URL]: multikeyV1, + [STATUS_LIST_2021_CREDENTIAL_URL]: statusList2021V1, }; export const trContexts: { [key: string]: Document } = { @@ -56,6 +66,10 @@ export const renderContexts: { [key: string]: Document } = { [RENDER_CONTEXT_URL]: renderContext, }; +export const renderContextsV2: { [key: string]: Document } = { + [RENDER_CONTEXT_V2_URL]: renderContextV2, +}; + export const attachmentsContexts: { [key: string]: Document } = { [ATTACHMENTS_CONTEXT_URL]: attachmentsContext, }; @@ -96,6 +110,7 @@ export async function getDocumentLoader( contexts, trContexts, renderContexts, + renderContextsV2, attachmentsContexts, qrCodeContexts, templateContexts, @@ -113,15 +128,38 @@ export async function getDocumentLoader( const resolveDid = async (did: string) => { const { wellKnownDid } = await queryDidDocument({ did }); - const result: DocumentLoaderObject = { - contextUrl: null, - document: wellKnownDid, - documentUrl: did, - }; - resultMap.set(did, result); + // Check if the DID includes a fragment (verification method ID) + if (did.includes('#')) { + // This is a verification method ID, find the specific verification method + const verificationMethod = wellKnownDid.verificationMethod?.find((vm) => vm.id === did); + + if (!verificationMethod) { + throw new Error(`Verification method ${did} could not be found.`); + } - return result; + const result: DocumentLoaderObject = { + contextUrl: null, + document: { + '@context': wellKnownDid['@context'], + ...verificationMethod, + }, + documentUrl: did, + }; + + resultMap.set(did, result); + return result; + } else { + // This is a DID document request, return the full document + const result: DocumentLoaderObject = { + contextUrl: null, + document: wellKnownDid, + documentUrl: did, + }; + + resultMap.set(did, result); + return result; + } }; const customDocLoader = async (url: string) => { diff --git a/packages/w3c-context/tsconfig.build.json b/packages/w3c-context/tsconfig.build.json index 0a3bc92c..1a33927b 100644 --- a/packages/w3c-context/tsconfig.build.json +++ b/packages/w3c-context/tsconfig.build.json @@ -10,7 +10,8 @@ }, "include": [ "src/**/*.ts", - "src/**/*.json" + "src/**/*.json", + "../declaration.d.ts" ], "exclude": ["node_modules", ".coverage", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/packages/w3c-context/tsconfig.json b/packages/w3c-context/tsconfig.json index 20201530..bdbf97b8 100644 --- a/packages/w3c-context/tsconfig.json +++ b/packages/w3c-context/tsconfig.json @@ -3,6 +3,5 @@ "compilerOptions": { "module": "commonjs" }, - "files": [], - "include": [] + "include": ["src/**/*.ts", "../declaration.d.ts"] } diff --git a/packages/w3c-credential-status/README.md b/packages/w3c-credential-status/README.md index cc7a9450..e3a0a0c6 100644 --- a/packages/w3c-credential-status/README.md +++ b/packages/w3c-credential-status/README.md @@ -2,14 +2,28 @@ Implements credential status management for Verifiable Credentials (VCs) using @trustvc/w3c-credential-status. It supports lifecycle operations such as revocation and suspension without altering the original credential, enabling verifiers to check a credential's current status. The project utilizes status lists and handles the credential validation process. -[Bitstring Status List](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/) refers to a method of representing the revocation/suspension status of multiple items (such as digital certificates or credentials) using a bitstring (a sequence of bits). This technique allows for efficient tracking and management of which items are valid or revoked without needing to maintain extensive individual records for each item. +This package supports both **W3C VC Data Model v1.1** (legacy) and **W3C VC Data Model v2.0** (modern) specifications: + +- **v1.1 Support**: Uses `StatusList2021Credential` with `StatusList2021Entry` credential status +- **v2.0 Support**: Uses `BitstringStatusListCredential` with `BitstringStatusListEntry` credential status + +The [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/) specification (v2.0) and the legacy [Status List 2021](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/) specification (v1.1) both refer to methods of representing the revocation/suspension status of multiple items (such as digital certificates or credentials) using a bitstring (a sequence of bits). This technique allows for efficient tracking and management of which items are valid or revoked without needing to maintain extensive individual records for each item. ## Revoking/Suspending a VC -The revocation or suspension of a [Verifiable Credentials v1.1. -](https://www.w3.org/TR/vc-data-model/) is achieved by changing the binary value of the bitstring at its given position in the bitstring. Every time the bitstring state changes, it must be compressed, encoded, and published as a Status VC of type `StatusList2021Entry`. +The revocation or suspension of Verifiable Credentials is achieved by changing the binary value of the bitstring at its given position in the bitstring. Every time the bitstring state changes, it must be compressed, encoded, and published as a Status VC. + +**For W3C VC Data Model v1.1:** +- Status VC type: `StatusList2021Credential` +- Credential status type: `StatusList2021Entry` +- Credential subject type: `StatusList2021` -This module provides functionality to create/updat a signed Verifiable Credential (VC) for credential status, using a specified cryptographic suite and key pair. It supports the creation of different types of credential status VCs such as [StatusList2021Credential](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/). +**For W3C VC Data Model v2.0:** +- Status VC type: `BitstringStatusListCredential` +- Credential status type: `BitstringStatusListEntry` +- Credential subject type: `BitstringStatusList` + +This module provides functionality to create/update a signed Verifiable Credential (VC) for credential status, using a specified cryptographic suite and key pair. It supports modern ECDSA-SD-2023 and BBS-2023 cryptosuites. ## Table of Contents @@ -49,8 +63,12 @@ npm install @trustvc/w3c-credential-status ## Features -- Create Credential Status Verifiable Credentials (e.g., BBS). -- Update revocation status for existing VC. +- **Version Support**: Compatible with both W3C VC Data Model v1.1 and v2.0 +- **Multiple Cryptosuites**: Supports modern signatures ECDSA-SD-2023 and BBS2023 +- **Flexible Status Types**: Create both `StatusList2021Credential` and `BitstringStatusListCredential` +- **Backward Compatibility**: Existing v1.1 implementations continue to work +- Create Credential Status Verifiable Credentials with various cryptographic suites +- Update revocation status for existing VCs --- @@ -76,7 +94,7 @@ import { signCredential, SignedVerifiableCredential } from '@trustvc/w3c-vc'; Pick a URL where you'd like to host your credential status. (e.g., https://example.com/credentials/statuslist/1) ```typescript -const hostingUrl = https://example.com/credentials/statuslist/1; +const hostingUrl = 'https://example.com/credentials/statuslist/1'; ``` You can create a new StatusList object with a default or custom length. By default, the list is 131,072 bits (16 KB), but you can adjust it if needed. @@ -92,12 +110,11 @@ const credentialStatus = new StatusList({ length: 131072 }); The StatusList can serve different purposes, such as revocation or suspension of credentials. You can choose the purpose using the following setup: -```typecript +```typescript type CredentialStatusPurpose = 'revocation' | 'suspension'; // Choose between 'revocation' or 'suspension' -const purpose: CredentialStatusPurpose = "revocation"; - +const purpose: CredentialStatusPurpose = 'revocation'; ``` #### Step 4 - Retrieve and update the status of the index of the StatusList @@ -140,37 +157,65 @@ import { PrivateKeyPair } from '@trustvc/w3c-issuer'; * - options.id (string): The ID of the credential. * - options.credentialSubject (object): The credential subject. * - keyPair (PrivateKeyPair): The key pair options for signing - * - type (VCCredentialStatusType): The type of the credential status VC. Defaults to 'StatusList2021Credential'. - * - cryptoSuite (string): The cryptosuite to be used for signing. Defaults to 'BbsBlsSignature2020'. + * - type (VCCredentialStatusType): The type of the credential status VC. + * Options: 'StatusList2021Credential' (v1.1) or 'BitstringStatusListCredential' (v2.0) + * - cryptoSuite (string): The cryptosuite to be used for signing. + * Options: 'ecdsa-sd-2023', or 'bbs-2023' * * Returns: * - A Promise that resolves to: * - RawCredentialStatusVC: The signed credential status Verifiable Credential. */ -const options = { + +// Example for W3C VC Data Model v2.0 (modern) +const optionsV2 = { id: hostingUrl, credentialSubject: { id: `${hostingUrl}#list`, - type: 'StatusList2021', + type: 'BitstringStatusList', // v2.0 credential subject type statusPurpose: purpose, encodedList, - }; -} - -const keyPair = { - "id": "did:web:trustvc.github.io:did:1#keys-1", - "type": "Bls12381G2Key2020", - "controller": "did:web:trustvc.github.io:did:1", - "seedBase58": "", - "privateKeyBase58": "", - "publicKeyBase58": "oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ" + }, }; -const credentialStatusVC = await createCredentialStatusPayload(options, keyPair); +// Example with ECDSA-SD-2023 +const credentialStatusVCV2_ECDSA = await createCredentialStatusPayload( + optionsV2, + keyPair, + 'BitstringStatusListCredential', // v2.0 credential type + 'ecdsa-sd-2023' // modern cryptosuite +); -console.log('Credential Status VC:', credentialStatusVC); +// Example with BBS-2023 +const credentialStatusVCV2_BBS = await createCredentialStatusPayload( + optionsV2, + keyPair, + 'BitstringStatusListCredential', // v2.0 credential type, + 'bbs-2023' // modern cryptosuite +); + +console.log('Credential Status VC (ECDSA):', credentialStatusVCV2_ECDSA); +console.log('Credential Status VC (BBS):', credentialStatusVCV2_BBS); + + +// Example for W3C VC Data Model v1.1 (legacy) +const optionsV1 = { + id: hostingUrl, + credentialSubject: { + id: `${hostingUrl}#list`, + type: 'StatusList2021', // v1.1 credential subject type + statusPurpose: purpose, + encodedList, + }, +}; +const credentialStatusVCV1 = await createCredentialStatusPayload( + optionsV1, + keyPair, + 'StatusList2021Credential', // v1.1 credential type + 'BbsBlsSignature2020' // ⚠️ DEPRECATED - this will result in error. Use modern cryptosuites +); // Sign the credential status payload const { signed, error } = await signCredential(credentialStatusPayload, keypairData); @@ -183,38 +228,74 @@ const signedCredentialStatusVC = signed; ```
- signCredential Result + signCredential Result (v2.0 with ECDSA-SD-2023) ```js Signed Credential: { '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3c-ccg.github.io/citizenship-vocab/contexts/citizenship-v1.jsonld', - 'https://w3id.org/security/bbs/v1', - 'https://w3id.org/vc/status-list/2021/v1' + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/data-integrity/v2' ], credentialStatus: { id: 'https://trustvc.github.io/did/credentials/statuslist/1#1', statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/1', statusListIndex: '1', statusPurpose: 'revocation', - type: 'StatusList2021Entry' + type: 'BitstringStatusListEntry' }, - issuanceDate: '2024-04-01T12:19:52Z', + validFrom: '2024-04-01T12:19:52Z', credentialSubject: { id: 'did:example:b34ca6cd37bbf23', type: [ 'Person' ], name: 'TrustVC' }, - expirationDate: '2029-12-03T12:19:52Z', + validUntil: '2029-12-03T12:19:52Z', issuer: 'did:web:trustvc.github.io:did:1', type: [ 'VerifiableCredential' ], proof: { - type: 'BbsBlsSignature2020', + type: 'DataIntegrityProof', + cryptosuite: 'ecdsa-sd-2023', created: '2024-10-02T09:04:07Z', proofPurpose: 'assertionMethod', - proofValue: 'tissP5pJF1q4txCMWNZI5LgwhXMWrLI8675ops8FwlQE/zBUQnVO9Iey505MjkNDD5GdmQmnb6+RUKkLVGEJLIJrKQXlU3Xr4DlMW7ShH/sIpuvZoobGs/0hw/B5agXz8cVWfnDGWtDYciVh0rwQvg==', - verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1' + proofValue: 'u2V0AhVhAip...', + verificationMethod: 'did:web:trustvc.github.io:did:1#multikey-1' + } +} +``` + +
+ +
+ signCredential Result (v2.0 with BBS-2023) + +```js +Signed Credential: { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/data-integrity/v2' + ], + credentialStatus: { + id: 'https://trustvc.github.io/did/credentials/statuslist/1#1', + type: 'BitstringStatusListEntry', + statusPurpose: 'revocation', + statusListIndex: '10', + statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/1' + }, + validFrom: '2024-04-01T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: [ 'Person' ], + name: 'TrustVC' + }, + validUntil: '2029-12-03T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + type: [ 'VerifiableCredential' ], + proof: { + type: 'DataIntegrityProof', + cryptosuite: 'bbs-2023', + proofPurpose: 'assertionMethod', + proofValue: 'u2V0ChVhQi0FELn9kYULQF...', + verificationMethod: 'did:web:trustvc.github.io:did:1#multikey-2' } } ``` @@ -284,19 +365,19 @@ After updating the status list, encode it and create a signed credential status // Encode the updated status list const encodedList = await statusList.encode(); -// Create the credential status payload const credentialStatusPayload = await createCredentialStatusPayload( { id: hostingUrl, credentialSubject: { id: `${hostingUrl}#list`, - type: 'StatusList2021', + type: 'BitstringStatusList', statusPurpose: purpose, encodedList, }, }, keypairData, // Your key pair data - 'StatusList2021Credential', + 'BitstringStatusListCredential', + 'ecdsa-sd-2023' // Use 'ecdsa-sd-2023' or 'bbs-2023' ); // Sign the credential status payload @@ -315,7 +396,7 @@ const signedCredentialStatusVC = signed; ### `createCredentialStatusPayload` -> Creates a credential status payload. +> Creates a credential status payload for both W3C VC Data Model v1.1 and v2.0. > > #### Parameters: > @@ -323,11 +404,18 @@ const signedCredentialStatusVC = signed; > > `keypairData (object)`: The key pair data used for signing. > -> `credentialType (string)`: The type of credential (e.g., >'StatusList2021Credential'). +> `credentialType (VCCredentialStatusType)`: The type of credential. Options: +> - `'BitstringStatusListCredential'` (v2.0 modern) +> - `'StatusList2021Credential'` (v1.1 legacy) +> +> `cryptoSuite (CryptoSuiteName)`: The cryptosuite for signing. Options: +> - `'ecdsa-sd-2023'` (modern ECDSA-SD-2023 signatures) +> - `'bbs-2023'` (modern BBS-2023 signatures) +> - `'BbsBlsSignature2020'` (⚠️ DEPRECATED: legacy BBS+ signatures, use `'ecdsa-sd-2023'` or `'bbs-2023'` instead) ### `signCredential` -> Signs the credential status payload. +> Signs the credential status payload using the specified cryptosuite. > > #### Parameters: > @@ -337,7 +425,7 @@ const signedCredentialStatusVC = signed; ### `StatusList` -> Creates a new StatusList instance with a specified length and an optional >initial buffer. +> Creates a new StatusList instance with a specified length and an optional initial buffer. Compatible with both v1.1 and v2.0 credential formats. > > #### Constructor > @@ -349,7 +437,7 @@ const signedCredentialStatusVC = signed; > > `length (number)`: The length of the bitstring. > -> `buffer (Buffer | undefined)`: An optional buffer containing the initial >state of the bitstring. +> `buffer (Buffer | undefined)`: An optional buffer containing the initial state of the bitstring. > > #### Methods > @@ -359,4 +447,48 @@ const signedCredentialStatusVC = signed; > > `encode(): Promise` > -> `static decode({ encodedList }: { encodedList: string }): >Promise` +> `static decode({ encodedList }: { encodedList: string }): Promise` + +### Types + +#### `VCCredentialStatusType` +```typescript +type VCCredentialStatusType = 'BitstringStatusListCredential' | 'StatusList2021Credential'; +``` + +#### `CredentialStatusType` +```typescript +type CredentialStatusType = 'BitstringStatusListEntry' | 'StatusList2021Entry'; +``` + +#### `VCCredentialSubjectType` +```typescript +type VCCredentialSubjectType = 'BitstringStatusList' | 'StatusList2021'; +``` + +#### `CredentialStatusPurpose` +```typescript +type CredentialStatusPurpose = 'revocation' | 'suspension' | 'message'; +``` + +## Migration Guide + +### From v1.1 to v2.0 + +To migrate from W3C VC Data Model v1.1 to v2.0: + +1. **Update credential types:** + - `StatusList2021Credential` → `BitstringStatusListCredential` + - `StatusList2021Entry` → `BitstringStatusListEntry` + - `StatusList2021` → `BitstringStatusList` + +2. **Update cryptosuite:** + - `BbsBlsSignature2020` → `ecdsa-sd-2023` or `bbs-2023` + +3. **Update key pair format:** + - Legacy BLS keys → Modern multikey format (ECDSA or BBS2023) + +4. **Update contexts:** + - v1.1 contexts → v2.0 contexts (handled automatically) + +The `StatusList` class remains the same and works with both versions. diff --git a/packages/w3c-credential-status/package.json b/packages/w3c-credential-status/package.json index 29c7d8ef..ec2b6a96 100644 --- a/packages/w3c-credential-status/package.json +++ b/packages/w3c-credential-status/package.json @@ -1,6 +1,6 @@ { "name": "@trustvc/w3c-credential-status", - "version": "1.2.13", + "version": "1.3.0-alpha.13", "description": "", "main": "dist/index.js", "module": "dist/esm/index.js", @@ -29,8 +29,8 @@ } }, "dependencies": { - "@trustvc/w3c-context": "^1.2.13", - "@trustvc/w3c-issuer": "^1.2.4", + "@trustvc/w3c-context": "^1.3.0-alpha.12", + "@trustvc/w3c-issuer": "^1.3.0-alpha.10", "base64url-universal": "^2.0.0", "pako": "^2.1.0" }, diff --git a/packages/w3c-credential-status/src/lib/BitstringStatusList/assertions.test.ts b/packages/w3c-credential-status/src/lib/BitstringStatusList/assertions.test.ts index d184570b..8f636a5d 100644 --- a/packages/w3c-credential-status/src/lib/BitstringStatusList/assertions.test.ts +++ b/packages/w3c-credential-status/src/lib/BitstringStatusList/assertions.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { _checkCredentialSubjectForStatusList2021Credential, + _checkCredentialSubjectForBitstringStatusListCredential, isBoolean, isNonNegativeInteger, isNumber, @@ -81,62 +82,103 @@ describe('assertions', () => { }); }); - describe('_checkCredentialSubjectForStatusList2021Credential', () => { + describe.each([ + { + functionName: '_checkCredentialSubjectForStatusList2021Credential', + testFunction: _checkCredentialSubjectForStatusList2021Credential, + expectedType: 'StatusList2021' as const, + credentialType: 'StatusList2021Credential', + wrongType: 'BitstringStatusList' as const, + }, + { + functionName: '_checkCredentialSubjectForBitstringStatusListCredential', + testFunction: _checkCredentialSubjectForBitstringStatusListCredential, + expectedType: 'BitstringStatusList' as const, + credentialType: 'BitstringStatusListCredential', + wrongType: 'StatusList2021' as const, + }, + ])('$functionName', ({ testFunction, expectedType, credentialType, wrongType }) => { it('should throw if no credentialSubject', () => { - expect(() => - _checkCredentialSubjectForStatusList2021Credential(undefined as any), - ).toThrowErrorMatchingInlineSnapshot(`[Error: Credential subject must be an object.]`); + expect(() => testFunction(undefined as any)).toThrow('Credential subject must be an object.'); }); it('should throw if no type', () => { expect(() => - _checkCredentialSubjectForStatusList2021Credential({} as any), - ).toThrowErrorMatchingInlineSnapshot(`[Error: Credential subject must have a type.]`); + testFunction({ + statusPurpose: 'revocation', + encodedList: 'test', + } as any), + ).toThrow('Credential subject must have a type.'); }); + it('should throw if wrong type', () => { expect(() => - _checkCredentialSubjectForStatusList2021Credential({ type: 'test' } as any), - ).toThrowErrorMatchingInlineSnapshot( - `[Error: Invalid type for credentialSubject: Credential subject for StatusList2021Credential must have type "StatusList2021".]`, + testFunction({ + type: wrongType, + statusPurpose: 'revocation', + encodedList: 'test', + } as any), + ).toThrow( + `Invalid type for credentialSubject: Credential subject for ${credentialType} must have type "${expectedType}".`, ); }); it('should throw if no statusPurpose', () => { expect(() => - _checkCredentialSubjectForStatusList2021Credential({ type: 'StatusList2021' } as any), - ).toThrowErrorMatchingInlineSnapshot( - `[Error: Credential subject must have a statusPurpose.]`, - ); + testFunction({ + type: expectedType, + encodedList: 'test', + } as any), + ).toThrow('Credential subject must have a statusPurpose.'); }); - it('should throw if wrong statusPurpose', () => { + + it('should throw if invalid statusPurpose', () => { expect(() => - _checkCredentialSubjectForStatusList2021Credential({ - type: 'StatusList2021', + testFunction({ + type: expectedType, + encodedList: 'test', statusPurpose: 'test', } as any), - ).toThrowErrorMatchingInlineSnapshot( - `[Error: Unsupported statusPurpose: statusPurpose must be "revocation" or "suspension".]`, - ); + ).toThrow('Unsupported statusPurpose: statusPurpose must be "revocation" or "suspension".'); }); it('should throw if no encodedList', () => { expect(() => - _checkCredentialSubjectForStatusList2021Credential({ - type: 'StatusList2021', + testFunction({ + type: expectedType, statusPurpose: 'revocation', } as any), - ).toThrowErrorMatchingInlineSnapshot(`[Error: Credential subject must have an encodedList.]`); + ).toThrow('Credential subject must have an encodedList.'); }); + it('should throw if empty encodedList', () => { expect(() => - _checkCredentialSubjectForStatusList2021Credential({ - type: 'StatusList2021', + testFunction({ + type: expectedType, statusPurpose: 'revocation', encodedList: '', } as any), - ).toThrowErrorMatchingInlineSnapshot( - `[Error: Credential subject must have a non-empty encodedList.]`, - ); + ).toThrow('Credential subject must have a non-empty encodedList.'); + }); + + it('should not throw for valid credential subject with revocation', () => { + expect(() => + testFunction({ + type: expectedType, + statusPurpose: 'revocation', + encodedList: 'H4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA', + }), + ).not.toThrow(); + }); + + it('should not throw for valid credential subject with suspension', () => { + expect(() => + testFunction({ + type: expectedType, + statusPurpose: 'suspension', + encodedList: 'H4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA', + }), + ).not.toThrow(); }); }); }); diff --git a/packages/w3c-credential-status/src/lib/BitstringStatusList/assertions.ts b/packages/w3c-credential-status/src/lib/BitstringStatusList/assertions.ts index 269fae6d..b1c8e26e 100644 --- a/packages/w3c-credential-status/src/lib/BitstringStatusList/assertions.ts +++ b/packages/w3c-credential-status/src/lib/BitstringStatusList/assertions.ts @@ -76,12 +76,16 @@ export const assertStatusListIndexWithinRange = ( }; /** - * Check if the credential subject is valid for a StatusList2021Credential. + * Check if the credential subject is valid for the given credential type. * @param credentialSubject - The credential subject to be signed. + * @param expectedType - The expected credential subject type. + * @param credentialType - The credential type for error messages. * @throws {Error} - Throws an error if the credential subject is invalid. */ -export function _checkCredentialSubjectForStatusList2021Credential( +export function _checkCredentialSubjectForStatusListCredential( credentialSubject: VCBitstringCredentialSubject, + expectedType: 'StatusList2021' | 'BitstringStatusList', + credentialType: 'StatusList2021Credential' | 'BitstringStatusListCredential', ): void { // Check if credentialSubject is an object if (!credentialSubject) { @@ -93,9 +97,9 @@ export function _checkCredentialSubjectForStatusList2021Credential( throw new Error('Credential subject must have a type.'); } // Check if credentialSubject has the correct type - if (credentialSubject.type !== 'StatusList2021') { + if (credentialSubject.type !== expectedType) { throw new Error( - 'Invalid type for credentialSubject: Credential subject for StatusList2021Credential must have type "StatusList2021".', + `Invalid type for credentialSubject: Credential subject for ${credentialType} must have type "${expectedType}".`, ); } @@ -114,3 +118,33 @@ export function _checkCredentialSubjectForStatusList2021Credential( throw new Error('Credential subject must have a non-empty encodedList.'); } } + +/** + * Check if the credential subject is valid for a StatusList2021Credential. + * @param credentialSubject - The credential subject to be signed. + * @throws {Error} - Throws an error if the credential subject is invalid. + */ +export function _checkCredentialSubjectForStatusList2021Credential( + credentialSubject: VCBitstringCredentialSubject, +): void { + _checkCredentialSubjectForStatusListCredential( + credentialSubject, + 'StatusList2021', + 'StatusList2021Credential', + ); +} + +/** + * Check if the credential subject is valid for a BitstringStatusListCredential. + * @param credentialSubject - The credential subject to be signed. + * @throws {Error} - Throws an error if the credential subject is invalid. + */ +export function _checkCredentialSubjectForBitstringStatusListCredential( + credentialSubject: VCBitstringCredentialSubject, +): void { + _checkCredentialSubjectForStatusListCredential( + credentialSubject, + 'BitstringStatusList', + 'BitstringStatusListCredential', + ); +} diff --git a/packages/w3c-credential-status/src/lib/index.test.ts b/packages/w3c-credential-status/src/lib/index.test.ts index 091bba80..670d17e7 100644 --- a/packages/w3c-credential-status/src/lib/index.test.ts +++ b/packages/w3c-credential-status/src/lib/index.test.ts @@ -1,20 +1,65 @@ -import { PrivateKeyPair, VerificationType } from '@trustvc/w3c-issuer'; +import { + BBSPrivateKeyPair, + EcdsaSd2023PrivateKeyPair, + Bbs2023PrivateKeyPair, + VerificationType, +} from '@trustvc/w3c-issuer'; import { describe, expect, it } from 'vitest'; import { createCredentialStatusPayload } from './index'; -const PRIVATE_KEY_PAIR: PrivateKeyPair = { +const BLS_KEY_PAIR: BBSPrivateKeyPair = { id: 'did:web:trustvc.github.io:did:1#keys-1', - type: 'Bls12381G2Key2020' as VerificationType, + type: VerificationType.Bls12381G2Key2020, controller: 'did:web:trustvc.github.io:did:1', seedBase58: 'GWP69tmSWJjqC1RoJ27FehcVqkVyeYAz6h5ABwoNSNdS', - privateKeyBase58: '4LDU56PUhA9ZEutnR1qCWQnUhtLtpLu2EHSq4h1o7vtF', + privateKeyBase58: 'privateKeyBase58', publicKeyBase58: 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ', }; +const ECDSA_SD_KEY_PAIR: EcdsaSd2023PrivateKeyPair = { + '@context': 'https://w3id.org/security/multikey/v1', + id: 'did:web:trustvc.github.io:did:1#multikey-1', + type: VerificationType.Multikey, + controller: 'did:web:trustvc.github.io:did:1', + publicKeyMultibase: 'zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc', + secretKeyMultibase: 'secretKeyMultibase', +}; + +const BBS2023_KEY_PAIR: Bbs2023PrivateKeyPair = { + '@context': 'https://w3id.org/security/multikey/v1', + id: 'did:web:trustvc.github.io:did:1#multikey-2', + type: VerificationType.Multikey, + controller: 'did:web:trustvc.github.io:did:1', + publicKeyMultibase: + 'zUC75kRac7BdtjawFUxowfgD6mzqnRHFxAfMDaBynebdYgakviQkPS1KNJEw7uGWqj91H3hSE4pTERb3EZKLgKXjpqHWrN8dyE8SKyPBE3k7kUGjBNAqJoNGgUzqUW3DSaWrcNr', + secretKeyMultibase: 'secretKeyMultibase', +}; + describe('w3c-credential-status', () => { describe('createCredentialStatusVC', () => { - it('should create a credential status VC successfully', async () => { + it('Should throw error when trying to create new VC with BLS cryptosuite', async () => { + await expect( + createCredentialStatusPayload( + { + id: 'https://example.com/credentials/3732', + credentialSubject: { + type: 'BitstringStatusList', + id: 'https://example.com/credentials/status/3#list', + statusPurpose: 'revocation', + encodedList: 'encodedList', + }, + }, + BLS_KEY_PAIR, + 'BitstringStatusListCredential', + 'BbsBlsSignature2020', + ), + ).rejects.toThrowError( + 'BbsBlsSignature2020 is no longer supported. Please use the latest cryptosuite versions instead.', + ); + }); + + it('should create a credential status VC with ECDSA-SD-2023 and v1.1 context', async () => { const credentialStatusPayload = await createCredentialStatusPayload( { id: 'https://example.com/credentials/3732', @@ -25,13 +70,15 @@ describe('w3c-credential-status', () => { encodedList: 'encodedList', }, }, - PRIVATE_KEY_PAIR, + ECDSA_SD_KEY_PAIR, + 'StatusList2021Credential', + 'ecdsa-sd-2023', ); expect(credentialStatusPayload).toMatchObject({ '@context': [ 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/security/bbs/v1', + 'https://w3id.org/security/data-integrity/v2', 'https://w3id.org/vc/status-list/2021/v1', ], credentialSubject: { @@ -47,13 +94,85 @@ describe('w3c-credential-status', () => { }); }); + it('should create a credential status VC with ECDSA-SD-2023 and v2.0 context', async () => { + const credentialStatusPayload = await createCredentialStatusPayload( + { + id: 'https://example.com/credentials/3732', + credentialSubject: { + type: 'BitstringStatusList', + id: 'https://example.com/credentials/status/3#list', + statusPurpose: 'revocation', + encodedList: 'encodedList', + }, + }, + ECDSA_SD_KEY_PAIR, + 'BitstringStatusListCredential', + 'ecdsa-sd-2023', + ); + + expect(credentialStatusPayload).toMatchObject({ + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/data-integrity/v2', + ], + credentialSubject: { + encodedList: 'encodedList', + id: 'https://example.com/credentials/status/3#list', + statusPurpose: 'revocation', + type: 'BitstringStatusList', + }, + issuer: 'did:web:trustvc.github.io:did:1', + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + validFrom: expect.any(String), + }); + + // Explicitly verify that issuanceDate is not present in v2.0 + expect(credentialStatusPayload).not.toHaveProperty('issuanceDate'); + }); + + it('should create a credential status VC with BBS-2023 and v2.0 context', async () => { + const credentialStatusPayload = await createCredentialStatusPayload( + { + id: 'https://example.com/credentials/3732', + credentialSubject: { + type: 'BitstringStatusList', + id: 'https://example.com/credentials/status/3#list', + statusPurpose: 'revocation', + encodedList: 'encodedList', + }, + }, + BBS2023_KEY_PAIR, + 'BitstringStatusListCredential', + 'bbs-2023', + ); + + expect(credentialStatusPayload).toMatchObject({ + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/data-integrity/v2', + ], + credentialSubject: { + encodedList: 'encodedList', + id: 'https://example.com/credentials/status/3#list', + statusPurpose: 'revocation', + type: 'BitstringStatusList', + }, + issuer: 'did:web:trustvc.github.io:did:1', + type: ['VerifiableCredential', 'BitstringStatusListCredential'], + validFrom: expect.any(String), + }); + + // Explicitly verify that issuanceDate is not present in v2.0 + expect(credentialStatusPayload).not.toHaveProperty('issuanceDate'); + }); + it('should return an error if type is not supported', async () => { expect( createCredentialStatusPayload( { id: 'https://example.com/credentials/3732', } as any, - PRIVATE_KEY_PAIR, + BLS_KEY_PAIR, 'unsupported' as any, ), ).rejects.toThrowError('Unsupported type: unsupported'); diff --git a/packages/w3c-credential-status/src/lib/index.ts b/packages/w3c-credential-status/src/lib/index.ts index f93556f0..da4e993c 100644 --- a/packages/w3c-credential-status/src/lib/index.ts +++ b/packages/w3c-credential-status/src/lib/index.ts @@ -1,16 +1,20 @@ import { - BBS_V1_URL, CredentialContextVersion, + DATA_INTEGRITY_V2_URL, STATUS_LIST_2021_CREDENTIAL_URL, } from '@trustvc/w3c-context'; import { PrivateKeyPair } from '@trustvc/w3c-issuer'; -import { _checkCredentialSubjectForStatusList2021Credential } from './BitstringStatusList/assertions'; +import { + _checkCredentialSubjectForStatusList2021Credential, + _checkCredentialSubjectForBitstringStatusListCredential, +} from './BitstringStatusList/assertions'; import { VCBitstringCredentialSubjectType, VCCredentialStatusType, } from './BitstringStatusList/types'; import { CreateVCCredentialStatusOptions, + CryptoSuiteName, GeneralCredentialStatus, RawCredentialStatusVC, } from './types'; @@ -33,23 +37,32 @@ export const VCCredentialStatusTypeToVCCredentialSubjectType: Record< * @param {string} options.id - The ID of the credential. * @param {object} options.credentialSubject - The credential subject. * @param {PrivateKeyPair} keyPair - The key pair options for signing. - * @param {VCCredentialStatusType} type - The type of the credential status VC. Defaults to 'StatusList2021Credential'. - * @param {string} cryptoSuite - The cryptosuite to be used for signing. Defaults to 'BbsBlsSignature2020'. + * @param {VCCredentialStatusType} type - The type of the credential status VC. Defaults to 'BitstringStatusListCredential'. + * @param {CryptoSuiteName} cryptoSuite - The cryptosuite to be used for signing. Defaults to 'ecdsa-sd-2023'. * @returns {Promise} */ export const createCredentialStatusPayload = async ( options: CreateVCCredentialStatusOptions, keyPair: PrivateKeyPair, - type: VCCredentialStatusType = 'StatusList2021Credential', - cryptoSuite = 'BbsBlsSignature2020', + type: VCCredentialStatusType = 'BitstringStatusListCredential', + cryptoSuite: CryptoSuiteName = 'ecdsa-sd-2023', ): Promise => { try { const { id, credentialSubject } = options; + if (cryptoSuite === 'BbsBlsSignature2020') { + throw new Error( + 'BbsBlsSignature2020 is no longer supported. Please use the latest cryptosuite versions instead.', + ); + } + switch (type) { case 'StatusList2021Credential': _checkCredentialSubjectForStatusList2021Credential(credentialSubject); break; + case 'BitstringStatusListCredential': + _checkCredentialSubjectForBitstringStatusListCredential(credentialSubject); + break; default: throw new Error(`Unsupported type: ${type}`); } @@ -58,12 +71,12 @@ export const createCredentialStatusPayload = async ( credentialSubject.id = `${id}#list`; } - const context = [CredentialContextVersion.v1]; - - if (cryptoSuite === 'BbsBlsSignature2020') { - context.push(BBS_V1_URL); - } + // Determine version based on credential type + const isV2 = type === 'BitstringStatusListCredential'; + const context = [isV2 ? CredentialContextVersion.v2 : CredentialContextVersion.v1]; + context.push(DATA_INTEGRITY_V2_URL); + // Add status list context only for v1.1 (v2.0 has it built-in) if (type === 'StatusList2021Credential') { context.push(STATUS_LIST_2021_CREDENTIAL_URL); } @@ -74,8 +87,12 @@ export const createCredentialStatusPayload = async ( '@context': context, type: ['VerifiableCredential', type], issuer: keyPair.controller, - issuanceDate: new Date().toISOString(), - validFrom, + ...(isV2 + ? { validFrom } + : { + issuanceDate: new Date().toISOString(), + validFrom, + }), credentialSubject, }; @@ -89,11 +106,13 @@ export const createCredentialStatusPayload = async ( }; /** - * Checks if the input credential status is a StatusList2021Credential. + * Checks if the input credential status is a StatusList2021Credential / BitstringStatusListCredential. * @param {GeneralCredentialStatus} credentialStatus - The credential status to be checked. - * @returns {boolean} - Returns true if the credential status is a StatusList2021Credential, false otherwise. + * @returns {boolean} - Returns true if the credential status is a StatusList2021Credential / BitstringStatusListCredential, false otherwise. */ -export const isCredentialStatusStatusList = (credentialStatus: GeneralCredentialStatus) => { +export const isCredentialStatusStatusList = ( + credentialStatus: GeneralCredentialStatus, +): boolean => { try { assertCredentialStatusStatusListType(credentialStatus?.type); return true; diff --git a/packages/w3c-credential-status/src/lib/types.ts b/packages/w3c-credential-status/src/lib/types.ts index 8e43de21..08d4df1d 100644 --- a/packages/w3c-credential-status/src/lib/types.ts +++ b/packages/w3c-credential-status/src/lib/types.ts @@ -44,7 +44,7 @@ export type RawCredentialStatusVC = { '@context': string | string[]; type: string[]; issuer: string | Record; - issuanceDate: string; + issuanceDate?: string; validFrom?: string; validUntil?: string; expirationDate?: string; @@ -60,3 +60,5 @@ export type SignedCredentialStatusVC = RawCredentialStatusVC & { proofValue: string; }; }; + +export type CryptoSuiteName = 'BbsBlsSignature2020' | 'bbs-2023' | 'ecdsa-sd-2023'; diff --git a/packages/w3c-credential-status/src/lib/utils.test.ts b/packages/w3c-credential-status/src/lib/utils.test.ts index 374e9d61..c47f90d9 100644 --- a/packages/w3c-credential-status/src/lib/utils.test.ts +++ b/packages/w3c-credential-status/src/lib/utils.test.ts @@ -89,7 +89,7 @@ describe('utils.ts', () => { it('should throw an error if the VC is not found', async () => { expect( - fetchCredentialStatusVC('https://trustvc.github.io/did/credentials/statuslist/2'), + fetchCredentialStatusVC('https://trustvc.github.io/did/credentials/statuslist/3'), ).rejects.toThrowError('Credential Status VC not found'); }); diff --git a/packages/w3c-credential-status/src/lib/utils.ts b/packages/w3c-credential-status/src/lib/utils.ts index ad8af4a3..983a0e64 100644 --- a/packages/w3c-credential-status/src/lib/utils.ts +++ b/packages/w3c-credential-status/src/lib/utils.ts @@ -20,7 +20,11 @@ import { * @throws {Error} - Throws an error if the type is not supported. */ export const assertCredentialStatusType = (type: T): void => { - const supportedTypes: T[] = ['StatusList2021Entry', 'TransferableRecords'] as T[]; + const supportedTypes: T[] = [ + 'StatusList2021Entry', + 'BitstringStatusListEntry', + 'TransferableRecords', + ] as T[]; if (!supportedTypes.includes(type)) { throw new Error(`Unsupported type: ${type}`); @@ -33,7 +37,7 @@ export const assertCredentialStatusType = (type: T): void => { * @throws {Error} - Throws an error if the type is not supported. */ export const assertCredentialStatusStatusListType = (type: T): void => { - const supportedTypes: T[] = ['StatusList2021Entry'] as T[]; + const supportedTypes: T[] = ['StatusList2021Entry', 'BitstringStatusListEntry'] as T[]; if (!supportedTypes.includes(type)) { throw new Error(`Unsupported type: ${type}`); @@ -75,6 +79,9 @@ export const assertCredentialStatus = (credentialStatus: GeneralCredentialStatus case 'StatusList2021Entry': assertStatusList2021Entry(credentialStatus as BitstringStatusListCredentialStatus); break; + case 'BitstringStatusListEntry': + assertBitstringStatusListEntry(credentialStatus as BitstringStatusListCredentialStatus); + break; default: throw new Error(`Unsupported type: ${credentialStatus.type}`); } @@ -103,7 +110,7 @@ export function _validateUriId({ id, propertyName }: { id: string; propertyName: } } -export const assertStatusList2021Entry = ( +const _assertStatusListCredentialStatus = ( credentialStatus: BitstringStatusListCredentialStatus, ): void => { const { type, statusPurpose, statusListIndex, statusListCredential } = credentialStatus; @@ -116,6 +123,9 @@ export const assertStatusList2021Entry = ( }); }; +export const assertStatusList2021Entry = _assertStatusListCredentialStatus; +export const assertBitstringStatusListEntry = _assertStatusListCredentialStatus; + export const assertTransferableRecords = ( credentialStatus: TransferableRecordsCredentialStatus, mode: 'sign' | 'verify' = 'verify', diff --git a/packages/w3c-credential-status/tsconfig.json b/packages/w3c-credential-status/tsconfig.json index d19c6f7a..7de45906 100644 --- a/packages/w3c-credential-status/tsconfig.json +++ b/packages/w3c-credential-status/tsconfig.json @@ -2,5 +2,5 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { }, - "include": ["src/**/*"] + "include": ["src/**/*", "../declaration.d.ts"] } diff --git a/packages/w3c-issuer/README.md b/packages/w3c-issuer/README.md index 59dbadc5..6fb47e66 100644 --- a/packages/w3c-issuer/README.md +++ b/packages/w3c-issuer/README.md @@ -10,7 +10,7 @@ npm install @trustvc/w3c-issuer ``` ## Features -- Create private key pairs for specific [Signature Suites](https://w3c-ccg.github.io/ld-cryptosuite-registry/) used for signing Verifiable Credentials (e.g., BBS). +- Create private key pairs for specific signature suites used for signing Verifiable Credentials: [ECDSA-SD-2023](https://w3c.github.io/vc-di-ecdsa/#ecdsa-sd-2023), [BBS-2023](https://w3c.github.io/vc-di-bbs/#bbs-2023), and [legacy suites](https://w3c-ccg.github.io/ld-cryptosuite-registry/). - Generate DID private key pairs and DID documents.
@@ -23,26 +23,30 @@ ________________________________ `generateKeyPair` function helps to generate a signature Key Pair. ```ts -import { generateKeyPair, VerificationType } from '@trustvc/w3c-issuer'; +import { generateKeyPair, CryptoSuite } from '@trustvc/w3c-issuer'; /** * Parameters: * - options (GenerateKeyPairOptions) - * - options.type (VerificationType): Key Pair algo type for Signature + * - options.type (CryptoSuite | VerificationType): Key Pair algo type for Signature * - options.seedBase58? (string): 32 byte base58 encoded seed (e.g. FVj12jBiBUqYFaEUkTuwAD73p9Hx5NzCJBge74nTguQN) (optional) * - options.privateKeyBase58? (string): private key value (optional) * - options.publicKeyBase58? (string): public key value (optional) + * - options.secretKeyMultibase? (string): private key value in multibase format (optional) + * - options.publicKeyMultibase? (string): public key value in multibase format (optional) * * Returns: * - A Promise that resolves to: - * - generatedKeyPair.type (VerificationType): Key Pair algo. - * - generatedKeyPair.seedBase58 (string): 32 byte base58 encoded seed - * - generatedKeyPair.privateKeyBase58 (string): Derieve private key from seed - * - generatedKeyPair.publicKeyBase58 (string): Detrieve public key from seed + * - generatedKeyPair.type (CryptoSuite | VerificationType): Key Pair algo. + * - generatedKeyPair.secretKeyMultibase (string): Private key in multibase format (for modern cryptosuites) + * - generatedKeyPair.publicKeyMultibase (string): Public key in multibase format (for modern cryptosuites) + * - generatedKeyPair.seedBase58 (string): 32 byte base58 encoded seed (for legacy types only) + * - generatedKeyPair.privateKeyBase58 (string): Private key in base58 format (for legacy types only) + * - generatedKeyPair.publicKeyBase58 (string): Public key in base58 format (for legacy types only) */ const options = { - type: VerificationType.Bls12381G2Key2020, + type: CryptoSuite.EcdsaSd2023, seedBase58: undefined } @@ -55,10 +59,9 @@ console.log('generatedKeyPair: ', generatedKeyPair) ```js generatedKeyPair: { - type: 'Bls12381G2Key2020', - seedBase58: '', - privateKeyBase58: '', + publicKeyMultibase: '' } ```
@@ -74,16 +77,18 @@ console.log('generatedKeyPair: ', generatedKeyPair) *Read [here]() for more signing instructions.* ```ts -import { VerificationType, issueDID } from '@trustvc/w3c-issuer'; +import { CryptoSuite, issueDID } from '@trustvc/w3c-issuer'; /** * Parameters: * - options (IssuedDIDOption) * - options.domain (string): URL where the DID Document will be located - * - options.type (VerificationType): Key Pair algo. - * - options.seedBase58? (string): 32 byte base58 encoded seed (optional) - * - options.privateKeyBase58? (string): Derieved private key from seed (optional) - * - options.publicKeyBase58? (string): Detrieved public key from seed (optional) + * - options.type (CryptoSuite | VerificationType): Key Pair algo. + * - options.secretKeyMultibase? (string): Private key in multibase format (optional, for modern cryptosuites) + * - options.publicKeyMultibase? (string): Public key in multibase format (optional, for modern cryptosuites) + * - options.seedBase58? (string): 32 byte base58 encoded seed (optional, for legacy types) + * - options.privateKeyBase58? (string): Private key in base58 format (optional, for legacy types) + * - options.publicKeyBase58? (string): Public key in base58 format (optional, for legacy types) * * Returns: * - A Promise that resolves to: @@ -93,10 +98,9 @@ import { VerificationType, issueDID } from '@trustvc/w3c-issuer'; const options = { domain: 'https://example.com/.well-known/did.json', - type: VerificationType.Bls12381G2Key2020, - seedBase58: '', - privateKeyBase58: '', - publicKeyBase58: 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ' + type: CryptoSuite.EcdsaSd2023, + secretKeyMultibase: '', + publicKeyMultibase: '' } const issuedDID = await issueDID(options); @@ -113,28 +117,28 @@ console.log("didKeyPairs:", didKeyPairs) id: 'did:web:example.com', verificationMethod: [ { - type: 'Bls12381G2Key2020', - id: 'did:web:example.com#keys-1', + type: 'Multikey', + id: 'did:web:example.com#multikey-1', controller: 'did:web:example.com', - publicKeyBase58: 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ' + publicKeyMultibase: '' } ], '@context': [ 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/bls12381-2020/v1' + 'https://w3id.org/security/multikey/v1' ], - authentication: [ 'did:web:example.com#keys-1' ], - assertionMethod: [ 'did:web:example.com#keys-1' ], - capabilityInvocation: [ 'did:web:example.com#keys-1' ], - capabilityDelegation: [ 'did:web:example.com#keys-1' ] + authentication: [ 'did:web:example.com#multikey-1' ], + assertionMethod: [ 'did:web:example.com#multikey-1' ], + capabilityInvocation: [ 'did:web:example.com#multikey-1' ], + capabilityDelegation: [ 'did:web:example.com#multikey-1' ] } didKeyPairs: { - id: 'did:web:example.com#keys-1', - type: 'Bls12381G2Key2020', + '@context': 'https://w3id.org/security/multikey/v1', + id: 'did:web:example.com#multikey-1', + type: 'Multikey', controller: 'did:web:example.com', - seedBase58: '', - privateKeyBase58: '', - publicKeyBase58: 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ' + secretKeyMultibase: '', + publicKeyMultibase: '' } ``` diff --git a/packages/w3c-issuer/package.json b/packages/w3c-issuer/package.json index c4e6a54f..ec75adc7 100644 --- a/packages/w3c-issuer/package.json +++ b/packages/w3c-issuer/package.json @@ -1,6 +1,6 @@ { "name": "@trustvc/w3c-issuer", - "version": "1.2.4", + "version": "1.3.0-alpha.10", "description": "", "main": "dist/index.js", "module": "dist/esm/index.js", @@ -29,6 +29,8 @@ } }, "dependencies": { + "@digitalbazaar/bls12-381-multikey": "^2.1.0", + "@digitalbazaar/ecdsa-multikey": "^1.8.0", "@mattrglobal/bls12381-key-pair": "^1.2.1", "bip39": "^3.1.0", "did-resolver": "^4.1.0", diff --git a/packages/w3c-issuer/src/did-web/README.md b/packages/w3c-issuer/src/did-web/README.md index bd380a4d..00b1023b 100644 --- a/packages/w3c-issuer/src/did-web/README.md +++ b/packages/w3c-issuer/src/did-web/README.md @@ -21,22 +21,27 @@ This guide explains how to self-host a Decentralized Identifier (DID) using the ## Step-by-Step Setup ### 1. Generate DID Document - **Use [`TrustVC W3C Issuer`](../../README.md) to generate your DID Document**: Our tool simplifies the process of creating a compliant DID Document. -- **Review your DID Document**: Ensure the generated file contains the required properties, such as `id`, `verificationMethod`, and `authentication`. Here's an example using the `BbsBlsSignature2020` verification method: +- **Review your DID Document**: Ensure the generated file contains the required properties, such as `id`, `verificationMethod`, and `authentication`. Here's an example using the modern `Multikey` verification method with ECDSA-SD-2023: ```json { - "@context": "https://www.w3.org/ns/did/v1", + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1" + ], "id": "did:web:yourdomain.com", "verificationMethod": [{ "id": "did:web:yourdomain.com#key-1", - "type": "BbsBlsSignature2020", + "type": "Multikey", "controller": "did:web:yourdomain.com", - "publicKeyBase58": "2qz8jVcPgs6xzL5mZHTZGkXQaDe5BsVofLpqqBfAw1Nc" + "publicKeyMultibase": "zDnaekGZTbQBerwcehBSXLqAg6s55hVEBms1zFy89VHXtJSa9" }], "authentication": [ "did:web:yourdomain.com#key-1" + ], + "assertionMethod": [ + "did:web:yourdomain.com#key-1" ] } - ``` - **Save the document** as `did.json`. ### 2. Host DID Document diff --git a/packages/w3c-issuer/src/did-web/index.ts b/packages/w3c-issuer/src/did-web/index.ts index a5c7ec2d..d4e21575 100644 --- a/packages/w3c-issuer/src/did-web/index.ts +++ b/packages/w3c-issuer/src/did-web/index.ts @@ -1,7 +1,7 @@ import { generateKeyPair } from './keyPair'; import { issueDID } from './wellKnown'; -import { queryDidDocument } from './wellKnown/query'; +import { queryDidDocument, resolve, resolveRepresentation, dereference } from './wellKnown/query'; export * from './keyPair/types'; export * from './wellKnown/types'; -export { generateKeyPair, issueDID, queryDidDocument }; +export { generateKeyPair, issueDID, queryDidDocument, resolve, resolveRepresentation, dereference }; diff --git a/packages/w3c-issuer/src/did-web/keyPair/bbs2023.ts b/packages/w3c-issuer/src/did-web/keyPair/bbs2023.ts new file mode 100644 index 00000000..bc2a3d1f --- /dev/null +++ b/packages/w3c-issuer/src/did-web/keyPair/bbs2023.ts @@ -0,0 +1,39 @@ +import * as Bls12381Multikey from '@digitalbazaar/bls12-381-multikey'; +import { base58btc } from 'multiformats/bases/base58'; +import { DidWebGeneratedKeyPair, DidWebGenerateKeyPairOptions } from './types'; +import { VerificationType } from '../../lib/types'; + +/** + * Generate BBS-2023 key pair using Bls12381Multikey. + * + * @param {DidWebGenerateKeyPairOptions} options - Options for key pair generation + * @returns {Promise} - Generated BBS-2023 key pair + */ +export const generateBbs2023KeyPair = async ({ + seed, +}: DidWebGenerateKeyPairOptions): Promise => { + if (!seed) { + throw new Error('Invalid seed'); + } + + // Generate BBS key pair using the new BBS-2023 algorithm + const bbsKeyPair = await Bls12381Multikey.generateBbsKeyPair({ + algorithm: 'BBS-BLS12-381-SHA-256', + seed, + }); + + // Export the key pair to get multibase-encoded keys + const exportedKeys = await bbsKeyPair.export({ + publicKey: true, + secretKey: true, + }); + + const keyPair: DidWebGeneratedKeyPair = { + type: VerificationType.Multikey, + seedBase58: base58btc.encode(seed).slice(1), + secretKeyMultibase: exportedKeys.secretKeyMultibase, + publicKeyMultibase: exportedKeys.publicKeyMultibase, + }; + + return keyPair; +}; diff --git a/packages/w3c-issuer/src/did-web/keyPair/ecdsaSd2023.ts b/packages/w3c-issuer/src/did-web/keyPair/ecdsaSd2023.ts new file mode 100644 index 00000000..d41126d0 --- /dev/null +++ b/packages/w3c-issuer/src/did-web/keyPair/ecdsaSd2023.ts @@ -0,0 +1,31 @@ +import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey'; +import { DidWebGeneratedKeyPair } from './types'; +import { VerificationType } from '../../lib/types'; + +/** + * Generate ECDSA-SD-2023 key pair using EcdsaMultikey. + * Note: ECDSA key generation does not support deterministic seed-based generation. + * Keys are generated randomly using secure cryptographic methods. + * + * @returns {Promise} - Generated ECDSA-SD-2023 key pair + */ +export const generateEcdsaSd2023KeyPair = async (): Promise => { + // Generate ECDSA key pair using P-256 curve for ECDSA-SD-2023 + const ecdsaKeyPair = await EcdsaMultikey.generate({ + curve: 'P-256', + }); + + // Export the key pair to get multibase-encoded keys + const exportedKeys = await ecdsaKeyPair.export({ + publicKey: true, + secretKey: true, + }); + + const keyPair: DidWebGeneratedKeyPair = { + type: VerificationType.Multikey, + secretKeyMultibase: exportedKeys.secretKeyMultibase, + publicKeyMultibase: exportedKeys.publicKeyMultibase, + }; + + return keyPair; +}; diff --git a/packages/w3c-issuer/src/did-web/keyPair/index.test.ts b/packages/w3c-issuer/src/did-web/keyPair/index.test.ts index e960cb27..fa944124 100644 --- a/packages/w3c-issuer/src/did-web/keyPair/index.test.ts +++ b/packages/w3c-issuer/src/did-web/keyPair/index.test.ts @@ -1,7 +1,8 @@ import { omit } from 'lodash'; import { describe, expect, it } from 'vitest'; import { generateKeyPair } from './index'; -import { VerificationType } from '../../lib/types'; +import { CryptoSuite, VerificationType } from '../../lib/types'; + describe('keyPair', () => { const seedBase58 = 'CxBwAH4ftdc9XkLhw7DkFAESxh3NEdetMyJXKrPiAKAX'; const privateKeyBase58 = '7e1VnFeqvMjqoq61qhGE2dgnQmgNDAYEX1FGzwywf2h7'; @@ -10,7 +11,7 @@ describe('keyPair', () => { const invalidPublicKeyBase58 = 'invalidPublicKeyBase58'; const invalidPrivateKeyBase58 = 'invalidPrivateKeyBase58'; - describe('generateKeyPair', () => { + describe('Legacy BLS12-381 Key Generation', () => { it('should fail to generateKeyPair without any input', async () => { await expect(generateKeyPair({} as any)).rejects.toThrowError('Invalid key pair type'); }); @@ -116,4 +117,82 @@ describe('keyPair', () => { ).rejects.toThrowError('Public key does not match'); }); }); + + describe('BBS2023 Key Generation', () => { + it('should generateKeyPair with BBS2023 type', async () => { + const keyPair = await generateKeyPair({ type: CryptoSuite.Bbs2023 }); + expect(keyPair.type).toBe('Multikey'); + expect(typeof keyPair.secretKeyMultibase).toBe('string'); + expect(keyPair.secretKeyMultibase?.length).toBeGreaterThan(1); + expect(typeof keyPair.publicKeyMultibase).toBe('string'); + expect(keyPair.publicKeyMultibase?.length).toBeGreaterThan(1); + // BBS-2023 keys should be multibase encoded (BLS12-381 format) + expect(keyPair.secretKeyMultibase).toMatch(/^z/); + expect(keyPair.publicKeyMultibase).toMatch(/^zUC/); + }); + + it('should generateKeyPair with BBS2023 type and seed', async () => { + const keyPair = await generateKeyPair({ + type: CryptoSuite.Bbs2023, + seedBase58, + }); + + expect(keyPair.type).toBe('Multikey'); + expect(keyPair.seedBase58).toBe(seedBase58); + expect(typeof keyPair.secretKeyMultibase).toBe('string'); + expect(keyPair.secretKeyMultibase?.length).toBeGreaterThan(1); + expect(typeof keyPair.publicKeyMultibase).toBe('string'); + expect(keyPair.publicKeyMultibase?.length).toBeGreaterThan(1); + // BBS-2023 keys should be multibase encoded (BLS12-381 format) + expect(keyPair.secretKeyMultibase).toMatch(/^z/); + expect(keyPair.publicKeyMultibase).toMatch(/^zUC/); + }); + + it('should generate deterministic BBS2023 keys with same seed', async () => { + const keyPair1 = await generateKeyPair({ + type: CryptoSuite.Bbs2023, + seedBase58, + }); + + const keyPair2 = await generateKeyPair({ + type: CryptoSuite.Bbs2023, + seedBase58, + }); + + expect(keyPair1.secretKeyMultibase).toBe(keyPair2.secretKeyMultibase); + expect(keyPair1.publicKeyMultibase).toBe(keyPair2.publicKeyMultibase); + expect(keyPair1.seedBase58).toBe(keyPair2.seedBase58); + }); + }); + + describe('ECDSASD2023 Key Generation', () => { + it('should generate ECDSASD2023 key pair without seed', async () => { + const keyPair = await generateKeyPair({ + type: CryptoSuite.EcdsaSd2023, + }); + + expect(keyPair.type).toBe('Multikey'); + expect(typeof keyPair.secretKeyMultibase).toBe('string'); + expect(keyPair.secretKeyMultibase?.length).toBeGreaterThan(1); + expect(typeof keyPair.publicKeyMultibase).toBe('string'); + expect(keyPair.publicKeyMultibase?.length).toBeGreaterThan(1); + // Keys should be multibase encoded (start with 'z') + expect(keyPair.secretKeyMultibase).toMatch(/^z/); + expect(keyPair.publicKeyMultibase).toMatch(/^zDn/); + }); + + it('should generate different ECDSASD2023 keys on each call', async () => { + const keyPair1 = await generateKeyPair({ + type: CryptoSuite.EcdsaSd2023, + }); + + const keyPair2 = await generateKeyPair({ + type: CryptoSuite.EcdsaSd2023, + }); + + // ECDSA-SD-2023 keys are random - should be different each time + expect(keyPair1.secretKeyMultibase).not.toBe(keyPair2.secretKeyMultibase); + expect(keyPair1.publicKeyMultibase).not.toBe(keyPair2.publicKeyMultibase); + }); + }); }); diff --git a/packages/w3c-issuer/src/did-web/keyPair/index.ts b/packages/w3c-issuer/src/did-web/keyPair/index.ts index abe36266..605ae15f 100644 --- a/packages/w3c-issuer/src/did-web/keyPair/index.ts +++ b/packages/w3c-issuer/src/did-web/keyPair/index.ts @@ -1,8 +1,32 @@ import crypto from 'crypto'; import { parseMultibase } from '../../lib'; -import { GeneratedKeyPair, GenerateKeyPairOptions, VerificationType } from '../../lib/types'; +import { + CryptoSuite, + GeneratedKeyPair, + GenerateKeyPairOptions, + VerificationType, +} from '../../lib/types'; import { generateBls12381KeyPair } from './bls12381'; import { DidWebGeneratedKeyPair } from './types'; +import { generateBbs2023KeyPair } from './bbs2023'; +import { generateEcdsaSd2023KeyPair } from './ecdsaSd2023'; + +/** + * Generate modern cryptosuite key pair (BBS-2023 or ECDSA-SD-2023) + */ +const generateModernCryptosuite = async ( + type: CryptoSuite, + keyPairOptions: GenerateKeyPairOptions, +): Promise => { + switch (type) { + case CryptoSuite.Bbs2023: + return await generateBbs2023KeyPair(keyPairOptions); + case CryptoSuite.EcdsaSd2023: + return await generateEcdsaSd2023KeyPair(); + default: + throw new Error(`Unsupported modern cryptosuite: ${type}`); + } +}; /** * Generate key pair based on the type. @@ -10,7 +34,7 @@ import { DidWebGeneratedKeyPair } from './types'; * If private key and public key are provided, it will be verified against the generated seed's key pair. * * @param {GenerateKeyPairOptions} keyPairOptions - * @param {VerificationType} keyPairOptions.type - Type of key pair to generate, supported types are Bls12381G2Key2020 and EcdsaSecp256k1RecoveryMethod2020 + * @param {VerificationType} keyPairOptions.type - Type of key pair to generate, supported types are Bls12381G2Key2020, Multikey, and EcdsaSecp256k1RecoveryMethod2020 * * @param {string} keyPairOptions.seedBase58 - Seed in base58 format (optional) * @param {string} keyPairOptions.privateKeyBase58 - Private key in base58 format (optional) @@ -33,7 +57,13 @@ export const generateKeyPair = async ( let seed: Uint8Array | undefined; - if (type !== VerificationType.EcdsaSecp256k1RecoveryMethod2020) { + // Only prepare seed for types that actually support/use seeds + const seedSupportedTypes: (VerificationType | CryptoSuite)[] = [ + VerificationType.Bls12381G2Key2020, // Legacy BLS + CryptoSuite.Bbs2023, // Modern BBS-2023 + ]; + + if (seedSupportedTypes.includes(type)) { if (seedBase58) { seed = await parseMultibase('z' + seedBase58); } @@ -42,18 +72,24 @@ export const generateKeyPair = async ( let generatedKeyPair: GeneratedKeyPair; - switch (type) { - case VerificationType.Ed25519VerificationKey2018: - throw new Error('Unsupported key pair type'); - case VerificationType.Bls12381G2Key2020: - generatedKeyPair = await generateBls12381KeyPair(keyPairOptions); - break; - default: - throw new Error('Unsupported key pair type'); + // Check if it's a modern cryptosuite + if (Object.values(CryptoSuite).includes(type as CryptoSuite)) { + generatedKeyPair = await generateModernCryptosuite(type as CryptoSuite, keyPairOptions); + } else { + // Legacy key pair generation + switch (type) { + case VerificationType.Ed25519VerificationKey2018: + throw new Error('Unsupported key pair type'); + case VerificationType.Bls12381G2Key2020: + generatedKeyPair = await generateBls12381KeyPair(keyPairOptions); + break; + default: + throw new Error('Unsupported key pair type'); + } } - // If seed is provided, check against provided private key and public key - if (seedBase58) { + // If seed is provided, validate against provided keys (legacy format only) + if (seedBase58 && type === VerificationType.Bls12381G2Key2020) { generatedKeyPair = generatedKeyPair as DidWebGeneratedKeyPair; if (privateKeyBase58 && privateKeyBase58 !== generatedKeyPair.privateKeyBase58) { throw new Error('Private key does not match'); diff --git a/packages/w3c-issuer/src/did-web/keyPair/types.ts b/packages/w3c-issuer/src/did-web/keyPair/types.ts index da7dee05..0ff40d91 100644 --- a/packages/w3c-issuer/src/did-web/keyPair/types.ts +++ b/packages/w3c-issuer/src/did-web/keyPair/types.ts @@ -5,11 +5,18 @@ export type DidWebGenerateKeyPairOptions = BaseKeyPair & { seedBase58?: string; privateKeyBase58?: string; publicKeyBase58?: string; + secretKeyMultibase?: string; + publicKeyMultibase?: string; }; export type DidWebGeneratedKeyPair = Pick< DidWebGenerateKeyPairOptions, - 'type' | 'seedBase58' | 'privateKeyBase58' | 'publicKeyBase58' + | 'type' + | 'seedBase58' + | 'privateKeyBase58' + | 'publicKeyBase58' + | 'secretKeyMultibase' + | 'publicKeyMultibase' > & { seed?: Uint8Array; privateKey?: Uint8Array; diff --git a/packages/w3c-issuer/src/did-web/wellKnown/generate.test.ts b/packages/w3c-issuer/src/did-web/wellKnown/generate.test.ts index 71bc3997..3703b086 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/generate.test.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/generate.test.ts @@ -217,6 +217,208 @@ describe('generate', () => { ).toThrowError('KeyPair already exists'); }); }); + + describe('generateWellKnownDid - BBS-2023', () => { + const keyPair = { + id: 'did:web:localhost.com#keys-3', + type: VerificationType.Multikey, + controller: 'did:web:localhost.com', + seedBase58: 'CxBwAH4ftdc9XkLhw7DkFAESxh3NEdetMyJXKrPiAKAX', + secretKeyMultibase: 'z42twTcNeSYcnqg1FLuSFs2bsGH3ZqbRHFmvS9XMsYhjxvHN', + publicKeyMultibase: + 'zUC724qx4TSVn1zuJAjbaPAzwMhNvjHXvJrg36gEtNjQoKSVqZpG7QfVgsUsV2WrNvTB2p41iF2ZtVruDtwjMtZnyHCFGQov', + }; + const verificationMethod = _.omit(keyPair, ['seedBase58', 'secretKeyMultibase']); + + it('should generateWellKnownDid with BBS-2023 keyPair', async () => { + const result = generateWellKnownDid({ + newKeyPair: keyPair, + }); + + expect(result).toBeTruthy(); + expect(result?.id).toBe(keyPair.controller); + expect(result).toHaveProperty('id'); + expect(result).toHaveProperty('verificationMethod'); + expect(result).toHaveProperty('@context'); + expect(result).toHaveProperty('assertionMethod'); + expect(result).toHaveProperty('authentication'); + expect(result).toHaveProperty('capabilityInvocation'); + expect(result).toHaveProperty('capabilityDelegation'); + expect(result?.verificationMethod?.[0]).toMatchObject(verificationMethod); + expect(result?.verificationMethod?.[0].type).toBe('Multikey'); + expect(result?.['@context']).toContain('https://www.w3.org/ns/did/v1'); + expect(result?.['@context']).toContain(VerificationContext[VerificationType.Multikey]); + }); + + it('should generateWellKnownDid with BBS-2023 keyPair and wellKnown', async () => { + const result = generateWellKnownDid({ + wellKnown: wellKnown, + newKeyPair: keyPair, + }); + + expect(result).toBeTruthy(); + expect(result?.id).toBe(keyPair.controller); + expect(result).toHaveProperty('id'); + expect(result).toHaveProperty('verificationMethod'); + expect(result).toHaveProperty('@context'); + expect(result).toHaveProperty('assertionMethod'); + expect(result).toHaveProperty('authentication'); + expect(result).toHaveProperty('capabilityInvocation'); + expect(result).toHaveProperty('capabilityDelegation'); + expect(result?.verificationMethod).toHaveLength(3); + expect(result?.verificationMethod?.[result?.verificationMethod?.length - 1]).toMatchObject( + verificationMethod, + ); + expect(result?.['@context']).toContain('https://www.w3.org/ns/did/v1'); + expect(result?.['@context']).toContain(VerificationContext[VerificationType.Multikey]); + }); + + it('should fail to generateWellKnownDid with same publicKeyMultibase as wellKnown - BBS-2023', async () => { + const existingMultikeyWellKnown = { + ...wellKnown, + verificationMethod: [ + ...(wellKnown.verificationMethod || []), + { + type: 'Multikey', + id: 'did:web:localhost.com#keys-existing', + controller: 'did:web:localhost.com', + publicKeyMultibase: keyPair.publicKeyMultibase, + }, + ], + }; + + const duplicatedKeyPair = { + ...keyPair, + id: 'did:web:localhost.com#keys-4', + }; + + expect(() => + generateWellKnownDid({ + wellKnown: existingMultikeyWellKnown, + newKeyPair: duplicatedKeyPair, + }), + ).toThrowError('KeyPair already exists'); + }); + }); + + describe('generateWellKnownDid - ECDSA-SD-2023', () => { + const keyPair = { + id: 'did:web:localhost.com#keys-3', + type: VerificationType.Multikey, + controller: 'did:web:localhost.com', + secretKeyMultibase: 'z42twTcNeSYcnqg1FLuSFs2bsGH3ZqbRHFmvS9XMsYhjxvHN', + publicKeyMultibase: 'zDnaekGZTbQBerwcehBSXLqAg6s55hVEBms1zFy89VHXtJSa9', + }; + const verificationMethod = _.omit(keyPair, ['secretKeyMultibase']); + + it('should generateWellKnownDid with ECDSA-SD-2023 keyPair', async () => { + const result = generateWellKnownDid({ + newKeyPair: keyPair, + }); + + expect(result).toBeTruthy(); + expect(result?.id).toBe(keyPair.controller); + expect(result).toHaveProperty('id'); + expect(result).toHaveProperty('verificationMethod'); + expect(result).toHaveProperty('@context'); + expect(result).toHaveProperty('assertionMethod'); + expect(result).toHaveProperty('authentication'); + expect(result).toHaveProperty('capabilityInvocation'); + expect(result).toHaveProperty('capabilityDelegation'); + expect(result?.verificationMethod?.[0]).toMatchObject(verificationMethod); + expect(result?.verificationMethod?.[0].type).toBe('Multikey'); + expect(result?.['@context']).toContain('https://www.w3.org/ns/did/v1'); + expect(result?.['@context']).toContain(VerificationContext[VerificationType.Multikey]); + }); + + it('should generateWellKnownDid with ECDSA-SD-2023 keyPair and wellKnown', async () => { + const result = generateWellKnownDid({ + wellKnown: wellKnown, + newKeyPair: keyPair, + }); + + expect(result).toBeTruthy(); + expect(result?.id).toBe(keyPair.controller); + expect(result).toHaveProperty('id'); + expect(result).toHaveProperty('verificationMethod'); + expect(result).toHaveProperty('@context'); + expect(result).toHaveProperty('assertionMethod'); + expect(result).toHaveProperty('authentication'); + expect(result).toHaveProperty('capabilityInvocation'); + expect(result).toHaveProperty('capabilityDelegation'); + expect(result?.verificationMethod).toHaveLength(3); + expect(result?.verificationMethod?.[result?.verificationMethod?.length - 1]).toMatchObject( + verificationMethod, + ); + expect(result?.['@context']).toContain('https://www.w3.org/ns/did/v1'); + expect(result?.['@context']).toContain(VerificationContext[VerificationType.Multikey]); + }); + + it('should fail to generateWellKnownDid with same publicKeyMultibase as wellKnown - ECDSA-SD-2023', async () => { + const existingMultikeyWellKnown = { + ...wellKnown, + verificationMethod: [ + ...(wellKnown.verificationMethod || []), + { + type: 'Multikey', + id: 'did:web:localhost.com#keys-existing', + controller: 'did:web:localhost.com', + publicKeyMultibase: keyPair.publicKeyMultibase, + }, + ], + }; + + const duplicatedKeyPair = { + ...keyPair, + id: 'did:web:localhost.com#keys-4', + }; + + expect(() => + generateWellKnownDid({ + wellKnown: existingMultikeyWellKnown, + newKeyPair: duplicatedKeyPair, + }), + ).toThrowError('KeyPair already exists'); + }); + }); + + describe('generateWellKnownDid - Mixed Cryptosuites', () => { + it('should handle mixed legacy and modern cryptosuites', async () => { + const bbsKeyPair = { + id: 'did:web:localhost.com#keys-3', + type: VerificationType.Multikey, + controller: 'did:web:localhost.com', + seedBase58: 'CxBwAH4ftdc9XkLhw7DkFAESxh3NEdetMyJXKrPiAKAX', + secretKeyMultibase: 'z42twTcNeSYcnqg1FLuSFs2bsGH3ZqbRHFmvS9XMsYhjxvHN', + publicKeyMultibase: + 'zUC724qx4TSVn1zuJAjbaPAzwMhNvjHXvJrg36gEtNjQoKSVqZpG7QfVgsUsV2WrNvTB2p41iF2ZtVruDtwjMtZnyHCFGQov', + }; + + const result = generateWellKnownDid({ + wellKnown: wellKnown, + newKeyPair: bbsKeyPair, + }); + + expect(result).toBeTruthy(); + expect(result?.verificationMethod).toHaveLength(3); + + // Check that all verification method types are present + const types = result?.verificationMethod?.map((vm) => vm.type); + expect(types).toContain('Bls12381G2Key2020'); + expect(types).toContain('EcdsaSecp256k1RecoveryMethod2020'); + expect(types).toContain('Multikey'); + + // Check that contexts include both legacy and modern contexts + expect(result?.['@context']).toContain('https://www.w3.org/ns/did/v1'); + expect(result?.['@context']).toContain( + VerificationContext[VerificationType.Bls12381G2Key2020], + ); + expect(result?.['@context']).toContain( + VerificationContext[VerificationType.EcdsaSecp256k1RecoveryMethod2020], + ); + expect(result?.['@context']).toContain(VerificationContext[VerificationType.Multikey]); + }); + }); }); describe('nextKeyId', () => { diff --git a/packages/w3c-issuer/src/did-web/wellKnown/generate.ts b/packages/w3c-issuer/src/did-web/wellKnown/generate.ts index 3da187aa..723c06cc 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/generate.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/generate.ts @@ -1,5 +1,5 @@ import { VerificationMethod } from 'did-resolver'; -import { VerificationContext, VerificationType } from '../../lib/types'; +import { VerificationType, VerificationContext } from '../../lib/types'; import { KeyPair, DidWellKnownDocument, @@ -7,6 +7,8 @@ import { WellKnownEnum, BBSKeyPair, ECDSAKeyPair, + Bbs2023KeyPair, + EcdsaSd2023KeyPair, } from './types'; /** @@ -36,7 +38,11 @@ export const generateWellKnownDid = ({ s?.publicKeyBase58 === (newKeyPair as BBSKeyPair)?.publicKeyBase58) || (newKeyPair.type === VerificationType.EcdsaSecp256k1RecoveryMethod2020 && s?.blockchainAccountId && - s?.blockchainAccountId === (newKeyPair as ECDSAKeyPair)?.blockchainAccountId) + s?.blockchainAccountId === (newKeyPair as ECDSAKeyPair)?.blockchainAccountId) || + (newKeyPair.type === VerificationType.Multikey && + s?.publicKeyMultibase && + s?.publicKeyMultibase === + (newKeyPair as Bbs2023KeyPair | EcdsaSd2023KeyPair)?.publicKeyMultibase) ); }) ) { @@ -88,6 +94,13 @@ export const generateWellKnownDid = ({ (newKeyPair as ECDSAKeyPair).blockchainAccountId ) { newVerificationMethod.blockchainAccountId = (newKeyPair as ECDSAKeyPair).blockchainAccountId; + } else if ( + newKeyPair.type === VerificationType.Multikey && + (newKeyPair as Bbs2023KeyPair | EcdsaSd2023KeyPair).publicKeyMultibase + ) { + newVerificationMethod.publicKeyMultibase = ( + newKeyPair as Bbs2023KeyPair | EcdsaSd2023KeyPair + ).publicKeyMultibase; } if (!wellKnown[WellKnownEnum.VERIFICATION_METHOD]) { diff --git a/packages/w3c-issuer/src/did-web/wellKnown/index.test.ts b/packages/w3c-issuer/src/did-web/wellKnown/index.test.ts index 91e211c3..3f0d0a99 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/index.test.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/index.test.ts @@ -1,9 +1,14 @@ import _ from 'lodash'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { issueDID } from '.'; -import { VerificationType } from '../../lib/types'; +import { CryptoSuite, VerificationType } from '../../lib/types'; import * as query from './query'; -import { BBSPrivateKeyPair, IssuedDID } from './types'; +import { + BBSPrivateKeyPair, + Bbs2023PrivateKeyPair, + EcdsaSd2023PrivateKeyPair, + IssuedDID, +} from './types'; const mockedQueryDidDocumentResult = { id: 'did:web:localhost.com', @@ -31,6 +36,13 @@ describe('wellKnown', () => { '23GznFBm8BZcPM2BX7tcPfmeCAfkKKLWUWYwAj1jnu8acYhaNB892YkCDCJ1LLhXRBDpXEigx2tQYaw6EP1e1Fia83AfRUnmSA35v96ZgU3PmEd2DGqhUAXZZa4rM1gVGB9v', }; + const bbs2023KeyPair = { + seedBase58: 'CxBwAH4ftdc9XkLhw7DkFAESxh3NEdetMyJXKrPiAKAX', + secretKeyMultibase: 'z42twTcNeSYcnqg1FLuSFs2bsGH3ZqbRHFmvS9XMsYhjxvHN', + publicKeyMultibase: + 'zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM', + }; + describe('issueDID', () => { beforeEach(() => { vi.restoreAllMocks(); @@ -178,5 +190,175 @@ describe('wellKnown', () => { ); }); }); + + describe('issueDID - BBS-2023', () => { + it('should issueDID with valid domain and BBS-2023 type', async () => { + const result: IssuedDID = await issueDID({ + domain: 'https://www.google.com', + type: CryptoSuite.Bbs2023, + }); + + expect(result).toBeTruthy(); + expect(result.wellKnownDid).toHaveProperty('id'); + expect(result.wellKnownDid).toHaveProperty('verificationMethod'); + expect(result.wellKnownDid).toHaveProperty('@context'); + expect(result.wellKnownDid).toHaveProperty('assertionMethod'); + expect(result.wellKnownDid).toHaveProperty('authentication'); + expect(result.wellKnownDid).toHaveProperty('capabilityInvocation'); + expect(result.wellKnownDid).toHaveProperty('capabilityDelegation'); + + // Check that the verification method uses Multikey type + expect(result.wellKnownDid?.verificationMethod?.[0]?.type).toBe('Multikey'); + expect(result.wellKnownDid?.verificationMethod?.[0]).toHaveProperty('publicKeyMultibase'); + expect(result.wellKnownDid?.verificationMethod?.[0]).not.toHaveProperty('seedBase58'); + expect(result.wellKnownDid?.verificationMethod?.[0]).not.toHaveProperty( + 'secretKeyMultibase', + ); + expect(result.wellKnownDid?.verificationMethod?.[0]).not.toHaveProperty('cryptosuite'); + + // Check that the context includes Multikey v1 + expect(result.wellKnownDid?.['@context']).toContain( + 'https://w3id.org/security/multikey/v1', + ); + + // Check key pair properties + expect(result.didKeyPairs).toHaveProperty('type', VerificationType.Multikey); + expect(result.didKeyPairs).toHaveProperty('id'); + expect(result.didKeyPairs).toHaveProperty('controller'); + expect(result.didKeyPairs).toHaveProperty('secretKeyMultibase'); + expect(result.didKeyPairs).toHaveProperty('publicKeyMultibase'); + }); + + it('should issueDID with valid domain, BBS-2023 type and seed', async () => { + const result = await issueDID({ + domain: 'https://www.google.com', + type: CryptoSuite.Bbs2023, + seedBase58: bbs2023KeyPair.seedBase58, + }); + + expect(result).toBeTruthy(); + expect(result.wellKnownDid).toHaveProperty('id'); + expect(result.wellKnownDid?.verificationMethod?.[0]?.type).toBe('Multikey'); + expect(result.wellKnownDid?.verificationMethod?.[0]).toHaveProperty('publicKeyMultibase'); + expect(result.wellKnownDid?.['@context']).toContain( + 'https://w3id.org/security/multikey/v1', + ); + + expect(result.didKeyPairs).toHaveProperty('type', VerificationType.Multikey); + expect(result.didKeyPairs).toHaveProperty('seedBase58'); + expect(result.didKeyPairs).toHaveProperty('secretKeyMultibase'); + expect(result.didKeyPairs).toHaveProperty('publicKeyMultibase'); + expect((result.didKeyPairs as Bbs2023PrivateKeyPair)?.seedBase58).toBe( + bbs2023KeyPair.seedBase58, + ); + }); + }); + + describe('issueDID - ECDSA-SD-2023', () => { + it('should issueDID with valid domain and ECDSA-SD-2023 type', async () => { + const result: IssuedDID = await issueDID({ + domain: 'https://www.google.com', + type: CryptoSuite.EcdsaSd2023, + }); + + expect(result).toBeTruthy(); + expect(result.wellKnownDid).toHaveProperty('id'); + expect(result.wellKnownDid).toHaveProperty('verificationMethod'); + expect(result.wellKnownDid).toHaveProperty('@context'); + expect(result.wellKnownDid).toHaveProperty('assertionMethod'); + expect(result.wellKnownDid).toHaveProperty('authentication'); + expect(result.wellKnownDid).toHaveProperty('capabilityInvocation'); + expect(result.wellKnownDid).toHaveProperty('capabilityDelegation'); + + // Check that the verification method uses Multikey type + expect(result.wellKnownDid?.verificationMethod?.[0]?.type).toBe('Multikey'); + expect(result.wellKnownDid?.verificationMethod?.[0]).toHaveProperty('publicKeyMultibase'); + expect(result.wellKnownDid?.['@context']).toContain( + 'https://w3id.org/security/multikey/v1', + ); + + // Check key pair properties + expect(result.didKeyPairs).toHaveProperty('type', VerificationType.Multikey); + expect(result.didKeyPairs).toHaveProperty('id'); + expect(result.didKeyPairs).toHaveProperty('controller'); + expect(result.didKeyPairs).toHaveProperty('secretKeyMultibase'); + expect(result.didKeyPairs).toHaveProperty('publicKeyMultibase'); + expect(result.didKeyPairs).not.toHaveProperty('seedBase58'); // ECDSA-SD-2023 doesn't use seeds + }); + + it('should generate different keys on multiple calls', async () => { + const result1 = await issueDID({ + domain: 'https://www.google.com', + type: CryptoSuite.EcdsaSd2023, + }); + + const result2 = await issueDID({ + domain: 'https://www.google.com', + type: CryptoSuite.EcdsaSd2023, + }); + + expect(result1.didKeyPairs).toHaveProperty('publicKeyMultibase'); + expect(result2.didKeyPairs).toHaveProperty('publicKeyMultibase'); + + // Keys should be different since ECDSA-SD-2023 generates random keys + expect((result1.didKeyPairs as EcdsaSd2023PrivateKeyPair).publicKeyMultibase).not.toBe( + (result2.didKeyPairs as EcdsaSd2023PrivateKeyPair).publicKeyMultibase, + ); + }); + }); + + describe('issueDID - Mixed Cryptosuites', () => { + it('should handle existing DID with different cryptosuite types', async () => { + const mockExistingDid = { + id: 'did:web:localhost.com', + verificationMethod: [ + { + type: 'Bls12381G2Key2020', + id: 'did:web:localhost.com#keys-0', + controller: 'did:web:localhost.com', + publicKeyBase58: 'existing-bls-key', + }, + { + type: 'Multikey', + id: 'did:web:localhost.com#keys-1', + controller: 'did:web:localhost.com', + publicKeyMultibase: 'z6MkexistingBbs2023Key', + }, + ], + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/bls12381-2020/v1', + 'https://w3id.org/security/multikey/v1', + ], + assertionMethod: ['did:web:localhost.com#keys-0', 'did:web:localhost.com#keys-1'], + authentication: ['did:web:localhost.com#keys-0', 'did:web:localhost.com#keys-1'], + capabilityInvocation: ['did:web:localhost.com#keys-0', 'did:web:localhost.com#keys-1'], + capabilityDelegation: ['did:web:localhost.com#keys-0', 'did:web:localhost.com#keys-1'], + }; + + const spy = vi.spyOn(query, 'queryDidDocument').mockImplementation( + () => + new Promise((resolve, _rejects) => + resolve({ + did: mockExistingDid.id, + wellKnownDid: _.cloneDeep(mockExistingDid), + }), + ), + ); + + const result = await issueDID({ + domain: 'https://localhost.com', + type: CryptoSuite.EcdsaSd2023, + }); + + expect(spy).toHaveBeenCalledTimes(1); + expect(result).toBeTruthy(); + expect(result.wellKnownDid?.verificationMethod).toHaveLength(3); // Original 2 + new 1 + expect(result.wellKnownDid?.verificationMethod?.[2]?.type).toBe('Multikey'); + expect(result.wellKnownDid?.verificationMethod?.[2]?.id).toBe( + 'did:web:localhost.com#keys-2', + ); + }); + }); }); }); diff --git a/packages/w3c-issuer/src/did-web/wellKnown/index.ts b/packages/w3c-issuer/src/did-web/wellKnown/index.ts index 221fa159..2f1dedb1 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/index.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/index.ts @@ -1,4 +1,4 @@ -import { VerificationType } from '../../lib/types'; +import { VerificationType, VerificationContext } from '../../lib/types'; import { generateKeyPair } from './../keyPair'; import { generateWellKnownDid, nextKeyId } from './generate'; import { queryDidDocument } from './query'; @@ -9,11 +9,11 @@ import { PrivateKeyPair, IssuedDID, IssuedDIDOption } from './types'; * * @param {IssuedDIDOption} didInput * @param {string} didInput.domain - Domain name - * @param {string} didInput.type - Type of key pair to generate, supported types are Bls12381G2Key2020 + * @param {string} didInput.type - Type of key pair to generate, supported types are Bls12381G2Key2020, EcdsaSecp256k1RecoveryMethod2020, Bbs2023, EcdsaSd2023 * - * @param {string} didInput.seedBase58 - Seed in base58 format (optional) - * @param {string} didInput.privateKeyBase58 - Private key in base58 format (optional) - * @param {string} didInput.publicKeyBase58 - Public key in base58 format (optional) + * @param {string} didInput.seedBase58 - Seed in base58 format (optional, for BLS and BBS-2023) + * @param {string} didInput.privateKeyBase58 - Private key in base58 format (optional, for BLS) + * @param {string} didInput.publicKeyBase58 - Public key in base58 format (optional, for BLS) * * @param {string} didInput.mnemonics - Mnemonics for did:ethr [EcdsaSecp256k1RecoveryMethod2020] (optional) * @param {string} didInput.path - Path for HDWalletNode address for did:ethr [EcdsaSecp256k1RecoveryMethod2020], default "m/44'/60'/0'/0/0" (optional) @@ -32,19 +32,29 @@ export const issueDID = async (didInput: IssuedDIDOption): Promise => const generatedKeyPair = await generateKeyPair(didInput); - let keyPairs: PrivateKeyPair = { - id: `${did}#keys-${keyId}`, - type: generatedKeyPair.type, - controller: did, - }; + let keyPairs: PrivateKeyPair; if (generatedKeyPair.type === VerificationType.Bls12381G2Key2020) { + // Legacy BLS keyPairs = { - ...keyPairs, + id: `${did}#keys-${keyId}`, + type: VerificationType.Bls12381G2Key2020, + controller: did, seedBase58: generatedKeyPair?.seedBase58, privateKeyBase58: generatedKeyPair?.privateKeyBase58, publicKeyBase58: generatedKeyPair?.publicKeyBase58, }; + } else if (generatedKeyPair.type === VerificationType.Multikey) { + // Modern cryptosuites (BBS-2023, ECDSA-SD-2023) + keyPairs = { + '@context': VerificationContext[VerificationType.Multikey], + id: `${did}#keys-${keyId}`, + type: VerificationType.Multikey, + controller: did, + secretKeyMultibase: generatedKeyPair?.secretKeyMultibase, + publicKeyMultibase: generatedKeyPair?.publicKeyMultibase, + ...(generatedKeyPair?.seedBase58 && { seedBase58: generatedKeyPair.seedBase58 }), // Only for BBS-2023 + }; } const generatedWellKnownDid = generateWellKnownDid({ diff --git a/packages/w3c-issuer/src/did-web/wellKnown/query.test.ts b/packages/w3c-issuer/src/did-web/wellKnown/query.test.ts index 055c122c..4747245d 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/query.test.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/query.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { queryDidDocument } from './query'; +import { queryDidDocument, resolve, resolveRepresentation, dereference } from './query'; describe('query', () => { describe('queryDidDocument', () => { @@ -28,18 +28,27 @@ describe('query', () => { "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/bls12381-2020/v1", + "https://w3id.org/security/multikey/v1", ], "assertionMethod": [ "did:web:trustvc.github.io:did:1#keys-1", + "did:web:trustvc.github.io:did:1#multikey-1", + "did:web:trustvc.github.io:did:1#multikey-2", ], "authentication": [ "did:web:trustvc.github.io:did:1#keys-1", + "did:web:trustvc.github.io:did:1#multikey-1", + "did:web:trustvc.github.io:did:1#multikey-2", ], "capabilityDelegation": [ "did:web:trustvc.github.io:did:1#keys-1", + "did:web:trustvc.github.io:did:1#multikey-1", + "did:web:trustvc.github.io:did:1#multikey-2", ], "capabilityInvocation": [ "did:web:trustvc.github.io:did:1#keys-1", + "did:web:trustvc.github.io:did:1#multikey-1", + "did:web:trustvc.github.io:did:1#multikey-2", ], "id": "did:web:trustvc.github.io:did:1", "verificationMethod": [ @@ -49,10 +58,166 @@ describe('query', () => { "publicKeyBase58": "oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ", "type": "Bls12381G2Key2020", }, + { + "controller": "did:web:trustvc.github.io:did:1", + "id": "did:web:trustvc.github.io:did:1#multikey-1", + "publicKeyMultibase": "zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc", + "type": "Multikey", + }, + { + "controller": "did:web:trustvc.github.io:did:1", + "id": "did:web:trustvc.github.io:did:1#multikey-2", + "publicKeyMultibase": "zUC7HnpncVAkTjtL6B8prX6bQM2WA5sJ7rXFeCqyrvPnrzoFBjYsVUTNwzhhPUazja73tWwPeEBWCUgq5qBSrtrXiYhVvBCgZPTCiWANj7TSiZJ6SnyC3pkt94GiuChhAvmRRbt", + "type": "Multikey", + }, ], } `); expect(did).toBe('did:web:trustvc.github.io:did:1'); }); }); + + describe('resolve', () => { + it('should resolve did', async () => { + const did = 'did:web:trustvc.github.io:did:1'; + const result = await resolve(did); + expect(result.didDocument).toMatchInlineSnapshot(` + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/bls12381-2020/v1", + "https://w3id.org/security/multikey/v1", + ], + "assertionMethod": [ + "did:web:trustvc.github.io:did:1#keys-1", + "did:web:trustvc.github.io:did:1#multikey-1", + "did:web:trustvc.github.io:did:1#multikey-2", + ], + "authentication": [ + "did:web:trustvc.github.io:did:1#keys-1", + "did:web:trustvc.github.io:did:1#multikey-1", + "did:web:trustvc.github.io:did:1#multikey-2", + ], + "capabilityDelegation": [ + "did:web:trustvc.github.io:did:1#keys-1", + "did:web:trustvc.github.io:did:1#multikey-1", + "did:web:trustvc.github.io:did:1#multikey-2", + ], + "capabilityInvocation": [ + "did:web:trustvc.github.io:did:1#keys-1", + "did:web:trustvc.github.io:did:1#multikey-1", + "did:web:trustvc.github.io:did:1#multikey-2", + ], + "id": "did:web:trustvc.github.io:did:1", + "verificationMethod": [ + { + "controller": "did:web:trustvc.github.io:did:1", + "id": "did:web:trustvc.github.io:did:1#keys-1", + "publicKeyBase58": "oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ", + "type": "Bls12381G2Key2020", + }, + { + "controller": "did:web:trustvc.github.io:did:1", + "id": "did:web:trustvc.github.io:did:1#multikey-1", + "publicKeyMultibase": "zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc", + "type": "Multikey", + }, + { + "controller": "did:web:trustvc.github.io:did:1", + "id": "did:web:trustvc.github.io:did:1#multikey-2", + "publicKeyMultibase": "zUC7HnpncVAkTjtL6B8prX6bQM2WA5sJ7rXFeCqyrvPnrzoFBjYsVUTNwzhhPUazja73tWwPeEBWCUgq5qBSrtrXiYhVvBCgZPTCiWANj7TSiZJ6SnyC3pkt94GiuChhAvmRRbt", + "type": "Multikey", + }, + ], + } + `); + }); + + it('should return notFound error for unresolvable did', async () => { + const did = 'did:web:trustvc2.github.io:did:1'; + const result = await resolve(did); + expect(result.didDocument).toBeNull(); + expect(result.didResolutionMetadata).toHaveProperty('error', 'notFound'); + }); + + it('should return inValidDid error for invalid did', async () => { + const did = 'did:web: trustvc .github.io:did:1'; + const result = await resolve(did); + expect(result.didDocument).toBeNull(); + expect(result.didResolutionMetadata).toHaveProperty('error', 'invalidDid'); + }); + }); + + describe('resolveRepresentation', () => { + it('Should return serialized data for valid did and should contain contentType', async () => { + const did = 'did:web:trustvc.github.io:did:1'; + const result = await resolveRepresentation(did); + + expect(result.didDocumentStream).toBeTruthy(); + expect(typeof result.didDocumentStream).toBe('string'); + const parsedDocument = JSON.parse(result.didDocumentStream); + expect(parsedDocument).toHaveProperty('@context'); + expect(result.didResolutionMetadata).toHaveProperty('contentType', 'application/did+ld+json'); + }); + + it('Should return correct contentType for requested accept value', async () => { + const did = 'did:web:trustvc.github.io:did:1'; + const result = await resolveRepresentation(did, { accept: 'application/did+json' }); + + expect(result.didDocumentStream).toBeTruthy(); + expect(typeof result.didDocumentStream).toBe('string'); + const parsedDocument = JSON.parse(result.didDocumentStream); + expect(parsedDocument).not.toHaveProperty('@context'); + expect(result.didResolutionMetadata).toHaveProperty('contentType', 'application/did+json'); + }); + + it('Should return representationNotSupported error for unsupported accept value', async () => { + const did = 'did:web:trustvc.github.io:did:1'; + const result = await resolveRepresentation(did, { accept: 'application/did+cbor' }); + + expect(result).toEqual({ + didResolutionMetadata: { + error: 'representationNotSupported', + message: 'Content type application/did+cbor is not supported.', + }, + didDocumentStream: '', + didDocumentMetadata: {}, + }); + }); + }); +}); + +describe('dereference', () => { + it('Should return serialized data for valid did', async () => { + const did = 'did:web:trustvc.github.io:did:1'; + const result = await dereference(did); + + expect(result.contentStream).toBeTruthy(); + expect(typeof result.contentStream).toBe('string'); + const parsedDocument = JSON.parse(result.contentStream); + expect(parsedDocument).toHaveProperty('@context'); + expect(result.dereferencingMetadata).toHaveProperty('contentType', 'application/did+ld+json'); + }); + + it('Should return valid verification method for a given fragment', async () => { + const did = 'did:web:trustvc.github.io:did:1#multikey-1'; + const result = await dereference(did); + + expect(result.contentStream).toBe( + '{"id":"did:web:trustvc.github.io:did:1#multikey-1","type":"Multikey","controller":"did:web:trustvc.github.io:did:1","publicKeyMultibase":"zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc"}', + ); + expect(result.dereferencingMetadata).toHaveProperty('contentType', 'application/did+ld+json'); + }); + + it('Should return notFound for query or path based dereferencing', async () => { + const didWithPath = 'did:web:trustvc.github.io:did:1/path'; + const resultWithPath = await dereference(didWithPath); + expect(resultWithPath.dereferencingMetadata).toHaveProperty('error', 'notFound'); + expect(resultWithPath.contentStream).toBe(''); + + const didWithQuery = 'did:web:trustvc.github.io:did:1?query=param'; + const resultWithQuery = await dereference(didWithQuery); + expect(resultWithQuery.dereferencingMetadata).toHaveProperty('error', 'notFound'); + expect(resultWithQuery.contentStream).toBe(''); + }); }); diff --git a/packages/w3c-issuer/src/did-web/wellKnown/query.ts b/packages/w3c-issuer/src/did-web/wellKnown/query.ts index 30323b96..9c6a009e 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/query.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/query.ts @@ -1,8 +1,15 @@ -import { Resolver } from 'did-resolver'; +import { Resolver, DIDResolutionResult, DIDResolutionOptions } from 'did-resolver'; import { getResolver as webGetResolver } from 'web-did-resolver'; import { getDomain } from '../../lib'; import { DidWellKnownDocument, QueryDidDocument, QueryDidDocumentOption } from './types'; +const SUPPORTED_CONTENT_TYPES = ['application/did+json', 'application/did+ld+json']; +const getResolver = (): Resolver => { + return new Resolver({ + ...webGetResolver(), + }); +}; + /** * Query well known DID document based on the domain. * @@ -41,3 +48,145 @@ export const queryDidDocument = async ({ did, }; }; + +/** + * Resolves a DID to a DID Document according to the W3C DID Core specification. + * This function implements the `resolve` operation which returns the DID document as a data model. + * + * @param did - The DID to resolve + * @returns A DID Resolution Result without contentType in the metadata + */ +export const resolve = async (did: string): Promise => { + try { + const resolver = getResolver(); + const doc: DIDResolutionResult = await resolver.resolve(did); + + if (doc.didResolutionMetadata && 'contentType' in doc.didResolutionMetadata) { + delete doc.didResolutionMetadata.contentType; + } + + return doc; + } catch { + throw new Error('Failed to resolve did'); + } +}; + +/** + * Resolves a DID to a DID Document representation according to the W3C DID Core specification. + * This function implements the `resolveRepresentation` operation which returns the DID document as a byte stream. + * + * @param did - The DID to resolve + * @param resolutionOptions - Options for resolution, including accept for content negotiation + * @returns A DID Resolution Result with the document as a string and contentType in the metadata + */ +export const resolveRepresentation = async ( + did: string, + resolutionOptions?: DIDResolutionOptions, +) => { + const { accept } = resolutionOptions || {}; + + if (accept && !SUPPORTED_CONTENT_TYPES.includes(accept)) { + return { + didResolutionMetadata: { + error: 'representationNotSupported', + message: `Content type ${accept} is not supported.`, + }, + didDocumentStream: '', + didDocumentMetadata: {}, + }; + } + + try { + const resolver = getResolver(); + const doc = await resolver.resolve(did, { accept }); + const { didDocument, didResolutionMetadata, ...rest } = doc; + if (didDocument && accept === 'application/did+json') { + delete didDocument['@context']; + } + + const response = { + ...rest, + didDocumentStream: didDocument ? JSON.stringify(didDocument) : '', + didResolutionMetadata, + }; + + if (!didResolutionMetadata.error) { + response.didResolutionMetadata.contentType = accept ?? didResolutionMetadata.contentType; + } + + return response; + } catch { + throw new Error('Failed to resolve did'); + } +}; + +/** + * Dereferences a DID URL to a resource according to the W3C DID Core specification. + * This function handles fragment-based dereferencing to retrieve verification methods. + * + * @param did - The DID URL to dereference (may include a fragment) + * @param dereferenceOptions - Options for dereferencing, including accept for content negotiation + * @returns A DID Dereferencing Result with the resource as a string + */ +export const dereference = async (did: string, dereferenceOptions?: DIDResolutionOptions) => { + const { accept } = dereferenceOptions || {}; + + const isInvalidRequest = + (accept && !SUPPORTED_CONTENT_TYPES.includes(accept)) || did.includes('?') || did.includes('/'); + + if (isInvalidRequest) { + return { + dereferencingMetadata: { error: 'notFound' }, + contentStream: '', + contentMetadata: {}, + }; + } + + try { + const resolver = getResolver(); + const { + didDocument, + didResolutionMetadata: dereferencingMetadata, + didDocumentMetadata: contentMetadata, + } = await resolver.resolve(did, { accept }); + + if (dereferencingMetadata.error) { + const error = + dereferencingMetadata.error === 'invalidDid' + ? 'invalidDidUrl' + : dereferencingMetadata.error; + + return { + dereferencingMetadata: { ...dereferencingMetadata, error }, + contentStream: '', + contentMetadata, + }; + } + + dereferencingMetadata.contentType = accept ?? dereferencingMetadata.contentType; + + if (did.includes('#')) { + const verificationMethod = didDocument.verificationMethod?.find((vm) => vm.id === did); + if (verificationMethod) { + return { + contentStream: JSON.stringify(verificationMethod), + contentMetadata, + dereferencingMetadata, + }; + } + } + + const documentToReturn = { ...didDocument }; + if (documentToReturn && accept === 'application/did+json') { + delete documentToReturn['@context']; + } + + return { + contentStream: JSON.stringify(documentToReturn), + contentMetadata, + dereferencingMetadata, + }; + } catch { + throw new Error('Failed to dereference did'); + } +}; diff --git a/packages/w3c-issuer/src/did-web/wellKnown/types.ts b/packages/w3c-issuer/src/did-web/wellKnown/types.ts index c72a26b0..3701309f 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/types.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/types.ts @@ -32,7 +32,7 @@ export const WellKnownAttribute: readonly WellKnownAttributeType[] = [ WellKnownEnum.CAPABILITY_DELEGATION, ] as const; -export type KeyPair = BBSKeyPair | ECDSAKeyPair; +export type KeyPair = BBSKeyPair | ECDSAKeyPair | MultikeyKeyPair; export type BBSKeyPair = { id: string; type: VerificationType; @@ -47,8 +47,21 @@ export type ECDSAKeyPair = { publicKeyMultibase?: string; blockchainAccountId?: string; }; +export type MultikeyKeyPair = { + '@context'?: string; + id: string; + type: VerificationType.Multikey; + controller: string; + publicKeyMultibase?: string; +}; +export type Bbs2023KeyPair = MultikeyKeyPair; +export type EcdsaSd2023KeyPair = MultikeyKeyPair; -export type PrivateKeyPair = BBSPrivateKeyPair | ECDSAPrivateKeyPair; +export type PrivateKeyPair = + | BBSPrivateKeyPair + | ECDSAPrivateKeyPair + | Bbs2023PrivateKeyPair + | EcdsaSd2023PrivateKeyPair; export type BBSPrivateKeyPair = BBSKeyPair & { seedBase58?: string; privateKeyBase58?: string; @@ -59,6 +72,13 @@ export type ECDSAPrivateKeyPair = ECDSAKeyPair & { privateKeyMultibase?: string; mnemonics?: string; }; +export type Bbs2023PrivateKeyPair = Bbs2023KeyPair & { + seedBase58?: string; + secretKeyMultibase?: string; +}; +export type EcdsaSd2023PrivateKeyPair = EcdsaSd2023KeyPair & { + secretKeyMultibase?: string; +}; export type DidWellKnownDocument = DIDDocument & { [key in WellKnownAttributeType]?: string[]; diff --git a/packages/w3c-issuer/src/lib/types.ts b/packages/w3c-issuer/src/lib/types.ts index 23466f8a..470385db 100644 --- a/packages/w3c-issuer/src/lib/types.ts +++ b/packages/w3c-issuer/src/lib/types.ts @@ -1,5 +1,10 @@ import { DidWebGenerateKeyPairOptions, DidWebGeneratedKeyPair } from './../did-web/keyPair/types'; +export enum CryptoSuite { + Bbs2023 = 'bbs-2023', + EcdsaSd2023 = 'ecdsa-sd-2023', +} + /** * https://www.w3.org/TR/did-spec-registries/#verification-method-types */ @@ -10,6 +15,7 @@ export enum VerificationType { Bls12381G1Key2020 = 'Bls12381G1Key2020', Bls12381G2Key2020 = 'Bls12381G2Key2020', EcdsaSecp256k1RecoveryMethod2020 = 'EcdsaSecp256k1RecoveryMethod2020', + Multikey = 'Multikey', } export const VerificationContext: { [key in VerificationType]: string } = { @@ -23,10 +29,11 @@ export const VerificationContext: { [key in VerificationType]: string } = { [VerificationType.Bls12381G2Key2020]: 'https://w3id.org/security/suites/bls12381-2020/v1', [VerificationType.EcdsaSecp256k1RecoveryMethod2020]: 'https://w3id.org/security/suites/secp256k1recovery-2020/v2', + [VerificationType.Multikey]: 'https://w3id.org/security/multikey/v1', }; export type BaseKeyPair = { - type: VerificationType; + type: VerificationType | CryptoSuite; }; export type GenerateKeyPairOptions = Required & Partial; diff --git a/packages/w3c-issuer/tsconfig.build.json b/packages/w3c-issuer/tsconfig.build.json index d3c5227e..eb191e4c 100644 --- a/packages/w3c-issuer/tsconfig.build.json +++ b/packages/w3c-issuer/tsconfig.build.json @@ -7,6 +7,6 @@ "rootDir": ".", "baseUrl": "." }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "../declaration.d.ts"], "exclude": ["node_modules", ".coverage", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/packages/w3c-issuer/tsconfig.json b/packages/w3c-issuer/tsconfig.json index 3cf45e0d..1a4ebe25 100644 --- a/packages/w3c-issuer/tsconfig.json +++ b/packages/w3c-issuer/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "declaration": false }, - "include": ["src/**/*"] + "include": ["src/**/*", "../declaration.d.ts"] } diff --git a/packages/w3c-vc/README.md b/packages/w3c-vc/README.md index 00ea3bce..21837cbb 100644 --- a/packages/w3c-vc/README.md +++ b/packages/w3c-vc/README.md @@ -1,6 +1,20 @@ # TrustVC W3C VC -This repository provides utilities for signing and verifying Verifiable Credentials (VCs) using the BBS+ signature scheme. These functions can be used to ensure the authenticity and integrity of VCs within a W3C-compliant ecosystem. +This repository provides utilities for signing, verifying, and deriving Verifiable Credentials (VCs) using modern cryptographic signature schemes including ECDSA-SD-2023 and BBS-2023, with selective disclosure capabilities. These functions ensure the authenticity and integrity of VCs within a W3C-compliant ecosystem. + +## Table of Contents + +- [Installation](#installation) +- [Features](#features) +- [Supported Cryptosuites](#supported-cryptosuites) +- [Usage](#usage) + - [1. Signing a Credential](#1-signing-a-credential) + - [2. Verifying a Credential](#2-verifying-a-credential) + - [3. Deriving a Credential (Selective Disclosure)](#3-deriving-a-credential-selective-disclosure) + - [4. Schema Validation](#4-schema-validation) +- [Migration from Legacy BbsBlsSignature2020](#migration-from-legacy-bbsblssignature2020) +- [Best Practices](#best-practices) +- [License](#license) ## Installation To install the package, use: @@ -9,15 +23,29 @@ To install the package, use: npm install @trustvc/w3c-vc ``` -## Feature -- Sign and Verify for BBS+ signature for W3C VC -- Derive a new VC with selective disclosure using BBS+ signature -- Checks if payload matches W3C VC schema +## Features +- **Modern Cryptosuites**: ECDSA-SD-2023 (default) and BBS-2023 with selective disclosure +- **W3C Compliance**: Support for both W3C VC Data Model v1.1 and v2.0 +- **Selective Disclosure**: Derive credentials with selective field revelation +- **Legacy Support**: Deprecated BbsBlsSignature2020 signature support (verification only) +- **Schema Validation**: Checks if payload matches W3C VC schema +- **Version Detection**: Helper functions to detect credential versions + +## Supported Cryptosuites + +| Cryptosuite | Status | Signing | Verification | Derivation | Notes | +|-------------|--------|---------|--------------|------------|-------| +| `ecdsa-sd-2023` | ✅ Active (Default) | ✅ | ✅ | ✅ | Modern, fast, selective disclosure | +| `bbs-2023` | ✅ Active | ✅ | ✅ | ✅ | Modern BBS with selective disclosure | +| `BbsBlsSignature2020` | ⚠️ Deprecated | ❌ | ✅ | ❌ | Legacy support only | ## Usage + ### 1. Signing a Credential -The signCredential function allows you to sign a Verifiable Credential using the BBS+ signature scheme. +The `signCredential` function allows you to sign a Verifiable Credential using modern cryptographic signature schemes. **Default cryptosuite is `ecdsa-sd-2023`**. + +#### ECDSA-SD-2023 Signing (Default) ```ts import { signCredential } from '@trustvc/w3c-vc'; @@ -26,7 +54,8 @@ import { signCredential } from '@trustvc/w3c-vc'; * Parameters: * - credential (RawVerifiableCredential): The credential to be signed. * - keyPair (PrivateKeyPair): The key pair options for signing. - * - cryptoSuite (string, optional): The cryptosuite to be used for signing. Defaults to "BbsBlsSignature2020". + * - cryptoSuite (CryptoSuiteName, optional): The cryptosuite to be used. Defaults to "ecdsa-sd-2023". + * - options (object, optional): Optional parameters including documentLoader and mandatoryPointers. * * Returns: * - A Promise that resolves to: @@ -34,43 +63,38 @@ import { signCredential } from '@trustvc/w3c-vc'; * - signedCredential.error (string): The error message in case of failure. */ -const keyPair = { - "id": "did:web:trustvc.github.io:did:1#keys-1", - "type": "Bls12381G2Key2020", +const ecdsaKeyPair = { + "id": "did:web:trustvc.github.io:did:1#multikey-1", + "type": "Multikey", "controller": "did:web:trustvc.github.io:did:1", - "seedBase58": "", - "privateKeyBase58": "", - "publicKeyBase58": "oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ" + "secretKeyMultibase": "", + "publicKeyMultibase": "" }; const credential = { "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3c-ccg.github.io/citizenship-vocab/contexts/citizenship-v1.jsonld", - "https://w3id.org/security/bbs/v1", - "https://w3id.org/vc/status-list/2021/v1" + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/data-integrity/v2", + "https://w3id.org/citizenship/v2" ], - "credentialStatus": { - "id": "https://trustvc.github.io/did/credentials/statuslist/1#1", - "statusListCredential": "https://trustvc.github.io/did/credentials/statuslist/1", - "statusListIndex": "1", - "statusPurpose": "revocation", - "type": "StatusList2021Entry" - }, - "issuanceDate": "2024-04-01T12:19:52Z", "credentialSubject": { "id": "did:example:b34ca6cd37bbf23", "type": ["Person"], "name": "TrustVC" }, - "expirationDate": "2029-12-03T12:19:52Z", + "validFrom": "2024-04-01T12:19:52Z", + "validUntil": "2029-12-03T12:19:52Z", "issuer": "did:web:trustvc.github.io:did:1", - "type": [ - "VerifiableCredential" - ] + "type": ["VerifiableCredential"] }; -const signedCredential = await signCredential(credential, keyPair); +// Sign with default ECDSA-SD-2023 cryptosuite +const signedCredential = await signCredential(credential, ecdsaKeyPair); + +// Or explicitly specify cryptosuite with mandatory pointers +const signedCredentialWithOptions = await signCredential(credential, ecdsaKeyPair, 'ecdsa-sd-2023', { + mandatoryPointers: ['/credentialSubject/name'] +}); if (signedCredential.signed) { console.log('Signed Credential:', signedCredential.signed); @@ -79,53 +103,75 @@ if (signedCredential.signed) { } ``` -
- signCredential Result - - ```js - Signed Credential: { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3c-ccg.github.io/citizenship-vocab/contexts/citizenship-v1.jsonld', - 'https://w3id.org/security/bbs/v1', - 'https://w3id.org/vc/status-list/2021/v1' - ], - credentialStatus: { - id: 'https://trustvc.github.io/did/credentials/statuslist/1#1', - statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/1', - statusListIndex: '1', - statusPurpose: 'revocation', - type: 'StatusList2021Entry' - }, - issuanceDate: '2024-04-01T12:19:52Z', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: [ 'Person' ], - name: 'TrustVC' - }, - expirationDate: '2029-12-03T12:19:52Z', - issuer: 'did:web:trustvc.github.io:did:1', - type: [ 'VerifiableCredential' ], - proof: { - type: 'BbsBlsSignature2020', - created: '2024-10-02T09:04:07Z', - proofPurpose: 'assertionMethod', - proofValue: 'tissP5pJF1q4txCMWNZI5LgwhXMWrLI8675ops8FwlQE/zBUQnVO9Iey505MjkNDD5GdmQmnb6+RUKkLVGEJLIJrKQXlU3Xr4DlMW7ShH/sIpuvZoobGs/0hw/B5agXz8cVWfnDGWtDYciVh0rwQvg==', - verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1' - } - } - ``` -
+#### BBS-2023 Signing + +```ts +import { signCredential } from '@trustvc/w3c-vc'; + +const bbsKeyPair = { + "id": "did:web:trustvc.github.io:did:1#multikey-2", + "type": "Multikey", + "controller": "did:web:trustvc.github.io:did:1", + "secretKeyMultibase": "", + "publicKeyMultibase": "" +}; + +const credential = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/data-integrity/v2", + "https://w3id.org/citizenship/v2" + ], + "credentialSubject": { + "id": "did:example:b34ca6cd37bbf23", + "type": ["Person"], + "name": "TrustVC" + }, + "validFrom": "2024-04-01T12:19:52Z", + "validUntil": "2029-12-03T12:19:52Z", + "issuer": "did:web:trustvc.github.io:did:1", + "type": ["VerifiableCredential"] +}; + +// Sign with BBS-2023 cryptosuite +const signedCredential = await signCredential(credential, bbsKeyPair, 'bbs-2023', { + mandatoryPointers: ['/credentialSubject/name'] +}); + +if (signedCredential.signed) { + console.log('BBS-2023 Signed Credential:', signedCredential.signed); +} else { + console.error('Error:', signedCredential.error); +} +``` + +#### Legacy BbsBlsSignature2020 Signing (Deprecated) + +```ts +import { signCredential } from '@trustvc/w3c-vc'; + +// ⚠️ DEPRECATED: BbsBlsSignature2020 signing is no longer supported +const legacyResult = await signCredential(credential, keyPair, 'BbsBlsSignature2020'); + +// This will return an error: +// { error: "BbsBlsSignature2020 is no longer supported. Please use the latest cryptosuite versions instead." } +``` ### 2. Verifying a Credential -The verifyCredential function allows you to verify a signed Verifiable Credential using the BBS+ signature scheme. + +The `verifyCredential` function allows you to verify signed Verifiable Credentials. + +#### Modern Cryptosuite Verification (ECDSA-SD-2023 / BBS-2023) + +> **Important**: For modern cryptosuites (ECDSA-SD-2023, BBS-2023), **base credentials must be derived before verification**. You cannot directly verify the original signed credential. ```ts -import { verifyCredential } from '@trustvc/w3c-vc'; +import { verifyCredential, deriveCredential } from '@trustvc/w3c-vc'; /** * Parameters: * - credential (SignedVerifiableCredential): The credential to be verified. + * - options (object, optional): Optional parameters including documentLoader. * * Returns: * - A Promise that resolves to: @@ -133,26 +179,67 @@ import { verifyCredential } from '@trustvc/w3c-vc'; * - verificationResult.error (string): The error message if the verification failed. */ -const credential = { +const signedCredential = { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/data-integrity/v2', + "https://w3id.org/citizenship/v2" + ], + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['Person'], + name: 'TrustVC' + }, + validFrom: '2024-04-01T12:19:52Z', + validUntil: '2029-12-03T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + type: ['VerifiableCredential'], + proof: { + type: 'DataIntegrityProof', + cryptosuite: 'ecdsa-sd-2023', + created: '2024-10-02T09:04:07Z', + proofPurpose: 'assertionMethod', + proofValue: 'z...', + verificationMethod: 'did:web:trustvc.github.io:did:1#multikey-1' + } +}; + +// ❌ This will fail - base credentials cannot be verified directly +const baseVerification = await verifyCredential(signedCredential); +// Returns: { verified: false, error: "ecdsa-sd-2023 base credentials must be derived before verification. Use deriveCredential() first." } + +// ✅ Correct approach: Derive first, then verify +const derivedResult = await deriveCredential(signedCredential, ['/credentialSubject/name']); + +if (derivedResult.derived) { + const verificationResult = await verifyCredential(derivedResult.derived); + + if (verificationResult.verified) { + console.log('Credential verified successfully.'); + } else { + console.error('Verification failed:', verificationResult.error); + } +} +``` + +#### Legacy BbsBlsSignature2020 Verification + +```ts +import { verifyCredential } from '@trustvc/w3c-vc'; + +// Legacy BbsBlsSignature2020 credentials can be verified directly (both base and derived credentials) +const bbsCredential = { '@context': [ 'https://www.w3.org/2018/credentials/v1', - 'https://w3c-ccg.github.io/citizenship-vocab/contexts/citizenship-v1.jsonld', 'https://w3id.org/security/bbs/v1', - 'https://w3id.org/vc/status-list/2021/v1' + 'https://w3id.org/citizenship/v1' ], - credentialStatus: { - id: 'https://trustvc.github.io/did/credentials/statuslist/1#1', - statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/1', - statusListIndex: '1', - statusPurpose: 'revocation', - type: 'StatusList2021Entry' - }, - issuanceDate: '2024-04-01T12:19:52Z', credentialSubject: { id: 'did:example:b34ca6cd37bbf23', - type: [ 'Person' ], + type: ['Person'], name: 'TrustVC' }, + issuanceDate: '2024-04-01T12:19:52Z', expirationDate: '2029-12-03T12:19:52Z', issuer: 'did:web:trustvc.github.io:did:1', type: [ 'VerifiableCredential' ], @@ -160,31 +247,25 @@ const credential = { type: 'BbsBlsSignature2020', created: '2024-10-02T09:04:07Z', proofPurpose: 'assertionMethod', - proofValue: 'tissP5pJF1q4txCMWNZI5LgwhXMWrLI8675ops8FwlQE/zBUQnVO9Iey505MjkNDD5GdmQmnb6+RUKkLVGEJLIJrKQXlU3Xr4DlMW7ShH/sIpuvZoobGs/0hw/B5agXz8cVWfnDGWtDYciVh0rwQvg==', + proofValue: 'A...', verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1' } }; -const verificationResult = await verifyCredential(credential); +const verificationResult = await verifyCredential(bbsCredential); if (verificationResult.verified) { - console.log('Credential verified successfully.'); + console.log('Legacy BbsBlsSignature2020 credential verified successfully.'); } else { console.error('Verification failed:', verificationResult.error); } ``` -
- verifyCredential Result - - ```js - Credential verified successfully. - ``` -
+### 3. Deriving a Credential (Selective Disclosure) -### 3. Deriving a Credential +The `deriveCredential` function allows you to create selective disclosure from signed credentials. -The deriveCredential function allows you to derive a new credential with selective disclosure using the BBS+ signature proof. +#### ECDSA-SD-2023 Selective Disclosure ```ts import { deriveCredential } from '@trustvc/w3c-vc'; @@ -192,128 +273,194 @@ import { deriveCredential } from '@trustvc/w3c-vc'; /** * Parameters: * - credential (SignedVerifiableCredential): The verifiable credential to be selectively disclosed. - * - revealedAttributes (ContextDocument): The attributes from the credential that should be revealed in the derived proof. + * - revealedAttributes (string[]): Array of JSON pointers specifying which fields to reveal. + * - options (object, optional): Optional parameters including documentLoader. * * Returns: * - A Promise that resolves to: - * - derived (DerivedProof): The selectively disclosed credential with the derived proof. + * - derived (SignedVerifiableCredential): The selectively disclosed credential. * - error (string): The error message in case of failure. */ -const credential = { +const signedCredential = { '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3c-ccg.github.io/citizenship-vocab/contexts/citizenship-v1.jsonld', - 'https://w3id.org/security/bbs/v1', - 'https://w3id.org/vc/status-list/2021/v1' + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/data-integrity/v2', + "https://w3id.org/citizenship/v2" ], - credentialStatus: { - id: 'https://trustvc.github.io/did/credentials/statuslist/1#1', - statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/1', - statusListIndex: '1', - statusPurpose: 'revocation', - type: 'StatusList2021Entry' - }, - issuanceDate: '2024-04-01T12:19:52Z', credentialSubject: { id: 'did:example:b34ca6cd37bbf23', - type: [ 'Person' ], + type: ['Person'], name: 'TrustVC' }, - expirationDate: '2029-12-03T12:19:52Z', + validFrom: '2024-04-01T12:19:52Z', + validUntil: '2029-12-03T12:19:52Z', issuer: 'did:web:trustvc.github.io:did:1', - type: [ 'VerifiableCredential' ], + type: ['VerifiableCredential'], proof: { - type: 'BbsBlsSignature2020', - created: '2024-10-02T09:04:07Z', - proofPurpose: 'assertionMethod', - proofValue: 'tissP5pJF1q4txCMWNZI5LgwhXMWrLI8675ops8FwlQE/zBUQnVO9Iey505MjkNDD5GdmQmnb6+RUKkLVGEJLIJrKQXlU3Xr4DlMW7ShH/sIpuvZoobGs/0hw/B5agXz8cVWfnDGWtDYciVh0rwQvg==', - verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1' + type: 'DataIntegrityProof', + cryptosuite: 'ecdsa-sd-2023', + // ... proof details } }; -const revealedAttributes = { +// Reveal only specific fields (mandatory pointers are automatically included) +const selectivePointers = ['/credentialSubject/name']; + +const derivedResult = await deriveCredential(signedCredential, selectivePointers); + +if (derivedResult.derived) { + console.log('Derived ECDSA-SD-2023 Credential:', derivedResult.derived); +} else { + console.error('Error:', derivedResult.error); +} + +// ❌ Multiple derivation rounds are not supported +const secondDerivation = await deriveCredential(derivedResult.derived, selectivePointers); +// Returns: { error: "ecdsa-sd-2023 derived credentials cannot be further derived. Multiple rounds of derivation are not supported by this cryptosuite." } +``` + +#### BBS-2023 Selective Disclosure + +```ts +import { deriveCredential } from '@trustvc/w3c-vc'; + +const bbsSignedCredential = { '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3c-ccg.github.io/citizenship-vocab/contexts/citizenship-v1.jsonld', - 'https://w3id.org/security/bbs/v1', - 'https://w3id.org/vc/status-list/2021/v1' + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/data-integrity/v2', + "https://w3id.org/citizenship/v2" ], credentialSubject: { - type: [ 'Person' ], - '@explicit': true + id: 'did:example:b34ca6cd37bbf23', + type: ['Person'], + name: 'TrustVC' }, - type: [ 'VerifiableCredential' ] + validFrom: '2024-04-01T12:19:52Z', + validUntil: '2029-12-03T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + type: ['VerifiableCredential'], + proof: { + type: 'DataIntegrityProof', + cryptosuite: 'bbs-2023', + // ... proof details + } }; -const derivedResult = await deriveCredential(credential, revealedAttributes); +// Reveal only specific fields (mandatory pointers are automatically included) +const selectivePointers = ['/credentialSubject/name']; + +const derivedResult = await deriveCredential(bbsSignedCredential, selectivePointers); if (derivedResult.derived) { - console.log('Derived Credential:', derivedResult.derived); + console.log('Derived BBS-2023 Credential:', derivedResult.derived); } else { console.error('Error:', derivedResult.error); } ``` -
- deriveCredential Result - - ```js - Derived Credential: { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3c-ccg.github.io/citizenship-vocab/contexts/citizenship-v1.jsonld', - 'https://w3id.org/security/bbs/v1', - 'https://w3id.org/vc/status-list/2021/v1' - ], - credentialStatus: { - id: 'https://trustvc.github.io/did/credentials/statuslist/1#1', - statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/1', - statusListIndex: '1', - statusPurpose: 'revocation', - type: 'StatusList2021Entry' - }, - issuanceDate: '2024-04-01T12:19:52Z', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: [ 'Person' ] - }, - expirationDate: '2029-12-03T12:19:52Z', - issuer: 'did:web:trustvc.github.io:did:1', - id: 'urn:bnid:_:c14n0', - type: [ 'VerifiableCredential' ], - proof: { - type: 'BbsBlsSignatureProof2020', - created: '2024-10-02T09:04:07Z', - nonce: 'YsFIiujnENBLMsuuXhyctszyGC72SLqiOvlT8OcvSOD6eDcehcGJgbTx5k+tfK00K5M=', - proofPurpose: 'assertionMethod', - proofValue: 'ABAA/++QJhxxPdA340RTSEwPfgmB1Z3kUnhOCE4ReITG6nSNhHvZxdP24jsvBUzyecIArsS2FZdgscCNVP2K2LvEXJteLh/pDjOVsTMOyuVuDaOPYclXxOJR4D3UQtL0DFhu4wC0NaZ+NXV8j1xG/zyJ+lzn4jrKaPhHyuySKFjCZnlQNVx01Cm3pKzgL94GdKsXsEsAAAB0p7aO6wVq4hcyOKmEK4UALxZHTIMet/QMoVlHI017QSQi8hHu+hnGEmmmpuyluTgrAAAAAnIbawi/noqB4Fb+Q3C8ck73LxWVeqBrisWfadhCfep0FVRp/l2McLCsr9mfcDwhFpDoPfh8jza82Mk0s15Q9J+LwH39CGtwjatgL22bM4Ulnwe+GsyYoOgcN6vbtkmYdw7TNJ+H76mRq1K82vDJ6sQAAAADDUVU/P0Exnz7Dvs5V0rSHEur/ySddDgjU7cZZVRjTARzCr7xpcs/yd9W6FGzvxDSIqwTjBgnah9I6v4QDOAdvVyjoZ+Joppjt1rIER9AYXxIN++wCqQtWqaC/X7jPtPb', - verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1' - } - } - ``` -
+#### Legacy BbsBlsSignature2020 Derivation (Deprecated) + +```ts +import { deriveCredential } from '@trustvc/w3c-vc'; + +// ⚠️ DEPRECATED: BbsBlsSignature2020 derivation is no longer supported +const legacyDerivation = await deriveCredential(credential, revealedAttributes); + +// This will return an error: +// { error: "BbsBlsSignature2020 is no longer supported for derivation. Please use the latest cryptosuite versions instead." } +``` + +### 4. Schema Validation -### 4. Validate if payload meets the schema +Validate if a document meets W3C VC schema requirements: ```ts -import { isRawDocument, isSignedDocument } from '@trustvc/w3c-vc'; +import { + isRawDocument, + isSignedDocument, + isRawDocumentV1_1, + isRawDocumentV2_0, + isSignedDocumentV1_1, + isSignedDocumentV2_0, + isDerived +} from '@trustvc/w3c-vc'; /** - * Parameters: - * - document (RawVerifiableCredential | SignedVerifiableCredential | unknown): The raw credential to be checked. - * - * Returns: - * - Returns true if the document is a raw credential, false otherwise. + * Available validation functions: + * - isRawDocument(document): Checks if document is a valid raw credential + * - isSignedDocument(document): Checks if document is a valid signed credential + * - isRawDocumentV1_1(document): Checks if document is a valid v1.1 raw credential + * - isRawDocumentV2_0(document): Checks if document is a valid v2.0 raw credential + * - isSignedDocumentV1_1(document): Checks if document is a valid v1.1 signed credential + * - isSignedDocumentV2_0(document): Checks if document is a valid v2.0 signed credential + * - isDerived(document): Checks if document is a derived (selective disclosure) credential */ const document = { - ... -} + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/data-integrity/v2", + "https://w3id.org/citizenship/v2" + ], + "type": ["VerifiableCredential"], + "issuer": "did:web:example.com", + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": { + "id": "did:example:123", + "type": ["Person"], + "name": "John Doe" + } +}; + +// Basic validation +console.log('Is raw document:', isRawDocument(document)); // true +console.log('Is signed document:', isSignedDocument(document)); // false (no proof) + +// Version-specific validation +console.log('Is v1.1 raw document:', isRawDocumentV1_1(document)); // false +console.log('Is v2.0 raw document:', isRawDocumentV2_0(document)); // true + +// Check if credential is derived (selective disclosure) +const signedDocument = { ...document, proof: { /* proof object */ } }; +console.log('Is derived credential:', await isDerived(signedDocument)); +``` + +## Migration from Legacy BbsBlsSignature2020 + +If you're migrating from legacy BbsBlsSignature2020 signatures: -const result1 = isRawDocument(document); -console.log('isRawDocument', result1); +1. **Update Key Pairs**: Use Multikey format instead of Bls12381G2Key2020 +2. **Change Cryptosuite**: Use 'ecdsa-sd-2023' or 'bbs-2023' instead of 'BbsBlsSignature2020' +3. **Update Contexts**: Use W3C VC Data Model v2.0 contexts +4. **Modify Derivation**: Use JSON pointer arrays instead of ContextDocument format +5. **Update Verification Flow**: Derive credentials before verification for modern cryptosuites -const result2 = isSignedDocument(document); -console.log('isSignedDocument', result2); +```ts +// Legacy BbsBlsSignature2020 (deprecated) +const legacyKeyPair = { + type: "Bls12381G2Key2020", + privateKeyBase58: "...", + publicKeyBase58: "..." +}; + +// Modern approach +const modernKeyPair = { + type: "Multikey", + secretKeyMultibase: "...", + publicKeyMultibase: "..." +}; ``` + +## Best Practices + +1. **Use Modern Cryptosuites**: Prefer 'ecdsa-sd-2023' (default) or 'bbs-2023' over legacy BbsBlsSignature2020 +2. **Implement Proper Error Handling**: Always check for errors in function results +3. **Derive Before Verify**: For modern cryptosuites, always derive credentials before verification +4. **Use Mandatory Pointers**: Specify fields that should always be revealed in selective disclosure +5. **Version Detection**: Use helper functions to detect credential versions when needed +6. **Single Derivation**: Remember that modern cryptosuites support only single-round derivation + +## License + +This project is licensed under the Apache 2.0 License. diff --git a/packages/w3c-vc/package.json b/packages/w3c-vc/package.json index 3cb3de55..3d7392e4 100644 --- a/packages/w3c-vc/package.json +++ b/packages/w3c-vc/package.json @@ -31,12 +31,20 @@ } }, "dependencies": { + "@digitalbazaar/bls12-381-multikey": "^2.1.0", + "@digitalbazaar/bbs-2023-cryptosuite": "^2.0.1", + "@digitalbazaar/data-integrity": "^2.5.0", + "@digitalbazaar/ecdsa-multikey": "^1.8.0", + "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.4.1", "@mattrglobal/jsonld-signatures-bbs": "^1.2.0", - "@trustvc/w3c-credential-status": "^1.2.13", - "@trustvc/w3c-issuer": "^1.2.4", + "@trustvc/w3c-credential-status": "^1.3.0-alpha.13", + "@trustvc/w3c-issuer": "^1.3.0-alpha.10", + "base64url-universal": "^2.0.0", + "cbor": "^9.0.2", "did-resolver": "^4.1.0", "jsonld": "^6.0.0", - "jsonld-signatures": "7.0.0", + "jsonld-signatures": "^11.5.0", + "jsonld-signatures-v7": "npm:jsonld-signatures@7.0.0", "uuid": "^10.0.0" }, "overrides": { diff --git a/packages/w3c-vc/src/index.ts b/packages/w3c-vc/src/index.ts index 75c05c4d..22d4f74e 100644 --- a/packages/w3c-vc/src/index.ts +++ b/packages/w3c-vc/src/index.ts @@ -5,6 +5,11 @@ import { isSignedDocument, signCredential, verifyCredential, + isRawDocumentV1_1, + isRawDocumentV2_0, + isSignedDocumentV1_1, + isSignedDocumentV2_0, + isDerived, } from './lib/w3c-vc'; import { getDocumentLoader } from '@trustvc/w3c-context'; @@ -25,4 +30,9 @@ export { signCredential, verifyCredential, verifyCredentialStatus, + isRawDocumentV1_1, + isRawDocumentV2_0, + isSignedDocumentV1_1, + isSignedDocumentV2_0, + isDerived, }; diff --git a/packages/w3c-vc/src/lib/__fixtures__/bbs2020-credentials.ts b/packages/w3c-vc/src/lib/__fixtures__/bbs2020-credentials.ts new file mode 100644 index 00000000..3aa4010f --- /dev/null +++ b/packages/w3c-vc/src/lib/__fixtures__/bbs2020-credentials.ts @@ -0,0 +1,296 @@ +import { SignedVerifiableCredential } from '../types'; + +export const bbs2020CredentialV1_1: SignedVerifiableCredential = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/bbs/v1', + 'https://w3id.org/vc/status-list/2021/v1', + 'https://trustvc.io/context/transferable-records-context.json', + 'https://trustvc.io/context/render-method-context.json', + 'https://trustvc.io/context/attachments-context.json', + 'https://trustvc.io/context/qrcode-context.json', + 'https://trustvc.io/context/bill-of-lading.json', + ], + qrCode: { type: 'TrustVCQRCode', uri: 'https://localhost:3000/qrcode' }, + credentialStatus: { + type: 'TransferableRecords', + tokenNetwork: { chain: 'MATIC', chainId: '80001' }, + tokenRegistry: '0xE0a94770B8e969B5D9179d6dA8730B01e19279e2', + tokenId: '0f6cda4d977bae4a92f55133cd64926a8b91d4239984dc1184dc9337463c2687', + }, + credentialSubject: { + billOfLadingName: 'TrustVC Bill of Lading', + scac: 'SGPU', + blNumber: 'SGCNM21566325', + vessel: 'vessel', + voyageNo: 'voyageNo', + portOfLoading: 'Singapore', + portOfDischarge: 'Paris', + carrierName: 'A.P. Moller', + placeOfReceipt: 'Beijing', + placeOfDelivery: 'Singapore', + packages: [ + { packagesDescription: 'package 1', packagesWeight: '10', packagesMeasurement: '20' }, + { packagesDescription: 'package 2', packagesWeight: '10', packagesMeasurement: '20' }, + ], + shipperName: 'Shipper Name', + shipperAddressStreet: '101 ORCHARD ROAD', + shipperAddressCountry: 'SINGAPORE', + consigneeName: 'Consignee name', + notifyPartyName: 'Notify Party Name', + links: 'https://localhost:3000/url', + attachments: [ + { data: 'BASE64_ENCODED_FILE', filename: 'sample1.pdf', mimeType: 'application/pdf' }, + { data: 'BASE64_ENCODED_FILE', filename: 'sample2.pdf', mimeType: 'application/pdf' }, + ], + type: ['BillOfLading'], + }, + renderMethod: [ + { + id: 'https://localhost:3000/renderer', + type: 'EMBEDDED_RENDERER', + templateName: 'BILL_OF_LADING', + }, + ], + expirationDate: '2029-12-03T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + type: ['VerifiableCredential'], + issuanceDate: '2024-04-01T12:19:52Z', + id: 'urn:bnid:_:01994c0f-c112-7446-b6a9-38a6bfbcfc5a', + proof: { + type: 'BbsBlsSignature2020', + created: '2025-09-15T06:28:45Z', + proofPurpose: 'assertionMethod', + proofValue: + 't43CydjSQLq5cLDxdnBf9c27ijWARBCW98RqoAn5WZzWIVaHhAmhUAhKXVelsIDXBqdyy/7UdRyvqvWX+lMz3n1Jvp+5o/lD1ckOxrWuG2c6vSa3nqU6MAxVvq9dOnUeAgQTrkpXhXuwNoYtR33vFQ==', + verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1', + }, +}; + +export const bbs2020DerivedCredentialV1_1: SignedVerifiableCredential = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/bbs/v1', + 'https://w3id.org/vc/status-list/2021/v1', + 'https://trustvc.io/context/transferable-records-context.json', + 'https://trustvc.io/context/render-method-context.json', + 'https://trustvc.io/context/attachments-context.json', + 'https://trustvc.io/context/qrcode-context.json', + 'https://trustvc.io/context/bill-of-lading.json', + ], + id: 'urn:bnid:_:01994c0f-c112-7446-b6a9-38a6bfbcfc5a', + type: 'VerifiableCredential', + qrCode: { + id: 'urn:bnid:_:c14n6', + type: 'TrustVCQRCode', + uri: 'https://localhost:3000/qrcode', + }, + renderMethod: [ + { + '@id': 'urn:bnid:_:c14n2', + id: 'https://localhost:3000/renderer', + templateName: 'BILL_OF_LADING', + type: 'EMBEDDED_RENDERER', + }, + ], + credentialStatus: { + id: 'urn:bnid:_:c14n0', + type: 'TransferableRecords', + tokenId: '0f6cda4d977bae4a92f55133cd64926a8b91d4239984dc1184dc9337463c2687', + tokenNetwork: { id: 'urn:bnid:_:c14n1', chain: 'MATIC', chainId: '80001' }, + tokenRegistry: '0xE0a94770B8e969B5D9179d6dA8730B01e19279e2', + }, + credentialSubject: { + id: 'urn:bnid:_:c14n5', + type: 'BillOfLading', + billOfLadingName: 'TrustVC Bill of Lading', + blNumber: 'SGCNM21566325', + shipperName: 'Shipper Name', + packages: [ + { + id: 'urn:bnid:_:c14n3', + packagesDescription: 'package 1', + packagesMeasurement: '20', + packagesWeight: '10', + }, + { + id: 'urn:bnid:_:c14n8', + packagesDescription: 'package 2', + packagesMeasurement: '20', + packagesWeight: '10', + }, + ], + attachments: [ + { + id: 'urn:bnid:_:c14n4', + data: 'BASE64_ENCODED_FILE', + filename: 'sample2.pdf', + mimeType: 'application/pdf', + }, + { + id: 'urn:bnid:_:c14n7', + data: 'BASE64_ENCODED_FILE', + filename: 'sample1.pdf', + mimeType: 'application/pdf', + }, + ], + }, + expirationDate: '2029-12-03T12:19:52Z', + issuanceDate: '2024-04-01T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + proof: { + type: 'BbsBlsSignatureProof2020', + created: '2025-09-15T06:28:45Z', + nonce: 'PT3EpqO7oPMhsS3jW8Q8FeEqooe10H00APHM/ub1HMuGYlASd6SdrHvQefjwRSTYRxs=', + proofPurpose: 'assertionMethod', + proofValue: + 'ADgA//IAP////4C4R21VMhthwYh3LFiS2DEM4ZhiK+/0PXiGJdiiH4ezNSPWE72ib2akIxoKCG4KfpNOnAa0fOnxB4NP3+CqOeMCOy+zkw6NoEEwW1rQpZ71nGmx/0zPvultMUvVyyrgVa8XYW2lXuKk0nV7/CNeQB8KRhyVProFTLQTL+/Fi4IbBnGwXDdhkefhnoJXVkWv7QAAAHSja8XIvjo7HnfkXsYJNEejfsJfGeXY5q+/flm4s3evvnD4vLGFYreumGMblT4lGGkAAAACWETb3IlZvmWZcK7Gi0b4wKj2E6xPNw5Kw8FZ6rvKTMEK6ywNvxGMYSx5jR3JmvgOZuykA9nnsfw062F5mZkPspcdx/58XIl/QzKqDly5/5o5Rg16Tjx8noZ4v8+OIpRMs6YFuZx8GKDvElvRh8jvbgAAAA8mO/dQhW8va9SDksr5JlSNZOlQ6/0vMWiR9bRHDqe5eDF6aGrgqbuwcw1AcveclumDy4JqMGB9Qn9mBRZSLhbIGphtQd1h7MvlIC8nMsZQq320I/zjS3btmnwFal3OXLc5nV7AgAOuMlfqtTl8MYSf93kMDjL6unfHMU5gPEd0UwXkLBsfEt0zrv9bRgG++EOMw4OtOjZ96n5GrfhqCRfZbfUnJRBtH/5F/EOeUmAveILJ08TUezJC/Z1vDJ3Moog5jQrxtHJ/U2AKbKvsow1K8BWn6PLDoSwYWYK3M1+Wcyf8DnM0TTZKhdcSPe45ZawktgMg74uIkg4Kbt/lPQFWcX8mwLAzukkuXYT3XhPIF88EzKtcNic/BAHZJFMzzQthNZFPuTX+3GDsDErxyEgZnedeHKZpfQdGzo/u9SoMTzjLmSrDTxeSG8J90eL4Jov0YvW7IcNg+vk56m36OFWHOaDb+SDY3GZGdSODyfWGyrMuwcVd/LG9ArVq2iodstEMhTAW9HS7B6fz5zp5eD0/de6rsORn7XkOHqsW3vj94VXikfUKow3z4MH0nJxTG4W1OPZn2vgxaXuCeQx8Yb/FQwUo+p3t5WTRIRI/Mw7XtFbfGJ6nY5wSDsmRsCvFd94=', + verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1', + }, +}; + +export const bbs2020CredentialV2_0: SignedVerifiableCredential = { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/bbs/v1', + 'https://trustvc.io/context/transferable-records-context.json', + 'https://trustvc.io/context/render-method-context-v2.json', + 'https://trustvc.io/context/attachments-context.json', + 'https://trustvc.io/context/qrcode-context.json', + 'https://trustvc.io/context/bill-of-lading.json', + ], + qrCode: { type: 'TrustVCQRCode', uri: 'https://localhost:3000/qrcode' }, + credentialStatus: { + type: 'TransferableRecords', + tokenNetwork: { chain: 'MATIC', chainId: '80001' }, + tokenRegistry: '0xE0a94770B8e969B5D9179d6dA8730B01e19279e2', + tokenId: 'f247599c416a3d55423495ef87cb188776e43553e91dcaeae5e60740719a1976', + }, + credentialSubject: { + billOfLadingName: 'TrustVC Bill of Lading', + scac: 'SGPU', + blNumber: 'SGCNM21566325', + vessel: 'vessel', + voyageNo: 'voyageNo', + portOfLoading: 'Singapore', + portOfDischarge: 'Paris', + carrierName: 'A.P. Moller', + placeOfReceipt: 'Beijing', + placeOfDelivery: 'Singapore', + packages: [ + { packagesDescription: 'package 1', packagesWeight: '10', packagesMeasurement: '20' }, + { packagesDescription: 'package 2', packagesWeight: '10', packagesMeasurement: '20' }, + ], + shipperName: 'Shipper Name', + shipperAddressStreet: '101 ORCHARD ROAD', + shipperAddressCountry: 'SINGAPORE', + consigneeName: 'Consignee name', + notifyPartyName: 'Notify Party Name', + links: 'https://localhost:3000/url', + attachments: [ + { data: 'BASE64_ENCODED_FILE', filename: 'sample1.pdf', mimeType: 'application/pdf' }, + { data: 'BASE64_ENCODED_FILE', filename: 'sample2.pdf', mimeType: 'application/pdf' }, + ], + type: ['BillOfLading'], + }, + renderMethod: [ + { + id: 'https://localhost:3000/renderer', + type: 'EMBEDDED_RENDERER', + templateName: 'BILL_OF_LADING', + }, + ], + validUntil: '2029-12-03T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + type: ['VerifiableCredential'], + validFrom: '2024-04-01T12:19:52Z', + id: 'urn:bnid:_:01995667-2960-722f-9869-959a7fd07150', + proof: { + type: 'BbsBlsSignature2020', + created: '2025-09-17T06:40:25Z', + proofPurpose: 'assertionMethod', + proofValue: + 'p7vUXYbUqm7i5QDQKRoprc/q/yqNNAqNTA6xSa7XjORZ03u4S8APmU+9cIrV4ZMrB0sky130H7U+VSQVJfpFd9LGuZ6XC32nJkR3pbhxPzwdY+b5HaUFcGQQ3NJQAxz5xmqkSKY7WXiPqFKf/Beuaw==', + verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1', + }, +}; + +export const bbs2020DerivedCredentialV2_0: SignedVerifiableCredential = { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/bbs/v1', + 'https://trustvc.io/context/transferable-records-context.json', + 'https://trustvc.io/context/render-method-context-v2.json', + 'https://trustvc.io/context/attachments-context.json', + 'https://trustvc.io/context/qrcode-context.json', + 'https://trustvc.io/context/bill-of-lading.json', + ], + id: 'urn:bnid:_:01995667-2960-722f-9869-959a7fd07150', + type: 'VerifiableCredential', + qrCode: { + id: 'urn:bnid:_:c14n3', + type: 'TrustVCQRCode', + uri: 'https://localhost:3000/qrcode', + }, + renderMethod: [ + { + id: 'https://localhost:3000/renderer', + templateName: 'BILL_OF_LADING', + type: 'EMBEDDED_RENDERER', + }, + ], + credentialStatus: { + id: 'urn:bnid:_:c14n0', + type: 'TransferableRecords', + tokenId: 'f247599c416a3d55423495ef87cb188776e43553e91dcaeae5e60740719a1976', + tokenNetwork: { id: 'urn:bnid:_:c14n1', chain: 'MATIC', chainId: '80001' }, + tokenRegistry: '0xE0a94770B8e969B5D9179d6dA8730B01e19279e2', + }, + credentialSubject: { + id: 'urn:bnid:_:c14n5', + type: 'BillOfLading', + billOfLadingName: 'TrustVC Bill of Lading', + blNumber: 'SGCNM21566325', + shipperName: 'Shipper Name', + packages: [ + { + id: 'urn:bnid:_:c14n2', + packagesDescription: 'package 1', + packagesMeasurement: '20', + packagesWeight: '10', + }, + { + id: 'urn:bnid:_:c14n7', + packagesDescription: 'package 2', + packagesMeasurement: '20', + packagesWeight: '10', + }, + ], + attachments: [ + { + id: 'urn:bnid:_:c14n4', + data: 'BASE64_ENCODED_FILE', + filename: 'sample2.pdf', + mimeType: 'application/pdf', + }, + { + id: 'urn:bnid:_:c14n6', + data: 'BASE64_ENCODED_FILE', + filename: 'sample1.pdf', + mimeType: 'application/pdf', + }, + ], + }, + validUntil: '2029-12-03T12:19:52Z', + validFrom: '2024-04-01T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + proof: { + type: 'BbsBlsSignatureProof2020', + created: '2025-09-17T06:40:25Z', + nonce: 'AVF+U0Wv7Un4OLmZ1JdGSh52zkmorNOncBs2MzuFn+BI7HeIVDQdLsusV57u7ghyWOw=', + proofPurpose: 'assertionMethod', + proofValue: + 'ADd/5AB/////qcx//X8Ll0Y76W4/xUFxxfy8RtX7h6IAVyHknG39MSR2sXKJ2HofXrBZU7aKCos0kvT5fUFr2KfuFL2bynKTQWYoXrXs+xLoxw6OvlvSNYuBq43B780lqgaoC0E8fKb+grA5rXOl7bklgLQxyinq9RTQJOKS7i6nBi6Fo0heBHCx6RX43dxdI0rvAg9QYwmPAAAAdJjk71Wh4YYVRZOPUU5CPdFaVXHce4ZuOzoGhTzgxVF+J44ak+JnGpv3FpaGNahX5AAAAAJOEoIWKyJcaQ3FoNnPtrW6XK4Ce2+wHb6vBIhhRaG0fGFjbsPU2SBWcfPXTdr2V60xidu0A3QC+73cN/hW9RfwpaBQ9KhFQijSFjWku5ZpxUxavAcliupG8rLAVeRRueGsfg08gsFcScyrAwrVBYwAAAAADwsnudyX1IcmJGcmO92k9SfTvnln2TQ0zpZE7TFdkaMCXrkAIpjmP0bZLAdsyo2Zi6R1R08MZJDgRoRYgAevhSIwdtRe0XkendSriPpRwCNheCZi1Rkoh5u8LJX+FdfuZ1g+MdxEdDeKzIjHPHJo4E+2bxT2eqSby0ZHDjxLSBkKHvkjjdc7P92zUP397lKSJ7MxlKN7fQ/2YUaygjDaLsIbfD/76sjJQyBJDo2tGXys/GaJ1aasp1FRf3QUGr1VIhNlpl2OA3wsMdFOnvgueEn122aF0GX1VWECxqDQPnkNHHC5WKdexFxxdUyXiVG5agIH+ajApOM0TUgwpKuZUpkd20v3SD5g5gPuaw5gpEbKjJuuBjlauD8jUeb8l6iu2DeDTgCDeuEU0Ie0MHJFWFR06yDR7RkOsLmo4Dqdg7MaIRXEkgvo/MULe4sn5bAwzG4nycQ8Zc0bIz4tKBCm5rE+m4P3RuQ7AONyzf9BpRH0rradIlHVfKpub+g+AnoDKy+OSOd2kTwgfKt5s/R+8NW6WDhdA8Br+poi83QdFwnyGJwiuGHTEx1H2oWTIKLXmAUaKj5VczzkhFv0pPDiacsoKskKjBdtP+/DbMZ1YXN0x8aFlddoLpQbC9m5ng3B/g==', + verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1', + }, +}; diff --git a/packages/w3c-vc/src/lib/__fixtures__/key-pairs.ts b/packages/w3c-vc/src/lib/__fixtures__/key-pairs.ts new file mode 100644 index 00000000..6735f9db --- /dev/null +++ b/packages/w3c-vc/src/lib/__fixtures__/key-pairs.ts @@ -0,0 +1,34 @@ +import { + Bbs2023PrivateKeyPair, + BBSPrivateKeyPair, + EcdsaSd2023PrivateKeyPair, + VerificationType, +} from '@trustvc/w3c-issuer'; + +export const bbs2020KeyPair: BBSPrivateKeyPair = { + id: 'did:web:trustvc.github.io:did:1#keys-1', + controller: 'did:web:trustvc.github.io:did:1', + type: VerificationType.Bls12381G2Key2020, + publicKeyBase58: + 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ', + privateKeyBase58: '4LDU56PUhA9ZEutnR1qCWQnUhtLtpLu2EHSq4h1o7vtF', +}; + +export const ecdsa2023KeyPair: EcdsaSd2023PrivateKeyPair = { + '@context': 'https://w3id.org/security/multikey/v1', + id: 'did:web:trustvc.github.io:did:1#multikey-1', + type: VerificationType.Multikey, + controller: 'did:web:trustvc.github.io:did:1', + publicKeyMultibase: 'zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc', + secretKeyMultibase: 'z42tmUXTVn3n9BihE6NhdMpvVBTnFTgmb6fw18o5Ud6puhRW', +}; + +export const bbs2023KeyPair: Bbs2023PrivateKeyPair = { + '@context': 'https://w3id.org/security/multikey/v1', + id: 'did:web:trustvc.github.io:did:1#multikey-2', + type: VerificationType.Multikey, + controller: 'did:web:trustvc.github.io:did:1', + publicKeyMultibase: + 'zUC7HnpncVAkTjtL6B8prX6bQM2WA5sJ7rXFeCqyrvPnrzoFBjYsVUTNwzhhPUazja73tWwPeEBWCUgq5qBSrtrXiYhVvBCgZPTCiWANj7TSiZJ6SnyC3pkt94GiuChhAvmRRbt', + secretKeyMultibase: 'z488ur1KSFDd3Y1L6pXcPrZRjE18PNBhgzwJvMeoSxKPNysj', +}; diff --git a/packages/w3c-vc/src/lib/__fixtures__/modern-credentials.ts b/packages/w3c-vc/src/lib/__fixtures__/modern-credentials.ts new file mode 100644 index 00000000..12854f2f --- /dev/null +++ b/packages/w3c-vc/src/lib/__fixtures__/modern-credentials.ts @@ -0,0 +1,126 @@ +import { VerifiableCredential } from '../types'; + +// Modern credential for v1.1 testing (used by both ECDSA-SD-2023 and BBS-2023) +export const modernCredentialV1_1: VerifiableCredential = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2', + 'https://w3id.org/vc/status-list/2021/v1', + 'https://trustvc.io/context/transferable-records-context.json', + 'https://trustvc.io/context/render-method-context.json', + 'https://trustvc.io/context/attachments-context.json', + 'https://trustvc.io/context/qrcode-context.json', + 'https://trustvc.io/context/bill-of-lading.json', + ], + qrCode: { + type: 'TrustVCQRCode', + uri: 'https://localhost:3000/qrcode', + }, + credentialStatus: { + type: 'TransferableRecords', + tokenNetwork: { + chain: 'MATIC', + chainId: '80001', + }, + tokenRegistry: '0xE0a94770B8e969B5D9179d6dA8730B01e19279e2', + }, + credentialSubject: { + billOfLadingName: 'TrustVC Bill of Lading', + scac: 'SGPU', + blNumber: 'SGCNM21566325', + vessel: 'vessel', + voyageNo: 'voyageNo', + portOfLoading: 'Singapore', + portOfDischarge: 'Paris', + carrierName: 'A.P. Moller', + placeOfReceipt: 'Beijing', + placeOfDelivery: 'Singapore', + packages: [ + { packagesDescription: 'package 1', packagesWeight: '10', packagesMeasurement: '20' }, + { packagesDescription: 'package 2', packagesWeight: '10', packagesMeasurement: '20' }, + ], + shipperName: 'Shipper Name', + shipperAddressStreet: '101 ORCHARD ROAD', + shipperAddressCountry: 'SINGAPORE', + consigneeName: 'Consignee name', + notifyPartyName: 'Notify Party Name', + links: 'https://localhost:3000/url', + attachments: [ + { data: 'BASE64_ENCODED_FILE', filename: 'sample1.pdf', mimeType: 'application/pdf' }, + { data: 'BASE64_ENCODED_FILE', filename: 'sample2.pdf', mimeType: 'application/pdf' }, + ], + type: ['BillOfLading'], + }, + renderMethod: [ + { + id: 'https://localhost:3000/renderer', + type: 'EMBEDDED_RENDERER', + templateName: 'BILL_OF_LADING', + }, + ], + expirationDate: '2029-12-03T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + type: ['VerifiableCredential'], +}; + +// Modern credential for v2.0 testing (used by both ECDSA-SD-2023 and BBS-2023) +export const modernCredentialV2_0: VerifiableCredential = { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'https://w3id.org/security/data-integrity/v2', + 'https://trustvc.io/context/transferable-records-context.json', + 'https://trustvc.io/context/render-method-context-v2.json', + 'https://trustvc.io/context/attachments-context.json', + 'https://trustvc.io/context/qrcode-context.json', + 'https://trustvc.io/context/bill-of-lading.json', + ], + qrCode: { + type: 'TrustVCQRCode', + uri: 'https://localhost:3000/qrcode', + }, + credentialStatus: { + type: 'TransferableRecords', + tokenNetwork: { + chain: 'MATIC', + chainId: '80001', + }, + tokenRegistry: '0xE0a94770B8e969B5D9179d6dA8730B01e19279e2', + }, + credentialSubject: { + billOfLadingName: 'TrustVC Bill of Lading', + scac: 'SGPU', + blNumber: 'SGCNM21566325', + vessel: 'vessel', + voyageNo: 'voyageNo', + portOfLoading: 'Singapore', + portOfDischarge: 'Paris', + carrierName: 'A.P. Moller', + placeOfReceipt: 'Beijing', + placeOfDelivery: 'Singapore', + packages: [ + { packagesDescription: 'package 1', packagesWeight: '10', packagesMeasurement: '20' }, + { packagesDescription: 'package 2', packagesWeight: '10', packagesMeasurement: '20' }, + ], + shipperName: 'Shipper Name', + shipperAddressStreet: '101 ORCHARD ROAD', + shipperAddressCountry: 'SINGAPORE', + consigneeName: 'Consignee name', + notifyPartyName: 'Notify Party Name', + links: 'https://localhost:3000/url', + attachments: [ + { data: 'BASE64_ENCODED_FILE', filename: 'sample1.pdf', mimeType: 'application/pdf' }, + { data: 'BASE64_ENCODED_FILE', filename: 'sample2.pdf', mimeType: 'application/pdf' }, + ], + type: ['BillOfLading'], + }, + renderMethod: [ + { + id: 'https://localhost:3000/renderer', + type: 'EMBEDDED_RENDERER', + templateName: 'BILL_OF_LADING', + }, + ], + validUntil: '2029-12-03T12:19:52Z', + issuer: 'did:web:trustvc.github.io:did:1', + type: ['VerifiableCredential'], +}; diff --git a/packages/w3c-vc/src/lib/__fixtures__/test-scenarios.ts b/packages/w3c-vc/src/lib/__fixtures__/test-scenarios.ts new file mode 100644 index 00000000..5001357a --- /dev/null +++ b/packages/w3c-vc/src/lib/__fixtures__/test-scenarios.ts @@ -0,0 +1,85 @@ +import { CryptoSuiteName, SignedVerifiableCredential, VerifiableCredential } from '../types'; +import { bbs2020KeyPair, bbs2023KeyPair, ecdsa2023KeyPair } from './key-pairs'; +import { modernCredentialV1_1, modernCredentialV2_0 } from './modern-credentials'; +import { + bbs2020CredentialV1_1, + bbs2020DerivedCredentialV1_1, + bbs2020CredentialV2_0, + bbs2020DerivedCredentialV2_0, +} from './bbs2020-credentials'; +import { + Bbs2023PrivateKeyPair, + BBSPrivateKeyPair, + EcdsaSd2023PrivateKeyPair, +} from '@trustvc/w3c-issuer'; + +export interface BBS2020TestScenario { + cryptosuite: CryptoSuiteName; + keyPair: BBSPrivateKeyPair; + version: string; + credential: SignedVerifiableCredential; + derivedCredential: SignedVerifiableCredential; +} + +export interface ModernCryptosuiteTestScenario { + cryptosuite: CryptoSuiteName; + keyPair: EcdsaSd2023PrivateKeyPair | Bbs2023PrivateKeyPair; + version: string; + credential: VerifiableCredential; + dateField: string; + dateValue: string; +} + +// Parameterized test data for BBS2020 (legacy cryptosuite) +export const bbs2020TestScenarios: BBS2020TestScenario[] = [ + { + cryptosuite: 'BbsBlsSignature2020' as CryptoSuiteName, + keyPair: bbs2020KeyPair, + version: 'v1.1', + credential: bbs2020CredentialV1_1, + derivedCredential: bbs2020DerivedCredentialV1_1, + }, + { + cryptosuite: 'BbsBlsSignature2020' as CryptoSuiteName, + keyPair: bbs2020KeyPair, + version: 'v2.0', + credential: bbs2020CredentialV2_0, + derivedCredential: bbs2020DerivedCredentialV2_0, + }, +]; + +// Parameterized test data for modern cryptosuites (ECDSA-SD-2023 and BBS-2023) +export const modernCryptosuiteTestScenarios: ModernCryptosuiteTestScenario[] = [ + { + cryptosuite: 'ecdsa-sd-2023' as CryptoSuiteName, + keyPair: ecdsa2023KeyPair, + version: 'v1.1', + credential: modernCredentialV1_1, + dateField: 'issuanceDate', + dateValue: '2024-04-01T12:19:52Z', + }, + { + cryptosuite: 'ecdsa-sd-2023' as CryptoSuiteName, + keyPair: ecdsa2023KeyPair, + version: 'v2.0', + credential: modernCredentialV2_0, + dateField: 'validFrom', + dateValue: '2024-04-01T12:19:52Z', + }, + { + cryptosuite: 'bbs-2023' as CryptoSuiteName, + keyPair: bbs2023KeyPair, + version: 'v1.1', + credential: modernCredentialV1_1, + dateField: 'issuanceDate', + dateValue: '2024-04-01T12:19:52Z', + }, + { + cryptosuite: 'bbs-2023' as CryptoSuiteName, + keyPair: bbs2023KeyPair, + version: 'v2.0', + credential: modernCredentialV2_0, + dateField: 'validFrom', + dateValue: '2024-04-01T12:19:52Z', + }, +]; diff --git a/packages/w3c-vc/src/lib/helper/index.ts b/packages/w3c-vc/src/lib/helper/index.ts index cdfb58da..71c7751e 100644 --- a/packages/w3c-vc/src/lib/helper/index.ts +++ b/packages/w3c-vc/src/lib/helper/index.ts @@ -1,14 +1,20 @@ import { CredentialContextVersion } from '@trustvc/w3c-context'; import { + assertBitstringStatusListEntry, assertCredentialStatusType, assertStatusList2021Entry, assertTransferableRecords, BitstringStatusListCredentialStatus, TransferableRecordsCredentialStatus, } from '@trustvc/w3c-credential-status'; -import { BBSPrivateKeyPair, PrivateKeyPair, VerificationType } from '@trustvc/w3c-issuer'; +import { + Bbs2023PrivateKeyPair, + BBSPrivateKeyPair, + EcdsaSd2023PrivateKeyPair, + PrivateKeyPair, + VerificationType, +} from '@trustvc/w3c-issuer'; import { createHash } from 'crypto'; -// @ts-ignore: No types available for jsonld import * as jsonld from 'jsonld'; import { v7 as uuidv7 } from 'uuid'; import { assertCredentialStatuses } from '../sign/credentialStatus'; @@ -19,6 +25,7 @@ import { proofTypeMapping, RawVerifiableCredential, VerifiableCredential, + CredentialSchema, } from '../types'; /** @@ -35,6 +42,7 @@ export function _checkKeyPair(keyPair: PrivateKeyPair) { if (!keyPair.id) { throw new Error('"id" property in keyPair is required.'); } + if (keyPair.type === VerificationType.Bls12381G2Key2020) { if (!(keyPair as BBSPrivateKeyPair).privateKeyBase58) { throw new Error('"privateKeyBase58" property in keyPair is required.'); @@ -42,6 +50,15 @@ export function _checkKeyPair(keyPair: PrivateKeyPair) { if (!(keyPair as BBSPrivateKeyPair).publicKeyBase58) { throw new Error('"publicKeyBase58" property in keyPair is required.'); } + } else if (keyPair.type === VerificationType.Multikey) { + // For Multikey types (BBS-2023 and ECDSA-SD-2023), check for multibase keys + const multikeyPair = keyPair as Bbs2023PrivateKeyPair | EcdsaSd2023PrivateKeyPair; + if (!multikeyPair.secretKeyMultibase) { + throw new Error('"secretKeyMultibase" property in keyPair is required.'); + } + if (!multikeyPair.publicKeyMultibase) { + throw new Error('"publicKeyMultibase" property in keyPair is required.'); + } } } @@ -66,7 +83,14 @@ function _getId(obj: T | string): string | undefined // These properties of a Verifiable Credential (VC) must be objects containing a type field // if they are present in the VC. -const mustHaveType = ['proof', 'credentialStatus']; +const mustHaveType = [ + 'proof', + 'credentialStatus', + 'credentialSchema', + 'termsOfUse', + 'refreshService', + 'evidence', +]; // Regular expression to validate date-time format according to XML schema. // Z and T must be uppercase @@ -101,6 +125,17 @@ function assertDateString({ } } +/** + * Retrieves the first value from the `@context` property of a Verifiable Credential. + * + * @param {object} credential - The Verifiable Credential object from which to extract the context. + * @returns {string} The first context value as a string. + */ +export function getFirstContext(credential: VerifiableCredential): string { + const v = jsonld.getValues(credential, '@context'); + return (Array.isArray(v) && v.length ? v[0] : credential['@context']) as string; +} + /** * Checks if the first element of the '@context' field in the credential is the expected value. * Returns true if the context is valid, otherwise false. @@ -109,8 +144,14 @@ function assertDateString({ * @throws {Error} If the context is invalid. */ function assertCredentialContext(credential: VerifiableCredential): void { - if (credential['@context'][0] !== CredentialContextVersion.v1) { - throw new Error(`The first element of '@context' must be '${CredentialContextVersion.v1}'`); + const firstContext = getFirstContext(credential); + if ( + firstContext !== CredentialContextVersion.v1 && + firstContext !== CredentialContextVersion.v2 + ) { + throw new Error( + `The first element of '@context' must be either '${CredentialContextVersion.v1}' (v1.1) or '${CredentialContextVersion.v2}' (v2.0).`, + ); } } @@ -164,39 +205,91 @@ export function _checkCredential( _validateUriId({ id: issuer, propertyName: 'issuer' }); } - // Validate issuanceDate field - if (!credential.issuanceDate) { - throw new Error('"issuanceDate" property is required.'); - } - assertDateString({ credential, prop: 'issuanceDate' }); + // Validate date fields - support both v1.1 and v2.0 formats + const firstContext = getFirstContext(credential); + const isV2 = firstContext === CredentialContextVersion.v2; - // Ensure issuanceDate has only one value - if (jsonld.getValues(credential, 'issuanceDate').length > 1) { - throw new Error('"issuanceDate" property can only have one value.'); - } + if (isV2) { + _checkCredentialSchemas(credential); + + // v2.0 format: validFrom is optional, validUntil is optional + if ('validFrom' in credential) { + assertDateString({ credential, prop: 'validFrom' }); + if (jsonld.getValues(credential, 'validFrom').length > 1) { + throw new Error('"validFrom" property can only have one value.'); + } + } + + if ('validUntil' in credential) { + assertDateString({ credential, prop: 'validUntil' }); + if (jsonld.getValues(credential, 'validUntil').length > 1) { + throw new Error('"validUntil" property can only have one value.'); + } + + if (mode === 'verify') { + if (now > new Date(credential.validUntil)) { + console.warn('Credential has expired.'); + // throw new Error('Credential has expired.'); + } + } + } + // Validate temporal relationship between validFrom and validUntil + if ('validFrom' in credential && 'validUntil' in credential) { + const validFromDate = new Date(credential.validFrom); + const validUntilDate = new Date(credential.validUntil); + + if (validFromDate > validUntilDate) { + throw new Error('validFrom must be temporally the same or earlier than validUntil'); + } + } + + // Check if the current date is before the validFrom date during verification + if (mode === 'verify' && credential.validFrom) { + const validFromDate = new Date(credential.validFrom); + if (now < validFromDate) { + throw new Error( + `The current date time (${now.toISOString()}) is before the ` + + `"validFrom" (${credential.validFrom}).`, + ); + } + } + } else { + // v1.1 format: issuanceDate is required + if (!credential.issuanceDate) { + throw new Error('"issuanceDate" property is required.'); + } + assertDateString({ credential, prop: 'issuanceDate' }); - // Optionally validate expirationDate field if it exists - if ('expirationDate' in credential) { - assertDateString({ credential, prop: 'expirationDate' }); + // Ensure issuanceDate has only one value + if (jsonld.getValues(credential, 'issuanceDate').length > 1) { + throw new Error('"issuanceDate" property can only have one value.'); + } + + // Optionally validate expirationDate field if it exists + if ('expirationDate' in credential) { + assertDateString({ credential, prop: 'expirationDate' }); + if (mode === 'verify') { + if (now > new Date(credential.expirationDate)) { + console.warn('Credential has expired.'); + // throw new Error('Credential has expired.'); + } + } + } + + // Check if the current date is before the issuance date during verification if (mode === 'verify') { - if (now > new Date(credential.expirationDate)) { - console.warn('Credential has expired.'); - // throw new Error('Credential has expired.'); + const issuanceDate = new Date(credential.issuanceDate); + if (now < issuanceDate) { + throw new Error( + `The current date time (${now.toISOString()}) is before the ` + + `"issuanceDate" (${credential.issuanceDate}).`, + ); } } } - // Check if the current date is before the issuance date during verification + // Validate proof field for presence and type during verification if (mode === 'verify') { - const issuanceDate = new Date(credential.issuanceDate); - if (now < issuanceDate) { - throw new Error( - `The current date time (${now.toISOString()}) is before the ` + - `"issuanceDate" (${credential.issuanceDate}).`, - ); - } - - // Validate proof field for presence and type during verification if (!credential.proof) { throw new Error('"proof" property is required.'); } @@ -216,8 +309,12 @@ export function _checkCredential( throw new Error('"proof" property is already there.'); } - // The "id" is generated programmatically later on - if (credential.id) { + // The "id" is generated programmatically later on, except for status list credentials which require it + const isStatusListCredential = credential.type?.some( + (type: string) => + type === 'BitstringStatusListCredential' || type === 'StatusList2021Credential', + ); + if (credential.id && !isStatusListCredential) { throw new Error('"id" is a defined field and should not be set by the user.'); } } @@ -272,12 +369,12 @@ function _checkCredentialSubjects(credential: VerifiableCredential): void { if (Array.isArray(credential?.credentialSubject)) { credential?.credentialSubject.map((subject: CredentialSubject) => - _checkCredentialSubject({ subject }), + _checkCredentialSubject(subject), ); return; } - _checkCredentialSubject({ subject: credential?.credentialSubject }); + _checkCredentialSubject(credential?.credentialSubject); } /** @@ -302,6 +399,56 @@ function _checkCredentialSubject(subject: CredentialSubject): void { }); } } +/** + * Validates the credentialSchema field in a Verifiable Credential. + * Throws an error if the field is missing or invalid. + * + * @param {VerifiableCredential} credential - The Verifiable Credential object. + * @throws {Error} If the credentialSchema field is missing or invalid. + */ +function _checkCredentialSchemas(credential: VerifiableCredential): void { + if ('credentialSchema' in credential) { + const schemas = Array.isArray(credential.credentialSchema) + ? credential.credentialSchema + : [credential.credentialSchema]; + + for (const schema of schemas) { + _checkCredentialSchema(schema); + } + } +} + +/** + * Validates a credential schema object to ensure it contains valid properties. + * Throws an error if the credential schema is not valid. + * + * @param {CredentialSchema} schema - The credential schema object to validate. + * @throws {Error} If the credential schema is invalid. + */ +function _checkCredentialSchema(schema: CredentialSchema): void { + // Validate credentialSchema if present + + if (typeof schema !== 'object' || schema === null) { + throw new Error('credentialSchema must be an object'); + } + + if (!schema.id) { + throw new Error( + 'credentialSchema objects must have an id property that is a URL identifying the schema file', + ); + } + + if (typeof schema.id !== 'string') { + throw new Error('credentialSchema id property must be a URL string'); + } + + // Basic URL validation for schema id + try { + new URL(schema.id); + } catch (e) { + throw new Error('credentialSchema id property must be a valid URL'); + } +} /** * Checks if the provided value is a valid object. @@ -375,6 +522,8 @@ export const _checkCredentialStatus = ( if (type === 'StatusList2021Entry') { assertStatusList2021Entry(credentialStatus as BitstringStatusListCredentialStatus); + } else if (type === 'BitstringStatusListEntry') { + assertBitstringStatusListEntry(credentialStatus as BitstringStatusListCredentialStatus); } else if (type === 'TransferableRecords') { assertTransferableRecords(credentialStatus as TransferableRecordsCredentialStatus, mode); } else { @@ -384,15 +533,33 @@ export const _checkCredentialStatus = ( /** * Prefills the credential ID with a UUIDv7. + * Uses blank node format for BBS+ compatibility and proper URI format for ECDSA-SD-2023. * If the credentialStatus is present with type TransferableRecords, set the tokenId. * * @param {RawVerifiableCredential} credential + * @param {string} cryptoSuite - The cryptosuite being used for signing * @returns {RawVerifiableCredential} */ export const prefilCredentialId = ( credential: RawVerifiableCredential, + cryptoSuite?: string, ): RawVerifiableCredential => { - credential.id = `urn:bnid:_:${uuidv7()}`; + // Don't overwrite existing id for status list credentials types + const isStatusListCredential = credential.type?.some( + (type: string) => + type === 'BitstringStatusListCredential' || type === 'StatusList2021Credential', + ); + if (credential.id && isStatusListCredential) { + // Keep the existing id for status list credentials + } else { + // Use proper URI format for ECDSA-SD-2023 and BBS-2023 + // Use blank node format for BBS+ (maintains backward compatibility) + if (cryptoSuite === 'ecdsa-sd-2023' || cryptoSuite === 'bbs-2023') { + credential.id = `urn:uuid:${uuidv7()}`; + } else { + credential.id = `urn:bnid:_:${uuidv7()}`; + } + } if (credential?.credentialStatus?.type === 'TransferableRecords') { credential.credentialStatus = { diff --git a/packages/w3c-vc/src/lib/sign/credentialStatus/index.ts b/packages/w3c-vc/src/lib/sign/credentialStatus/index.ts index 2a27bf95..753ab013 100644 --- a/packages/w3c-vc/src/lib/sign/credentialStatus/index.ts +++ b/packages/w3c-vc/src/lib/sign/credentialStatus/index.ts @@ -1,4 +1,3 @@ -// @ts-ignore: No types available for jsonld import * as jsonld from 'jsonld'; import { _checkCredentialStatus, _validateUriId } from '../../helper'; import { CredentialStatus, VerifiableCredential } from '../../types'; diff --git a/packages/w3c-vc/src/lib/types.ts b/packages/w3c-vc/src/lib/types.ts index 8439b906..97f5ba46 100644 --- a/packages/w3c-vc/src/lib/types.ts +++ b/packages/w3c-vc/src/lib/types.ts @@ -1,4 +1,5 @@ import { BbsBlsSignature2020, BbsBlsSignatureProof2020 } from '@mattrglobal/jsonld-signatures-bbs'; +import { DataIntegrityProof } from '@digitalbazaar/data-integrity'; // Define the type for the signing result export interface SigningResult { @@ -23,13 +24,42 @@ export type CredentialStatus = { type: string; } & Record; export type CredentialStatuses = CredentialStatus | CredentialStatus[]; - +export type CredentialSchema = { + id: string; + type: string; +} & Record; +export type CredentialSchemas = CredentialSchema | CredentialSchema[]; export type CredentialSubject = Record; export type CredentialSubjects = CredentialSubject | CredentialSubject[]; +export type TermsOfUse = { + type: string; +} & Record; +export type TermsOfUses = TermsOfUse | TermsOfUse[]; + +type RelatedResource = { + id: string; + mediaType?: string; +} & ( + | { digestSRI: string; digestMultibase?: string } + | { digestSRI?: string; digestMultibase: string } +) & + Record; +export type RelatedResources = RelatedResource | RelatedResource[]; + +export type RefreshService = { + type: string; +} & Record; +export type RefreshServices = RefreshService | RefreshService[]; + +export type Evidence = { + type: string; +} & Record; +export type Evidences = Evidence | Evidence[]; + export type Proof = { type: string; - created: string; + created?: string; proofPurpose: string; verificationMethod: string; proofValue: string; @@ -43,12 +73,17 @@ export type SignedVerifiableCredential = { id: string; type: string | string[]; issuer: string | Record; - issuanceDate: string; + issuanceDate?: string; validFrom?: string; validUntil?: string; expirationDate?: string; credentialStatus?: CredentialStatuses; credentialSubject: CredentialSubjects; + credentialSchema?: CredentialSchemas; + termsOfUse?: TermsOfUses; + evidence?: Evidences; + relatedResource?: RelatedResources; + refreshService?: RefreshServices; renderMethod?: Record; qrCode?: Record; proof?: Proof; @@ -56,12 +91,15 @@ export type SignedVerifiableCredential = { export type RawVerifiableCredential = Omit; -export type ProofType = 'BbsBlsSignature2020' | 'BbsBlsSignatureProof2020'; +export type CryptoSuiteName = 'BbsBlsSignature2020' | 'bbs-2023' | 'ecdsa-sd-2023'; + +export type ProofType = 'BbsBlsSignature2020' | 'BbsBlsSignatureProof2020' | 'DataIntegrityProof'; export const proofTypeMapping: Record< ProofType, - typeof BbsBlsSignature2020 | typeof BbsBlsSignatureProof2020 + typeof BbsBlsSignature2020 | typeof BbsBlsSignatureProof2020 | typeof DataIntegrityProof > = { BbsBlsSignature2020: BbsBlsSignature2020, BbsBlsSignatureProof2020: BbsBlsSignatureProof2020, + DataIntegrityProof: DataIntegrityProof, }; diff --git a/packages/w3c-vc/src/lib/verify/credentialStatus/index.test.ts b/packages/w3c-vc/src/lib/verify/credentialStatus/index.test.ts index 5fb565b2..64a50704 100644 --- a/packages/w3c-vc/src/lib/verify/credentialStatus/index.test.ts +++ b/packages/w3c-vc/src/lib/verify/credentialStatus/index.test.ts @@ -1,12 +1,12 @@ import * as w3c_credential_status from '@trustvc/w3c-credential-status'; import { CredentialStatusPurpose, CredentialStatusType } from '@trustvc/w3c-credential-status'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi, afterEach } from 'vitest'; import { SignedVerifiableCredential } from '../../types'; import * as w3c_vc from './../../w3c-vc'; import { verifyCredentialStatus } from './index'; // First 10 (index 0 - 9) position is marked as True. -const credentialStatusVC: SignedVerifiableCredential = { +const credentialStatusVC_BBS_V1: SignedVerifiableCredential = { '@context': [ 'https://www.w3.org/2018/credentials/v1', 'https://w3id.org/security/bbs/v1', @@ -33,7 +33,7 @@ const credentialStatusVC: SignedVerifiableCredential = { }, }; -const credentialStatusVC_withInvalidEncodedList = { +const credentialStatusVC_withInvalidEncodedList: SignedVerifiableCredential = { '@context': [ 'https://www.w3.org/2018/credentials/v1', 'https://w3id.org/security/bbs/v1', @@ -68,10 +68,22 @@ const credentialStatus = { statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/1', }; +const credentialStatus2 = { + id: 'https://trustvc.github.io/did/credentials/statuslist/2#1', + type: 'BitstringStatusListEntry' as CredentialStatusType, + statusPurpose: 'revocation' as CredentialStatusPurpose, + statusListIndex: '5', + statusListCredential: 'https://trustvc.github.io/did/credentials/statuslist/2', +}; + +afterEach(() => { + vi.restoreAllMocks(); +}); + describe('verifyCredentialStatus', () => { it('should verify a credential status successfully', async () => { vi.spyOn(w3c_credential_status, 'fetchCredentialStatusVC').mockResolvedValue( - credentialStatusVC, + credentialStatusVC_BBS_V1, ); vi.spyOn(w3c_vc, 'verifyCredential').mockResolvedValue({ verified: true }); @@ -95,7 +107,7 @@ describe('verifyCredentialStatus', () => { it('should return an error if the credential is not verified', async () => { vi.spyOn(w3c_credential_status, 'fetchCredentialStatusVC').mockResolvedValue( - credentialStatusVC, + credentialStatusVC_BBS_V1, ); vi.spyOn(w3c_vc, 'verifyCredential').mockResolvedValue({ verified: false }); @@ -117,7 +129,7 @@ describe('verifyCredentialStatus', () => { it('should return an error if statusPurpose does not match the statusPurpose in the VC', async () => { vi.spyOn(w3c_credential_status, 'fetchCredentialStatusVC').mockResolvedValue( - credentialStatusVC, + credentialStatusVC_BBS_V1, ); vi.spyOn(w3c_vc, 'verifyCredential').mockResolvedValue({ verified: true }); @@ -161,7 +173,7 @@ describe('verifyCredentialStatus', () => { it('should return an error if statusListIndex is out of range', async () => { vi.spyOn(w3c_credential_status, 'fetchCredentialStatusVC').mockResolvedValue( - credentialStatusVC, + credentialStatusVC_BBS_V1, ); vi.spyOn(w3c_vc, 'verifyCredential').mockResolvedValue({ verified: true }); @@ -172,4 +184,17 @@ describe('verifyCredentialStatus', () => { expect(error).toBe('Invalid statusListIndex: Index out of range: min=0, max=131071'); }); + + it('should verify a credential status successfully with ECDSA-SD-2023 and v2.0 context', async () => { + const { status } = await verifyCredentialStatus(credentialStatus2); + expect(status).toBe(true); + + // Test with different index to verify it returns false + const { status: status2, purpose } = await verifyCredentialStatus({ + ...credentialStatus2, + statusListIndex: '10', + }); + expect(status2).toBe(false); + expect(purpose).toBe('revocation'); + }); }); diff --git a/packages/w3c-vc/src/lib/verify/credentialStatus/index.ts b/packages/w3c-vc/src/lib/verify/credentialStatus/index.ts index 5d1bf256..31addf1d 100644 --- a/packages/w3c-vc/src/lib/verify/credentialStatus/index.ts +++ b/packages/w3c-vc/src/lib/verify/credentialStatus/index.ts @@ -9,7 +9,7 @@ import { } from '@trustvc/w3c-credential-status'; import { _checkCredentialStatus } from '../../helper'; import { CredentialStatus, CredentialStatusResult } from './types'; -import { verifyCredential } from '../../w3c-vc'; +import { deriveCredential, verifyCredential } from '../../w3c-vc'; import { SignedVerifiableCredential } from '../../types'; import { DocumentLoader } from '@trustvc/w3c-context'; @@ -52,14 +52,27 @@ export const verifyCredentialStatus = async ( assertStatusListIndexWithinRange(bitstringStatusList, index); // Check if the statusListCredential is valid - const vcStatusListVerificationResult = await verifyCredential(vcStatusList, options); + const DERIVE_CREDENTIAL_ERROR = 'Use deriveCredential() first'; + let vcStatusListVerificationResult = await verifyCredential(vcStatusList, options); + + // Handle ECDSA-SD-2023 base credentials that need derivation before verification + if ( + !vcStatusListVerificationResult?.verified && + vcStatusListVerificationResult.error?.includes(DERIVE_CREDENTIAL_ERROR) + ) { + const derivedResult = await deriveCredential(vcStatusList, []); + vcStatusListVerificationResult = await verifyCredential(derivedResult.derived, options); + } + + // Throw error if verification still fails if (!vcStatusListVerificationResult?.verified) { - console.error( - `Failed to verify Credential Status VC: ${vcStatusListVerificationResult.verified}. Error: ${vcStatusListVerificationResult.error}`, - ); - throw new Error( - `Failed to verify Credential Status VC: ${vcStatusListVerificationResult.verified}`, - ); + const errorMessage = `Failed to verify Credential Status VC: ${vcStatusListVerificationResult.verified}`; + const detailedError = vcStatusListVerificationResult.error + ? `. Error: ${vcStatusListVerificationResult.error}` + : ''; + + console.error(errorMessage + detailedError); + throw new Error(errorMessage); } const status = bitstringStatusList.getStatus(index); diff --git a/packages/w3c-vc/src/lib/w3c-vc.test.ts b/packages/w3c-vc/src/lib/w3c-vc.test.ts index b903aee0..68a07d13 100644 --- a/packages/w3c-vc/src/lib/w3c-vc.test.ts +++ b/packages/w3c-vc/src/lib/w3c-vc.test.ts @@ -1,240 +1,319 @@ -import { VerificationType } from '@trustvc/w3c-issuer'; import { describe, expect, it } from 'vitest'; import { deriveCredential, signCredential, verifyCredential } from './w3c-vc'; +import { + modernCryptosuiteTestScenarios, + bbs2020TestScenarios, +} from './__fixtures__/test-scenarios'; -const modifiedCredential: any = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/security/bbs/v1', - 'https://w3id.org/vc/status-list/2021/v1', - 'https://trustvc.io/context/transferable-records-context.json', - 'https://trustvc.io/context/render-method-context.json', - 'https://trustvc.io/context/attachments-context.json', - 'https://trustvc.io/context/qrcode-context.json', - 'https://trustvc.io/context/bill-of-lading.json', - ], - qrCode: { - type: 'TrustVCQRCode', - uri: 'https://localhost:3000/qrcode', - }, - credentialStatus: { - type: 'TransferableRecords', - tokenNetwork: { - chain: 'MATIC', - chainId: '80001', - }, - tokenRegistry: '0xE0a94770B8e969B5D9179d6dA8730B01e19279e2', - }, - credentialSubject: { - billOfLadingName: 'TrustVC Bill of Lading', - scac: 'SGPU', - blNumber: 'SGCNM21566325', - vessel: 'vessel', - voyageNo: 'voyageNo', - portOfLoading: 'Singapore', - portOfDischarge: 'Paris', - carrierName: 'A.P. Moller', - placeOfReceipt: 'Beijing', - placeOfDelivery: 'Singapore', - packages: [ - { packagesDescription: 'package 1', packagesWeight: '10', packagesMeasurement: '20' }, - { packagesDescription: 'package 2', packagesWeight: '10', packagesMeasurement: '20' }, - ], - shipperName: 'Shipper Name', - shipperAddressStreet: '101 ORCHARD ROAD', - shipperAddressCountry: 'SINGAPORE', - consigneeName: 'Consignee name', - notifyPartyName: 'Notify Party Name', - links: 'https://localhost:3000/url', - attachments: [ - { data: 'BASE64_ENCODED_FILE', filename: 'sample1.pdf', mimeType: 'application/pdf' }, - { data: 'BASE64_ENCODED_FILE', filename: 'sample2.pdf', mimeType: 'application/pdf' }, - ], - type: ['BillOfLading'], - }, - renderMethod: [ - { - id: 'https://localhost:3000/renderer', - type: 'EMBEDDED_RENDERER', - templateName: 'BILL_OF_LADING', - }, - ], - expirationDate: '2029-12-03T12:19:52Z', - issuer: 'did:web:trustvc.github.io:did:1', - type: ['VerifiableCredential'], -}; - -const revealDocument: any = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/security/bbs/v1', - 'https://w3id.org/vc/status-list/2021/v1', - 'https://trustvc.io/context/transferable-records-context.json', - 'https://trustvc.io/context/render-method-context.json', - 'https://trustvc.io/context/attachments-context.json', - 'https://trustvc.io/context/qrcode-context.json', - 'https://trustvc.io/context/bill-of-lading.json', - ], - credentialSubject: { - type: ['BillOfLading'], - '@explicit': true, - billOfLadingName: {}, - blNumber: {}, - packages: {}, - shipperName: {}, - attachments: {}, - }, - type: ['VerifiableCredential'], -}; - -const modifiedKeyPair: any = { - id: 'did:web:trustvc.github.io:did:1#keys-1', - controller: 'did:web:trustvc.github.io:did:1', - type: VerificationType.Bls12381G2Key2020, - publicKeyBase58: - 'oRfEeWFresvhRtXCkihZbxyoi2JER7gHTJ5psXhHsdCoU1MttRMi3Yp9b9fpjmKh7bMgfWKLESiK2YovRd8KGzJsGuamoAXfqDDVhckxuc9nmsJ84skCSTijKeU4pfAcxeJ', -}; - -describe('Credential Signing and Verification', () => { - it('should successfully sign, derive and verify a credential', async () => { - let signingKeyPair = modifiedKeyPair; - signingKeyPair = { - ...signingKeyPair, - privateKeyBase58: '4LDU56PUhA9ZEutnR1qCWQnUhtLtpLu2EHSq4h1o7vtF', - }; - - let credential = modifiedCredential; - credential = { ...credential, issuanceDate: '2024-04-01T12:19:52Z' }; - - const signedCredential = await signCredential(credential, signingKeyPair); - expect(signedCredential.signed).toBeDefined(); - expect(signedCredential.error).toBeUndefined(); - - let verificationResult = await verifyCredential(signedCredential.signed); - expect(verificationResult.verified).toBe(true); - expect(verificationResult.error).toBeUndefined(); - - const derivedCredential = await deriveCredential(signedCredential.signed, revealDocument); - expect(derivedCredential.derived).toBeDefined(); - expect(derivedCredential.error).toBeUndefined(); - - verificationResult = await verifyCredential(derivedCredential.derived); - expect(verificationResult.verified).toBe(true); - expect(verificationResult.error).toBeUndefined(); - }); +/** + * W3C Verifiable Credentials Test Suite + * + * This test suite covers: + * - Modern cryptosuites: ECDSA-SD-2023 and BBS-2023 (full functionality) + * - Legacy BBS2020 cryptosuite (deprecation + verification) + */ +describe('W3C Verifiable Credentials', () => { + describe('Legacy BBS2020 Cryptosuite', () => { + describe.each(bbs2020TestScenarios)( + 'BBS2020 $version Operations (Deprecated)', + ({ cryptosuite, keyPair, credential, derivedCredential }) => { + it('should return deprecation error when signing', async () => { + // Remove proof to get unsigned credential + const { proof: _proof, ...testCredential } = credential; - it('should fail to sign if the private key is missing from the key pair', async () => { - let credential = modifiedCredential; - credential = { ...credential, issuanceDate: '2024-04-01T12:19:52Z' }; + // BBS2020 signing should return deprecation error + const signedCredential = await signCredential(testCredential, keyPair, cryptosuite); + expect(signedCredential.signed).toBeUndefined(); + expect(signedCredential.error).toBe( + 'BbsBlsSignature2020 is no longer supported. Please use the latest cryptosuite versions instead.', + ); + }); - const signedCredential = await signCredential(credential, modifiedKeyPair); - expect(signedCredential.signed).toBeUndefined(); - expect(signedCredential.error).toBeDefined(); - expect(signedCredential.error).equals('"privateKeyBase58" property in keyPair is required.'); - }); + it('should return deprecation error when deriving', async () => { + // BBS2020 derivation should return deprecation error + const revealDocument: any = { + '@context': credential['@context'], + credentialSubject: { + type: ['BillOfLading'], + '@explicit': true, + billOfLadingName: {}, + blNumber: {}, + }, + type: ['VerifiableCredential'], + }; + + const derivedResult = await deriveCredential(credential, revealDocument); + expect(derivedResult.derived).toBeUndefined(); + expect(derivedResult.error).toBe( + 'BbsBlsSignature2020 is no longer supported for derivation. Please use the latest cryptosuite versions instead.', + ); + }); + + it('should still verify existing credentials (backward compatibility)', async () => { + // Should still be able to verify existing BBS2020 credentials + const baseVerificationResult = await verifyCredential(credential); + expect(baseVerificationResult.verified).toBe(true); + expect(baseVerificationResult.error).toBeUndefined(); + + const derivedVerificationResult = await verifyCredential(derivedCredential); + expect(derivedVerificationResult.verified).toBe(true); + expect(derivedVerificationResult.error).toBeUndefined(); + }); + + it('should detect tampered credentials (comprehensive security)', async () => { + // Test tampering with base credential - modify credentialSubject + const tamperedBaseCredential = { + ...credential, + credentialSubject: { + ...credential.credentialSubject, + billOfLadingName: 'TAMPERED Bill of Lading', + }, + }; + + const baseVerificationResult = await verifyCredential(tamperedBaseCredential); + expect(baseVerificationResult.verified).toBe(false); + expect(baseVerificationResult.error).toBeDefined(); + + // Test tampering with base credential proof value + const tamperedProofCredential = { + ...credential, + proof: { + ...credential.proof, + proofValue: credential.proof.proofValue.replace(/A/g, 'B'), + }, + }; + + const proofVerificationResult = await verifyCredential(tamperedProofCredential); + expect(proofVerificationResult.verified).toBe(false); + expect(proofVerificationResult.error).toBeDefined(); + + // Test tampering with derived credential - modify credentialSubject + const tamperedDerivedCredential = { + ...derivedCredential, + credentialSubject: { + ...derivedCredential.credentialSubject, + billOfLadingName: 'TAMPERED Derived Bill of Lading', + }, + }; + + const derivedVerificationResult = await verifyCredential(tamperedDerivedCredential); + expect(derivedVerificationResult.verified).toBe(false); + expect(derivedVerificationResult.error).toBeDefined(); + + // Test tampering with derived credential proof value + const tamperedDerivedProofCredential = { + ...derivedCredential, + proof: { + ...derivedCredential.proof, + proofValue: derivedCredential.proof.proofValue.replace(/A/g, 'B'), + }, + }; - it('should fail to sign if the id field is present in the credential', async () => { - let signingKeyPair = modifiedKeyPair; - signingKeyPair = { - ...signingKeyPair, - privateKeyBase58: '4LDU56PUhA9ZEutnR1qCWQnUhtLtpLu2EHSq4h1o7vtF', - }; - - let credential = modifiedCredential; - credential = { - ...credential, - issuanceDate: '2024-04-01T12:19:52Z', - id: 'urn:uuid:0192d19c-d82c-7cc7-9431-cb495374f43b', - }; - - const signedCredential = await signCredential(credential, signingKeyPair); - expect(signedCredential.signed).toBeUndefined(); - expect(signedCredential.error).toBeDefined(); - expect(signedCredential.error).equals( - '"id" is a defined field and should not be set by the user.', + const derivedProofVerificationResult = await verifyCredential( + tamperedDerivedProofCredential, + ); + expect(derivedProofVerificationResult.verified).toBe(false); + expect(derivedProofVerificationResult.error).toBeDefined(); + }); + }, ); }); - it('should fail to sign if a required property in the credential is missing', async () => { - let signingKeyPair = modifiedKeyPair; - signingKeyPair = { - ...signingKeyPair, - privateKeyBase58: '4LDU56PUhA9ZEutnR1qCWQnUhtLtpLu2EHSq4h1o7vtF', - }; - - const signedCredential = await signCredential(modifiedCredential, signingKeyPair); - expect(signedCredential.signed).toBeUndefined(); - expect(signedCredential.error).toBeDefined(); - expect(signedCredential.error).equals('"issuanceDate" property is required.'); - }); + describe('Modern Cryptosuites (ECDSA-SD-2023 & BBS-2023)', () => { + describe.each(modernCryptosuiteTestScenarios)( + '$cryptosuite $version Credential Operations', + ({ cryptosuite, keyPair, credential, dateField, dateValue }) => { + it('should successfully sign, derive, and verify credentials', async () => { + const testCredential = { ...credential, [dateField]: dateValue }; - it('should fail verification if the signed credential is tampered with', async () => { - let signedCredential = modifiedCredential; - signedCredential = { - ...signedCredential, - id: 'urn:bnid:_:0193b647-66b6-7ffc-ae79-ef9c590f3301', - credentialStatus: { - ...signedCredential.credentialStatus, - tokenId: '398124e7f1ec797a3dea6322e5ce4ff5ee242ab6293c2acf41a95178dfb27dae', - }, - issuanceDate: '2024-04-01T12:19:53Z', - proof: { - type: 'BbsBlsSignature2020', - created: '2024-08-23T03:08:31Z', - proofPurpose: 'assertionMethod', - proofValue: - 'sHGTYvavHMHVJ3NgyEgiyDDa0IjG3wS9GChizckQHANWzcZRqBjD4uSZjxmS2fGzEgJJB6/JaL7FY9rx42Bkg/SRjvaaUiBVyOwUXeXUZMdlGEIpjzO8GDognziPqN7S9KEZagvnv3MESEx0EwvgEw==', - verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1', - }, - }; + // Sign the credential + const signedCredential = await signCredential(testCredential, keyPair, cryptosuite); + expect(signedCredential.signed).toBeDefined(); + expect(signedCredential.error).toBeUndefined(); + expect(signedCredential.signed?.proof?.type).toBe('DataIntegrityProof'); - const verificationResult = await verifyCredential(signedCredential); - expect(verificationResult.verified).toBe(false); - expect(verificationResult.error).equals('Invalid signature.'); - }); + // Derive the credential with selective disclosure + const selectivePointers = ['/credentialSubject/billOfLadingName']; + const derivedCredential = await deriveCredential( + signedCredential.signed, + selectivePointers, + ); + expect(derivedCredential.derived).toBeDefined(); + expect(derivedCredential.error).toBeUndefined(); - it('should fail verification if an unsupported signature suite is used', async () => { - let signedCredential = modifiedCredential; - signedCredential = { - ...signedCredential, - issuanceDate: '2024-04-01T12:19:52Z', - proof: { - type: 'Ed25519Signature2020', - created: '2024-08-23T03:08:31Z', - proofPurpose: 'assertionMethod', - proofValue: - 'sHGTYvavHMHVJ3NgyEgiyDDa0IjG3wS9GChizckQHANWzcZRqBjD4uSZjxmS2fGzEgJJB6/JaL7FY9rx42Bkg/SRjvaaUiBVyOwUXeXUZMdlGEIpjzO8GDognziPqN7S9KEZagvnv3MESEx0EwvgEw==', - verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1', - }, - }; + // Verify core mandatory pointers are included + expect(derivedCredential.derived?.issuer).toBeDefined(); + expect(derivedCredential.derived?.[dateField]).toBeDefined(); - const verificationResult = await verifyCredential(signedCredential); - expect(verificationResult.verified).toBe(false); - expect(verificationResult.error).equals('"proof" type is not of BbsBlsSignature2020.'); - }); + // Verify selective disclosure works + const credentialSubject = Array.isArray(derivedCredential.derived?.credentialSubject) + ? derivedCredential.derived?.credentialSubject[0] + : derivedCredential.derived?.credentialSubject; + expect(credentialSubject?.billOfLadingName).toBeDefined(); + expect(credentialSubject?.blNumber).toBeUndefined(); - it('should fail verification if the verification method cannot be found', async () => { - let signedCredential = modifiedCredential; - signedCredential = { - ...signedCredential, - id: 'urn:bnid:_:0193b647-66b6-7ffc-ae79-ef9c590f3301', - credentialStatus: { - ...signedCredential.credentialStatus, - tokenId: '398124e7f1ec797a3dea6322e5ce4ff5ee242ab6293c2acf41a95178dfb27dae', - }, - issuanceDate: '2024-04-01T12:19:52Z', - proof: { - type: 'BbsBlsSignature2020', - created: '2024-08-23T03:08:31Z', - proofPurpose: 'assertionMethod', - proofValue: - 'sHGTYvavHMHVJ3NgyEgiyDDa0IjG3wS9GChizckQHANWzcZRqBjD4uSZjxmS2fGzEgJJB6/JaL7FY9rx42Bkg/SRjvaaUiBVyOwUXeXUZMdlGEIpjzO8GDognziPqN7S9KEZagvnv3MESEx0EwvgEw==', - verificationMethod: 'did:web:trustvc.github.io:did:1#keys-2', - }, - }; + // Verify the derived credential + const verificationResult = await verifyCredential(derivedCredential.derived); + expect(verificationResult.verified).toBe(true); + expect(verificationResult.error).toBeUndefined(); + }); + + it('should support custom mandatory pointers with selective disclosure', async () => { + const testCredential = { ...credential, [dateField]: dateValue }; + + // Sign with custom mandatory pointers + const customMandatoryPointers = ['/credentialSubject/billOfLadingName']; + const signedCredential = await signCredential(testCredential, keyPair, cryptosuite, { + mandatoryPointers: customMandatoryPointers, + }); + expect(signedCredential.signed).toBeDefined(); + expect(signedCredential.error).toBeUndefined(); + + // Derive with selective disclosure + const selectivePointers = ['/credentialSubject/blNumber']; + const derivedCredential = await deriveCredential( + signedCredential.signed, + selectivePointers, + ); + expect(derivedCredential.derived).toBeDefined(); + expect(derivedCredential.error).toBeUndefined(); - const verificationResult = await verifyCredential(signedCredential); - expect(verificationResult.verified).toBe(false); - expect(verificationResult.error).equals('Verification method could not be found.'); + // Verify mandatory and selective pointers are included + const credentialSubject = Array.isArray(derivedCredential.derived?.credentialSubject) + ? derivedCredential.derived?.credentialSubject[0] + : derivedCredential.derived?.credentialSubject; + expect(credentialSubject?.billOfLadingName).toBeDefined(); // Mandatory + expect(credentialSubject?.blNumber).toBeDefined(); // Selected + expect(credentialSubject?.scac).toBeUndefined(); // Not selected + + // Verify the derived credential + const verificationResult = await verifyCredential(derivedCredential.derived); + expect(verificationResult.verified).toBe(true); + expect(verificationResult.error).toBeUndefined(); + }); + + it('should automatically include entire credentialSubject when no properties selected', async () => { + const testCredential = { ...credential, [dateField]: dateValue }; + + // Sign the credential + const signedCredential = await signCredential(testCredential, keyPair, cryptosuite); + expect(signedCredential.signed).toBeDefined(); + + // Derive with no credentialSubject pointers + const selectivePointers: string[] = []; + const derivedCredential = await deriveCredential( + signedCredential.signed, + selectivePointers, + ); + expect(derivedCredential.derived).toBeDefined(); + + // Verify entire credentialSubject is included + const credentialSubject = Array.isArray(derivedCredential.derived?.credentialSubject) + ? derivedCredential.derived?.credentialSubject[0] + : derivedCredential.derived?.credentialSubject; + expect(credentialSubject?.scac).toBeDefined(); + expect(credentialSubject?.vessel).toBeDefined(); + expect(credentialSubject?.billOfLadingName).toBeDefined(); + expect(credentialSubject?.blNumber).toBeDefined(); + + // Verify the derived credential + const verificationResult = await verifyCredential(derivedCredential.derived); + expect(verificationResult.verified).toBe(true); + expect(verificationResult.error).toBeUndefined(); + }); + + it('should require derivation before verification', async () => { + const testCredential = { ...credential, [dateField]: dateValue }; + + // Sign the credential + const signedCredential = await signCredential(testCredential, keyPair, cryptosuite); + expect(signedCredential.signed).toBeDefined(); + + // Verify original signed credential directly (should fail) + const verificationResult = await verifyCredential(signedCredential.signed); + expect(verificationResult.verified).toBe(false); + expect(verificationResult.error).toBe( + `${cryptosuite} base credentials must be derived before verification. Use deriveCredential() first.`, + ); + }); + + it('should prevent multiple rounds of derivation', async () => { + const testCredential = { ...credential, [dateField]: dateValue }; + + // Sign and first derivation + const signedCredential = await signCredential(testCredential, keyPair, cryptosuite); + const firstDerivedCredential = await deriveCredential(signedCredential.signed, [ + '/credentialSubject/blNumber', + ]); + expect(firstDerivedCredential.derived).toBeDefined(); + + // Attempt second derivation (should fail) + const secondDerivedCredential = await deriveCredential(firstDerivedCredential.derived, [ + '/credentialSubject/billOfLadingName', + ]); + expect(secondDerivedCredential.derived).toBeUndefined(); + expect(secondDerivedCredential.error).toBe( + `${cryptosuite} derived credentials cannot be further derived. Multiple rounds of derivation are not supported by this cryptosuite.`, + ); + }); + + it('should detect tampered credentials', async () => { + const testCredential = { ...credential, [dateField]: dateValue }; + + // Sign and derive credential + const signedCredential = await signCredential(testCredential, keyPair, cryptosuite); + const derivedCredential = await deriveCredential(signedCredential.signed, [ + '/credentialSubject/billOfLadingName', + ]); + + // Tamper with the derived credential + const tamperedCredential = { + ...derivedCredential.derived, + [dateField]: new Date().toISOString(), + }; + + // Verify tampered credential (should fail) + const verificationResult = await verifyCredential(tamperedCredential); + expect(verificationResult.verified).toBe(false); + expect(verificationResult.error).equals('Invalid signature.'); + }); + + it('should validate key pair requirements', async () => { + const testCredential = { ...credential, [dateField]: dateValue }; + + const invalidKeyPair: any = { + id: 'did:web:trustvc.github.io:did:1#multikey-1', + type: 'Multikey', + controller: 'did:web:trustvc.github.io:did:1', + publicKeyMultibase: 'zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc', + // Missing secretKeyMultibase + }; + + // Sign with invalid key pair (should fail) + const signedCredential = await signCredential( + testCredential, + invalidKeyPair, + cryptosuite, + ); + expect(signedCredential.signed).toBeUndefined(); + expect(signedCredential.error).toBe( + '"secretKeyMultibase" property in keyPair is required.', + ); + }); + + it('should reject unsupported cryptosuites', async () => { + const testCredential = { ...credential, [dateField]: dateValue }; + + // Sign with unsupported cryptosuite (should fail) + const signedCredential = await signCredential( + testCredential, + keyPair, + 'unsupported-suite' as any, + ); + expect(signedCredential.signed).toBeUndefined(); + expect(signedCredential.error).toBe('"unsupported-suite" is not supported.'); + }); + }, + ); }); }); diff --git a/packages/w3c-vc/src/lib/w3c-vc.ts b/packages/w3c-vc/src/lib/w3c-vc.ts index bf45d80c..c53dc034 100644 --- a/packages/w3c-vc/src/lib/w3c-vc.ts +++ b/packages/w3c-vc/src/lib/w3c-vc.ts @@ -1,18 +1,16 @@ -import { KeyPairOptions } from '@mattrglobal/bls12381-key-pair'; -import { - BbsBlsSignature2020, - BbsBlsSignatureProof2020, - Bls12381G2KeyPair, - deriveProof, -} from '@mattrglobal/jsonld-signatures-bbs'; -import { ContextDocument, DocumentLoader, getDocumentLoader } from '@trustvc/w3c-context'; +import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey'; +import * as ecdsaSd2023Cryptosuite from '@digitalbazaar/ecdsa-sd-2023-cryptosuite'; +import * as Bls12381Multikey from '@digitalbazaar/bls12-381-multikey'; +import * as bbs2023Cryptosuite from '@digitalbazaar/bbs-2023-cryptosuite'; +import { DataIntegrityProof } from '@digitalbazaar/data-integrity'; +import { CredentialContextVersion, DocumentLoader, getDocumentLoader } from '@trustvc/w3c-context'; import { PrivateKeyPair } from '@trustvc/w3c-issuer'; -// @ts-ignore: No types available for jsonld-signatures import jsonldSignatures from 'jsonld-signatures'; -// @ts-ignore: No types available for jsonld +import jsonldSignaturesV7 from 'jsonld-signatures-v7'; import * as jsonld from 'jsonld'; -import { _checkCredential, _checkKeyPair, prefilCredentialId } from './helper'; +import { _checkCredential, _checkKeyPair, getFirstContext, prefilCredentialId } from './helper'; import { + CryptoSuiteName, DerivedResult, ProofType, proofTypeMapping, @@ -22,6 +20,23 @@ import { VerificationResult, } from './types'; +const { + createSignCryptosuite: createEcdsaSd2023SignCryptosuite, + createDiscloseCryptosuite: createEcdsaSd2023DiscloseCryptosuite, + createVerifyCryptosuite: createEcdsaSd2023VerifyCryptosuite, +} = ecdsaSd2023Cryptosuite; +const { + createSignCryptosuite: createBbs2023SignCryptosuite, + createDiscloseCryptosuite: createBbs2023DiscloseCryptosuite, + createVerifyCryptosuite: createBbs2023VerifyCryptosuite, +} = bbs2023Cryptosuite; +const { + purposes: { AssertionProofPurpose }, +} = jsonldSignatures; +const { + purposes: { AssertionProofPurpose: AssertionProofPurposeV7 }, +} = jsonldSignaturesV7; + /** * Checks if the input document is a raw credential. * @param {RawVerifiableCredential | unknown} document - The raw credential to be checked. @@ -36,6 +51,22 @@ export const isRawDocument = (document: RawVerifiableCredential | unknown): bool return typeof document === 'object'; }; +/** + * Checks if the input document is a valid raw credential with context version 1.1. + * @param {RawVerifiableCredential | unknown} document - The document to check. + * @returns {boolean} - True if the document is a valid raw credential and uses v1.1 context, otherwise false. + */ +export const isRawDocumentV1_1 = (document: RawVerifiableCredential | unknown): boolean => + isRawDocument(document) && getFirstContext(document) === CredentialContextVersion.v1; + +/** + * Checks if the input document is a valid raw credential with context version 2.0. + * @param {RawVerifiableCredential | unknown} document - The document to check. + * @returns {boolean} - True if the document is a valid raw credential and uses v2.0 context, otherwise false. + */ +export const isRawDocumentV2_0 = (document: RawVerifiableCredential | unknown): boolean => + isRawDocument(document) && getFirstContext(document) === CredentialContextVersion.v2; + /** * Checks if the input document is a signed credential. * @param {SignedVerifiableCredential | unknown} document - The signed credential to be checked. @@ -53,37 +84,243 @@ export const isSignedDocument = ( }; /** - * Signs a credential using the specified cryptosuite. Defaults to 'BbsBlsSignature2020'. + * Checks if the input document is a valid signed credential with context version 1.1. + * @param {SignedVerifiableCredential | unknown} document - The document to check. + * @returns {boolean} - True if the document is a valid signed credential and uses v1.1 context, otherwise false. + */ +export const isSignedDocumentV1_1 = ( + document: SignedVerifiableCredential | unknown, +): document is SignedVerifiableCredential => + isSignedDocument(document) && getFirstContext(document) === CredentialContextVersion.v1; + +/** + * Checks if the input document is a valid signed credential with context version 2.0. + * @param {SignedVerifiableCredential | unknown} document - The document to check. + * @returns {boolean} - True if the document is a valid signed credential and uses v2.0 context, otherwise false. + */ +export const isSignedDocumentV2_0 = ( + document: SignedVerifiableCredential | unknown, +): document is SignedVerifiableCredential => + isSignedDocument(document) && getFirstContext(document) === CredentialContextVersion.v2; + +/** + * Checks if a proof value represents a base (non-derived) ECDSA-SD-2023 credential + * @param {string} proofValue - The proof value string to check + * @returns {boolean} - true if this is a base proof, false otherwise + */ +const isEcdsaSdBaseProof = async (proofValue: string): Promise => { + try { + if (!proofValue || !proofValue.startsWith('u')) { + return false; + } + // @ts-ignore: No types available for base64url-universal + const { decode } = await import('base64url-universal'); + const decoded = decode(proofValue.slice(1)); + // Check if it has the base proof header (0xd9, 0x5d, 0x00) + // Convert to numbers to handle both Buffer (Node.js) and Uint8Array (browser) environments + return ( + decoded.length >= 3 && + Number(decoded[0]) === 0xd9 && + Number(decoded[1]) === 0x5d && + Number(decoded[2]) === 0x00 + ); + } catch { + return false; + } +}; + +const isBbs2023BaseProof = async (proofValue: string): Promise => { + try { + if (!proofValue || !proofValue.startsWith('u')) { + return false; + } + // @ts-ignore: No types available for base64url-universal + const { decode } = await import('base64url-universal'); + const decoded = decode(proofValue.slice(1)); + // Check if it has the BBS-2023 base proof header (0xd9, 0x5d, and even third byte) + // Base proof headers: 0x02 (baseline), 0x04 (anonymous_holder_binding), 0x06 (pseudonym), 0x08 (holder_binding_pseudonym) + // Derived proof headers: 0x03, 0x05, 0x07 (odd numbers) + // Convert to numbers to handle both Buffer (Node.js) and Uint8Array (browser) environments + if (decoded.length < 3 || Number(decoded[0]) !== 0xd9 || Number(decoded[1]) !== 0x5d) { + return false; + } + + const thirdByte = Number(decoded[2]); + // Base proofs have even third bytes: 0x02, 0x04, 0x06, 0x08 + return thirdByte === 0x02 || thirdByte === 0x04 || thirdByte === 0x06 || thirdByte === 0x08; + } catch { + return false; + } +}; + +const baseProofDetectors = { + 'ecdsa-sd-2023': isEcdsaSdBaseProof, + 'bbs-2023': isBbs2023BaseProof, +} as const; + +type SupportedCryptosuite = keyof typeof baseProofDetectors; + +/** + * Determines whether a verifiable credential is a derived credential. + * + * Derived credentials are selective disclosure proofs that contain only a subset + * of the original credential's claims, rather than the full base credential. + * + * @param {SignedVerifiableCredential} document - The document to check. + * @returns {Promise} - True if the document is a derived credential, false otherwise. + */ +export const isDerived = async (document: SignedVerifiableCredential) => { + // BBS+ signatures always indicate derived credentials (selective disclosure proofs) + if (document.proof?.type === 'BbsBlsSignatureProof2020') { + return true; + } else if (document.proof?.type === 'DataIntegrityProof') { + // For Data Integrity Proofs, check the cryptosuite to determine the specific verification approach + const proof = jsonld.getValues(document, 'proof')[0]; + const cryptosuite = proof.cryptosuite; + + // ECDSA-SD-2023 and BBS-2023 cryptosuites + if (cryptosuite === 'ecdsa-sd-2023' || cryptosuite === 'bbs-2023') { + // Check if this is a base proof (original credential) or derived proof (selective disclosure) + if ( + await baseProofDetectors[cryptosuite as SupportedCryptosuite](proof.proofValue as string) + ) { + return false; // Base proof - contains all original claims + } else return true; // Derived proof - contains only selected claims + } + // Other Data Integrity cryptosuites are not derived + return false; + } + // No recognized proof type for selective disclosure + return false; +}; + +/** + * Extracts mandatory pointers from ECDSA-SD-2023 or BBS-2023 base proof values + * @param {string} proofValue - The base proof value string + * @param {string} cryptosuite - The cryptosuite type ('ecdsa-sd-2023' or 'bbs-2023') + * @returns {Promise} - Array of mandatory pointers, empty array if extraction fails + */ +const extractMandatoryPointers = async ( + proofValue: string, + cryptosuite: string, +): Promise => { + try { + // Check if this is a base proof for the specified cryptosuite + const isBaseProof = + cryptosuite === 'ecdsa-sd-2023' + ? await isEcdsaSdBaseProof(proofValue) + : await isBbs2023BaseProof(proofValue); + + if (!isBaseProof) { + return []; + } + + // Decode the base64url-no-pad-encoded value + const decodedProofValue = Buffer.from(proofValue.slice(1), 'base64url'); + + // CBOR decode the components after the 3-byte header + const cbor = await import('cbor'); + const components = cbor.decode(decodedProofValue.slice(3)); + + if (cryptosuite === 'ecdsa-sd-2023') { + // ECDSA-SD-2023 components: [baseSignature, publicKey, hmacKey, signatures, mandatoryPointers] + if (Array.isArray(components) && components.length === 5) { + return components[4] || []; + } + } else if (cryptosuite === 'bbs-2023') { + // BBS-2023 components: [baseSignature, publicKey, hmacKey, signatures, mandatoryPointers] + // Note: BBS-2023 has the same structure as ECDSA-SD-2023 for mandatory pointers + if (Array.isArray(components) && components.length === 5) { + return components[4] || []; + } + } + + return []; + } catch (error) { + // If we can't parse the mandatory pointers, return empty array + return []; + } +}; + +/** + * Signs a credential using the specified cryptosuite. Defaults to 'ecdsa-sd-2023'. * @param {object} credential - The credential to be signed. * @param {object} keyPair - The key pair options for signing. - * @param {string} cryptoSuite - The cryptosuite to be used for signing. Defaults to 'BbsBlsSignature2020'. + * @param {string} cryptoSuite - The cryptosuite to be used for signing. Defaults to 'ecdsa-sd-2023'. + * @param {object} options - Optional parameters including documentLoader and mandatoryPointers. * @returns {Promise} The signed credential or an error message in case of failure. */ export const signCredential = async ( credential: RawVerifiableCredential, keyPair: PrivateKeyPair, - cryptoSuite = 'BbsBlsSignature2020', + cryptoSuite: CryptoSuiteName = 'ecdsa-sd-2023', options?: { documentLoader?: DocumentLoader; + mandatoryPointers?: string[]; }, ): Promise => { try { + const documentLoader = options?.documentLoader ?? (await getDocumentLoader()); + if (cryptoSuite === 'BbsBlsSignature2020') { + return { + error: + 'BbsBlsSignature2020 is no longer supported. Please use the latest cryptosuite versions instead.', + }; + } else if (cryptoSuite === 'ecdsa-sd-2023' || cryptoSuite === 'bbs-2023') { _checkKeyPair(keyPair); _checkCredential(credential, undefined, 'sign'); - const documentLoader = options?.documentLoader ?? (await getDocumentLoader()); - const key = new Bls12381G2KeyPair(keyPair as KeyPairOptions); + // Import the key pair object into a usable signer instance + const keyPairInstance = + cryptoSuite === 'ecdsa-sd-2023' + ? await EcdsaMultikey.from({ ...keyPair }) + : await Bls12381Multikey.from({ ...keyPair }); + + // Determine required mandatory pointers based on credential format + const firstContext = credential['@context'][0]; + const isV2 = firstContext === CredentialContextVersion.v2; + + // Core mandatory pointers for fields required for credential validity + const coreMandatoryPointers = ['/issuer']; + + // Add date field pointer based on credential version + if (isV2) { + // For v2.0, validFrom is optional but if present should be mandatory for consistency + if (credential.validFrom) { + coreMandatoryPointers.push('/validFrom'); + } + } else { + // For v1.1, issuanceDate is required + coreMandatoryPointers.push('/issuanceDate'); + } + + // Combine core mandatory pointers with user-provided ones, ensuring core fields are always included + const userMandatoryPointers = options?.mandatoryPointers || []; + const mandatoryPointers = [ + ...coreMandatoryPointers, + ...userMandatoryPointers.filter((pointer) => !coreMandatoryPointers.includes(pointer)), + ]; + + // Create the DataIntegrityProof suite using the appropriate cryptosuite + const cryptosuiteInstance = + cryptoSuite === 'ecdsa-sd-2023' + ? createEcdsaSd2023SignCryptosuite({ mandatoryPointers }) + : createBbs2023SignCryptosuite({ mandatoryPointers }); + + const suite = new DataIntegrityProof({ + signer: keyPairInstance.signer(), + cryptosuite: cryptosuiteInstance, + }); // This ensures each credential has a distinct, system-generated ID in the UUIDv7 format - credential = prefilCredentialId(credential); + credential = prefilCredentialId(credential, cryptoSuite); const signed = await jsonldSignatures.sign(credential, { - suite: new BbsBlsSignature2020({ key }), - purpose: new jsonldSignatures.purposes.AssertionProofPurpose(), + suite, + purpose: new AssertionProofPurpose(), documentLoader, }); - return { signed: signed }; } else { return { error: `"${cryptoSuite}" is not supported.` }; @@ -108,8 +345,9 @@ export const signCredential = async ( }; /** - * Verifies a credential using the BBS+ signature scheme. + * Verifies a credential using BBS+ signature scheme, ECDSA-SD-2023, or BBS-2023. * @param {object} credential - The credential to be verified. + * @param {object} options - Optional parameters including documentLoader. * @returns {Promise} The verification result indicating success * or failure, along with an error message if applicable. */ @@ -127,12 +365,59 @@ export const verifyCredential = async ( const proofType = jsonld.getValues(credential, 'proof')[0].type as ProofType; const SuiteClass = proofTypeMapping[proofType]; - if (SuiteClass) - verificationResult = await jsonldSignatures.verify(credential, { - suite: new SuiteClass(), - purpose: new jsonldSignatures.purposes.AssertionProofPurpose(), - documentLoader, - }); + if (SuiteClass) { + if (proofType === 'DataIntegrityProof') { + // Check the cryptosuite to determine the specific verification approach + const proof = jsonld.getValues(credential, 'proof')[0]; + const cryptosuite = proof.cryptosuite; + + if (cryptosuite === 'ecdsa-sd-2023' || cryptosuite === 'bbs-2023') { + // Check if this is a base credential (non-derived) by examining the proofValue structure + const isBaseCredential = await baseProofDetectors[cryptosuite as SupportedCryptosuite]( + proof.proofValue as string, + ); + + if (isBaseCredential) { + // This is a base proof - modern cryptosuites require derivation before verification + return { + verified: false, + error: `${cryptosuite} base credentials must be derived before verification. Use deriveCredential() first.`, + }; + } + + // Create the appropriate verification cryptosuite + const verifyCryptosuite = + cryptosuite === 'ecdsa-sd-2023' + ? createEcdsaSd2023VerifyCryptosuite() + : createBbs2023VerifyCryptosuite(); + + verificationResult = await jsonldSignatures.verify(credential, { + suite: new DataIntegrityProof({ + cryptosuite: verifyCryptosuite, + }), + purpose: new AssertionProofPurpose(), + documentLoader, + }); + } else { + return { + verified: false, + error: `DataIntegrityProof with cryptosuite "${cryptosuite}" is not supported for verification.`, + }; + } + } else { + // For BBS+ proofs, create the suite normally + verificationResult = await jsonldSignaturesV7.verify(credential, { + suite: new SuiteClass(), + purpose: new AssertionProofPurposeV7(), + documentLoader, + }); + } + } else { + return { + verified: false, + error: `Proof type "${proofType}" is not supported for verification.`, + }; + } if (verificationResult.verified) { return { verified: true }; @@ -173,12 +458,13 @@ export const verifyCredential = async ( /** * Derives a credential with selective disclosure based on revealed attributes. * @param {object} credential - The verifiable credential to be selectively disclosed. - * @param {object} revealedAttributes - The attributes from the credential that should be revealed in the derived proof. + * @param {string[]} revealedAttributes - Array of JSON pointers for selective disclosure. + * @param {object} options - Optional parameters including documentLoader. * @returns {Promise} A DerivedResult containing the derived proof or an error message. */ export const deriveCredential = async ( credential: SignedVerifiableCredential, - revealedAttributes: ContextDocument, + revealedAttributes: string[], options?: { documentLoader?: DocumentLoader; }, @@ -187,13 +473,86 @@ export const deriveCredential = async ( _checkCredential(credential); const documentLoader = options?.documentLoader ?? (await getDocumentLoader()); - // Generate a derived proof with selective disclosure using the BbsBlsSignatureProof2020 suite - const derivedProof = await deriveProof(credential, revealedAttributes, { - suite: new BbsBlsSignatureProof2020(), - documentLoader, - }); + const proofType = jsonld.getValues(credential, 'proof')[0].type as ProofType; + + if (proofType === 'BbsBlsSignature2020') { + return { + error: + 'BbsBlsSignature2020 is no longer supported for derivation. Please use the latest cryptosuite versions instead.', + }; + } else if (proofType === 'DataIntegrityProof') { + // Check the cryptosuite to determine the specific proof type + const proof = jsonld.getValues(credential, 'proof')[0]; + const cryptosuite = proof.cryptosuite; + + if (cryptosuite === 'ecdsa-sd-2023' || cryptosuite === 'bbs-2023') { + // Modern cryptosuites with selective disclosure (ECDSA-SD-2023 and BBS-2023) + if (!Array.isArray(revealedAttributes)) { + return { + error: `${cryptosuite} requires revealedAttributes to be an array of JSON pointers (string[]).`, + }; + } + + const selectivePointers = revealedAttributes; + + // Check if this is already a derived credential by examining the proofValue structure + const isAlreadyDerived = !(await baseProofDetectors[cryptosuite as SupportedCryptosuite]( + proof.proofValue as string, + )); - return { derived: derivedProof }; + if (isAlreadyDerived) { + return { + error: `${cryptosuite} derived credentials cannot be further derived. Multiple rounds of derivation are not supported by this cryptosuite.`, + }; + } + + // Extract mandatory pointers from the base proof + const mandatoryPointers = await extractMandatoryPointers( + proof.proofValue as string, + cryptosuite, + ); + + // Check if credentialSubject is already in mandatory pointers or selective pointers + const hasCredentialSubjectInMandatory = mandatoryPointers.some((pointer) => + pointer.startsWith('/credentialSubject'), + ); + const hasCredentialSubjectInSelective = selectivePointers.some((pointer) => + pointer.startsWith('/credentialSubject'), + ); + + if (!hasCredentialSubjectInMandatory && !hasCredentialSubjectInSelective) { + // Only add /credentialSubject if it's not already in mandatory pointers + // and no credentialSubject properties are selected + // This ensures the derived credential remains valid per W3C VC specification + selectivePointers.push('/credentialSubject'); + } + + // Create the DataIntegrityProof suite for disclosure using appropriate cryptosuite + const cryptosuiteInstance = + cryptosuite === 'ecdsa-sd-2023' + ? createEcdsaSd2023DiscloseCryptosuite({ selectivePointers }) + : createBbs2023DiscloseCryptosuite({ selectivePointers }); + + const suite = new DataIntegrityProof({ + cryptosuite: cryptosuiteInstance, + }); + + // Derive the selectively disclosed credential + const derivedProof = await jsonldSignatures.derive(credential, { + suite, + purpose: new AssertionProofPurpose(), + documentLoader, + }); + + return { derived: derivedProof }; + } else { + return { + error: `DataIntegrityProof with cryptosuite "${cryptosuite}" is not supported for derivation.`, + }; + } + } else { + return { error: `Proof type "${proofType}" is not supported for derivation.` }; + } } catch (err: unknown) { // Handle any errors that occur during the proof derivation process if (!(err instanceof Error)) { diff --git a/packages/w3c-vc/tsconfig.build.json b/packages/w3c-vc/tsconfig.build.json index 1f1061f9..22b520bc 100644 --- a/packages/w3c-vc/tsconfig.build.json +++ b/packages/w3c-vc/tsconfig.build.json @@ -8,6 +8,6 @@ "rootDir": ".", "baseUrl": "." }, - "include": ["src/**/*.ts", "src/**/*.json"], + "include": ["src/**/*.ts", "src/**/*.json", "../declaration.d.ts"], "exclude": ["node_modules", ".coverage", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/packages/w3c-vc/tsconfig.json b/packages/w3c-vc/tsconfig.json index 3cf45e0d..1a4ebe25 100644 --- a/packages/w3c-vc/tsconfig.json +++ b/packages/w3c-vc/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "declaration": false }, - "include": ["src/**/*"] + "include": ["src/**/*", "../declaration.d.ts"] } diff --git a/packages/w3c/package.json b/packages/w3c/package.json index 391c2fb3..4b06e373 100644 --- a/packages/w3c/package.json +++ b/packages/w3c/package.json @@ -1,6 +1,6 @@ { "name": "@trustvc/w3c", - "version": "1.2.17", + "version": "1.3.0-alpha.17", "description": "", "main": "dist/index.js", "module": "dist/esm/index.js", @@ -32,10 +32,10 @@ } }, "dependencies": { - "@trustvc/w3c-context": "^1.2.13", - "@trustvc/w3c-credential-status": "^1.2.13", - "@trustvc/w3c-issuer": "^1.2.4", - "@trustvc/w3c-vc": "^1.2.17" + "@trustvc/w3c-context": "^1.3.0-alpha.12", + "@trustvc/w3c-credential-status": "^1.3.0-alpha.13", + "@trustvc/w3c-issuer": "^1.3.0-alpha.10", + "@trustvc/w3c-vc": "^1.3.0-alpha.17" }, "repository": { "type": "git", diff --git a/packages/w3c/tsconfig.json b/packages/w3c/tsconfig.json index 3cf45e0d..1a4ebe25 100644 --- a/packages/w3c/tsconfig.json +++ b/packages/w3c/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "declaration": false }, - "include": ["src/**/*"] + "include": ["src/**/*", "../declaration.d.ts"] }