diff --git a/README.md b/README.md index 444fa88..09a1996 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## About -A wrapper lib build for TrustVC to work with W3C [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) (VCs) Data Model v1.1. Provides packages to facilitate the creation of [Decentralized Identifiers](https://www.w3.org/TR/did-core/) (DIDs) v1, specifically [`did:web`](https://w3c-ccg.github.io/did-method-web/), and [Verifiable Credentials Status](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/) Status List v2021. +A wrapper library built for TrustVC to work with W3C [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) (VCs) Data Model v1.1 and [v2.0](https://www.w3.org/TR/vc-data-model-2.0/). Provides packages to facilitate the creation of [Decentralized Identifiers](https://www.w3.org/TR/did-core/) (DIDs) v1, supporting both [`did:web`](https://w3c-ccg.github.io/did-method-web/) (host a DID document) and [`did:key`](https://w3c-ccg.github.io/did-key-spec/) (self-certifying, no hosting required), and Verifiable Credentials Status — both [Status List 2021](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/) and the latest [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/). ## Packages @@ -21,7 +21,9 @@ For more details on each packages, refer to the individual README doc. 1. **Pre Requisite** 1. Generate a signature specific key pair. [link](https://github.com/TrustVC/w3c/tree/main/packages/w3c-issuer#1-create-private-key) - 2. Generate and host a DID web identity. [link](https://github.com/TrustVC/w3c/tree/main/packages/w3c-issuer#2-generate-did-key-pair-and-did-document) + 2. Choose a DID method: + - `did:web` — generate a DID document and host it on a domain you control. [did:web setup guide](https://github.com/TrustVC/w3c/tree/main/packages/w3c-issuer#2-generate-did-key-pair-and-did-document) + - `did:key` — generate a self-certifying DID key pair (no hosting required). [did:key setup guide](https://github.com/TrustVC/w3c/tree/main/packages/w3c-issuer#3-generate-a-didkey-key-pair) 2. **Sign and Verify VC** diff --git a/package-lock.json b/package-lock.json index 19274d1..32f5bf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33008,13 +33008,13 @@ }, "packages/w3c": { "name": "@trustvc/w3c", - "version": "2.0.2", + "version": "2.1.0", "license": "Apache-2.0", "dependencies": { - "@trustvc/w3c-context": "^2.0.2", - "@trustvc/w3c-credential-status": "^2.0.2", - "@trustvc/w3c-issuer": "^2.0.2", - "@trustvc/w3c-vc": "^2.0.2" + "@trustvc/w3c-context": "^2.1.0", + "@trustvc/w3c-credential-status": "^2.1.0", + "@trustvc/w3c-issuer": "^2.1.0", + "@trustvc/w3c-vc": "^2.1.0" }, "engines": { "node": ">=18.x" @@ -33022,7 +33022,7 @@ }, "packages/w3c-context": { "name": "@trustvc/w3c-context", - "version": "2.0.2", + "version": "2.1.0", "license": "Apache-2.0", "dependencies": { "did-resolver": "^4.1.0", @@ -33114,11 +33114,11 @@ }, "packages/w3c-credential-status": { "name": "@trustvc/w3c-credential-status", - "version": "2.0.2", + "version": "2.1.0", "license": "Apache-2.0", "dependencies": { - "@trustvc/w3c-context": "^2.0.2", - "@trustvc/w3c-issuer": "^2.0.2", + "@trustvc/w3c-context": "^2.1.0", + "@trustvc/w3c-issuer": "^2.1.0", "base64url-universal": "^2.0.0", "pako": "^2.1.0" }, @@ -33128,7 +33128,7 @@ }, "packages/w3c-issuer": { "name": "@trustvc/w3c-issuer", - "version": "2.0.2", + "version": "2.1.0", "license": "Apache-2.0", "dependencies": { "@digitalbazaar/bls12-381-multikey": "^2.1.0", @@ -33145,7 +33145,7 @@ }, "packages/w3c-vc": { "name": "@trustvc/w3c-vc", - "version": "2.0.2", + "version": "2.1.0", "license": "Apache-2.0", "dependencies": { "@digitalbazaar/bbs-2023-cryptosuite": "^2.0.1", @@ -33154,8 +33154,8 @@ "@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": "^2.0.2", - "@trustvc/w3c-issuer": "^2.0.2", + "@trustvc/w3c-credential-status": "^2.1.0", + "@trustvc/w3c-issuer": "^2.1.0", "base64url-universal": "^2.0.0", "cbor": "^9.0.2", "did-resolver": "^4.1.0", diff --git a/packages/w3c-issuer/README.md b/packages/w3c-issuer/README.md index 6fb47e6..e039c74 100644 --- a/packages/w3c-issuer/README.md +++ b/packages/w3c-issuer/README.md @@ -1,6 +1,6 @@ # TrustVC W3C Issuer -A library to facilitate the creation of [Decentralized Identifiers](https://www.w3.org/TR/did-core/) DIDs v1, specifically [`did:web`](https://w3c-ccg.github.io/did-method-web/), for the signing of [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) v1.1. +A library to facilitate the creation of [Decentralized Identifiers](https://www.w3.org/TR/did-core/) DIDs v1, supporting both [`did:web`](https://w3c-ccg.github.io/did-method-web/) (host a DID document) and [`did:key`](https://w3c-ccg.github.io/did-key-spec/) (self-certifying, no hosting required), for the signing of [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) v1.1 and [v2.0](https://www.w3.org/TR/vc-data-model-2.0/). ## Installation To install the package, use: @@ -12,6 +12,7 @@ npm install @trustvc/w3c-issuer ## Features - 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. +- Issue self-certifying `did:key` DIDs (P-256 for ECDSA-SD-2023, BLS12-381 G2 for BBS-2023) — no hosting required.
@@ -142,3 +143,56 @@ console.log("didKeyPairs:", didKeyPairs) } ``` + +### 3. Generate a `did:key` Key Pair + +`generateDidKeyPair` issues a self-certifying [`did:key`](https://w3c-ccg.github.io/did-key-spec/) DID together with the matching Multikey private key pair. Unlike `did:web`, there is no DID document to host — the public key is encoded directly into the DID, so the DID document is reconstructed deterministically by any verifier from the identifier alone. + +> __DID Private Key Pair__ needs to be kept securely. Required for signing Verifiable Credentials. \ +> __Issuer identity__ is not bound to any domain. If you need to bind a `did:key` to a real-world entity, use an out-of-band mechanism (trust registry, delegation credential, etc.). See the [`did:key` guide](https://github.com/TrustVC/w3c/tree/main/packages/w3c-issuer/src/did-key#trust-model) for the trust-model discussion. + +```ts +import { CryptoSuite, generateDidKeyPair, parseDidKey } from '@trustvc/w3c-issuer'; + +/** + * Parameters: + * - cryptosuite (CryptoSuite): Either CryptoSuite.EcdsaSd2023 (P-256) or CryptoSuite.Bbs2023 (BLS12-381 G2) + * - options.seedBase58? (string): 32 byte base58 encoded seed for deterministic BBS-2023 generation (optional, ignored for ECDSA-SD-2023) + * + * Returns: + * - A Promise that resolves to: + * - did (string): The resulting did:key DID + * - didKeyPairs (PrivateKeyPair): Multikey private key pair scoped to the new DID + */ + +const { did, didKeyPairs } = await generateDidKeyPair(CryptoSuite.EcdsaSd2023); +console.log('did:', did); +console.log('didKeyPairs:', didKeyPairs); +``` + +
+ generateDidKeyPair Result + + ```js + did: 'did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc' + didKeyPairs: { + '@context': 'https://w3id.org/security/multikey/v1', + id: 'did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc#zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc', + type: 'Multikey', + controller: 'did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc', + publicKeyMultibase: 'zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc', + secretKeyMultibase: '' + } + ``` +
+ +`parseDidKey` decodes a `did:key` identifier back into its key type and raw public key bytes — useful if you receive a `did:key` from elsewhere and need to inspect it: + +```ts +const info = parseDidKey('did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc'); +// info.keyType === 'P-256' +// info.publicKey is a Uint8Array of the compressed 33-byte P-256 public key +// info.verificationMethodId === 'did:key:zDna...#zDna...' +``` + +**Supported key types:** `P-256` (for `ecdsa-sd-2023`) and `BLS12-381 G2` (for `bbs-2023`). Other `did:key` types defined in the spec (Ed25519, secp256k1, P-384, etc.) are rejected at parse time because no matching cryptosuite is wired up in `@trustvc/w3c-vc`. diff --git a/packages/w3c-issuer/package.json b/packages/w3c-issuer/package.json index 333b57e..9425799 100644 --- a/packages/w3c-issuer/package.json +++ b/packages/w3c-issuer/package.json @@ -50,4 +50,4 @@ "registry": "https://registry.npmjs.org/" }, "private": false -} +} \ No newline at end of file diff --git a/packages/w3c-issuer/src/did-key/README.md b/packages/w3c-issuer/src/did-key/README.md new file mode 100644 index 0000000..300b210 --- /dev/null +++ b/packages/w3c-issuer/src/did-key/README.md @@ -0,0 +1,164 @@ +# Using `did:key` + +This guide explains how to issue and use a Decentralized Identifier (DID) with the [`did:key`](https://w3c-ccg.github.io/did-key-spec/) method. Unlike `did:web`, `did:key` is **self-certifying** — the public key is encoded directly into the DID itself, so no hosting infrastructure is required. + +## Table of Contents + +- 1. [How it differs from `did:web`](#how-it-differs-from-didweb) +- 2. [Trust model](#trust-model) +- 3. [Supported key types](#supported-key-types) +- 4. [Step-by-Step Usage](#step-by-step-usage) + - 1. [Generate a `did:key` Key Pair](#1-generate-a-didkey-key-pair) + - 2. [Sign a Credential](#2-sign-a-credential) + - 3. [Verify a Credential](#3-verify-a-credential) +- 5. [Anatomy of a `did:key` DID](#anatomy-of-a-didkey-did) +- 6. [Specifications](#specifications) + +## How it differs from `did:web` + +| | `did:web` | `did:key` | +|---|---|---| +| Hosting required | Yes (a `.well-known/did.json` or similar) | **No** | +| Verifier needs network | Yes (to fetch the DID document) | **No** (DID document is synthesised in memory) | +| Human-readable identifier | Yes (domain name in DID) | No (the DID is the encoded public key) | +| Multiple keys per DID | Yes | No (exactly one key per DID) | +| Key rotation | Update hosted document, DID stays the same | DID changes — a new key is a new DID | +| Revocation by removal | Yes (take the doc down, remove the VM) | Not possible — anyone with the key can keep signing | +| Trust anchor | The domain | **Must be supplied out-of-band** (see below) | + +Example identifiers: + +```text +did:web:trustvc.github.io:did:1 ← did:web (resolves via HTTPS) +did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc ← did:key (resolves locally) +``` + +## Trust model + +Cryptographically, a `did:key`-signed credential proves *the holder of this private key signed this credential*. That part is identical to `did:web` — same crypto, same guarantees. + +What `did:key` does **not** give you on its own is *who* in the real world holds that private key. With `did:web`, the domain answers that question (you trust whoever controls `trustvc.github.io`). With `did:key`, the DID is just a number and you need to bind it to a real-world entity through one of: + +1. **Trust registry / allow-list** — maintain a list of approved `did:key` DIDs out-of-band. The verifier checks the credential's issuer against the registry. +2. **Chained credentials (delegation)** — a known authority (often a `did:web` issuer) issues a credential **to** a `did:key` saying "this key is authorised to act on behalf of X". The verifier walks the chain. +3. **Sidechannel exchange** — exchange the `did:key` through a trusted channel (business card, contract, signed email, etc.). +4. **Self-contained credentials** — for use cases where the credential proves a property of itself (a hash, a receipt) and issuer identity is not relevant. + +If you are using `did:key` in production for regulated contexts (Bills of Lading, KYC, etc.), do **not** rely on the cryptographic identity check alone — layer one of the patterns above in your application code. + +## Supported key types + +This library supports two `did:key` key types, chosen to match the cryptosuites available in `@trustvc/w3c-vc`: + +| Multibase prefix | Key type | Multicodec | Cryptosuite | +|---|---|---|---| +| `zDna...` | P-256 (NIST secp256r1, compressed) | `0x1200` | `ecdsa-sd-2023` | +| `zUC7...` | BLS12-381 G2 | `0xeb` | `bbs-2023` | + +Other `did:key` types defined in the method spec (Ed25519 `z6Mk...`, secp256k1 `zQ3s...`, P-384, P-521, RSA, etc.) are rejected at parse time with a clear error, because no matching cryptosuite is wired up in `@trustvc/w3c-vc`. + +## Step-by-Step Usage + +### 1. Generate a `did:key` Key Pair + +```ts +import { CryptoSuite, generateDidKeyPair } from '@trustvc/w3c-issuer'; + +const { did, didKeyPairs } = await generateDidKeyPair(CryptoSuite.EcdsaSd2023); +// did is: 'did:key:zDna...' +// didKeyPairs is a Multikey private key pair (id/controller/publicKeyMultibase/secretKeyMultibase) +``` + +Keep `didKeyPairs.secretKeyMultibase` private — it is what allows you to sign as this DID. The other fields (`did`, `publicKeyMultibase`) are safe to publish. + +For BBS-2023 you may pass an optional `seedBase58` if you want deterministic generation: + +```ts +const { did, didKeyPairs } = await generateDidKeyPair(CryptoSuite.Bbs2023, { + seedBase58: 'FVj12jBiBUqYFaEUkTuwAD73p9Hx5NzCJBge74nTguQN', +}); +``` + +ECDSA-SD-2023 does not support seeded generation. + +### 2. Sign a Credential + +The signing call is identical to `did:web` — pass the key pair to `signCredential` from `@trustvc/w3c-vc`. The resulting proof's `verificationMethod` references the canonical `did:key:zXxx#zXxx` form: + +```ts +import { signCredential } from '@trustvc/w3c-vc'; + +const credential = { + '@context': ['https://www.w3.org/ns/credentials/v2', 'https://w3id.org/security/data-integrity/v2'], + type: ['VerifiableCredential'], + issuer: did, // the did:key from above + validFrom: '2024-04-01T12:19:52Z', + credentialSubject: { name: 'TrustVC Demo' }, +}; + +const { signed } = await signCredential(credential, didKeyPairs, 'ecdsa-sd-2023'); +``` + +### 3. Verify a Credential + +`verifyCredential` works without any extra configuration — the default document loader from `@trustvc/w3c-context` automatically dispatches `did:key:*` resolution through this package's `queryDidDocument`, which synthesises the DID document in memory. No network call is made. + +```ts +import { deriveCredential, verifyCredential } from '@trustvc/w3c-vc'; + +// ECDSA-SD-2023 and BBS-2023 are selective-disclosure schemes: derive before verifying. +const { derived } = await deriveCredential(signed, ['/credentialSubject/name']); +const result = await verifyCredential(derived); +// result.verified === true +``` + +## Anatomy of a `did:key` DID + +A `did:key` DID is constructed as: + +```text +did:key: + z + base58btc( || ) +``` + +Worked example for the P-256 key `zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc`: + +```text +did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc + │ + └── z = base58btc multibase prefix + ── decode base58btc → [0x80, 0x24, ...33 bytes pubkey] + └──┬───┘ └──────┬────────┘ + varint(0x1200) raw compressed P-256 public key + = "p256-pub" +``` + +The synthesised DID document has exactly one verification method, whose `id` is `#` (the fragment **must** equal the multibase identifier per the did:key spec): + +```json +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1" + ], + "id": "did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc", + "verificationMethod": [{ + "id": "did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc#zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc", + "type": "Multikey", + "controller": "did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc", + "publicKeyMultibase": "zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc" + }], + "authentication": ["did:key:zDnae...#zDnae..."], + "assertionMethod": ["did:key:zDnae...#zDnae..."], + "capabilityInvocation": ["did:key:zDnae...#zDnae..."], + "capabilityDelegation": ["did:key:zDnae...#zDnae..."] +} +``` + +## Specifications + +- [DID Core 1.0](https://www.w3.org/TR/did-core/) — DID and verification method data model. +- [The `did:key` Method v0.9](https://w3c-ccg.github.io/did-key-spec/) — encoding rules and canonical fragment form. +- [VC Data Integrity 1.0](https://www.w3.org/TR/vc-data-integrity/) — `DataIntegrityProof` and `verificationMethod` requirements. +- [W3C Data Integrity ECDSA Cryptosuites](https://www.w3.org/TR/vc-di-ecdsa/) — `ecdsa-sd-2023`, P-256 binding. +- [W3C Data Integrity BBS Cryptosuites](https://www.w3.org/TR/vc-di-bbs/) — `bbs-2023`, BLS12-381 G2 binding. +- [Multicodec table](https://github.com/multiformats/multicodec/blob/master/table.csv) — multicodec code points. diff --git a/packages/w3c-issuer/src/did-key/index.ts b/packages/w3c-issuer/src/did-key/index.ts new file mode 100644 index 0000000..5ba6e61 --- /dev/null +++ b/packages/w3c-issuer/src/did-key/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './parse'; +export * from './keyPair'; diff --git a/packages/w3c-issuer/src/did-key/keyPair.test.ts b/packages/w3c-issuer/src/did-key/keyPair.test.ts new file mode 100644 index 0000000..3b0c421 --- /dev/null +++ b/packages/w3c-issuer/src/did-key/keyPair.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { CryptoSuite, VerificationType } from '../lib/types'; +import { generateDidKeyPair } from './keyPair'; +import { parseDidKey } from './parse'; + +describe('generateDidKeyPair', () => { + it('generates a P-256 did:key for ECDSA-SD-2023', async () => { + const result = await generateDidKeyPair(CryptoSuite.EcdsaSd2023); + expect(result.did.startsWith('did:key:z')).toBe(true); + expect(result.didKeyPairs.type).toBe(VerificationType.Multikey); + expect(result.didKeyPairs.controller).toBe(result.did); + expect(result.didKeyPairs.id).toBe(`${result.did}#${result.didKeyPairs.publicKeyMultibase}`); + expect(result.didKeyPairs.publicKeyMultibase).toBeDefined(); + expect(result.didKeyPairs.secretKeyMultibase).toBeDefined(); + + // The generated DID must round-trip through the parser. + const parsed = parseDidKey(result.did); + expect(parsed.keyType).toBe('P-256'); + }); + + it('generates a BLS12-381 G2 did:key for BBS-2023', async () => { + const result = await generateDidKeyPair(CryptoSuite.Bbs2023); + expect(result.did.startsWith('did:key:z')).toBe(true); + expect(result.didKeyPairs.type).toBe(VerificationType.Multikey); + expect(result.didKeyPairs.controller).toBe(result.did); + + const parsed = parseDidKey(result.did); + expect(parsed.keyType).toBe('Bls12381G2'); + }); +}); diff --git a/packages/w3c-issuer/src/did-key/keyPair.ts b/packages/w3c-issuer/src/did-key/keyPair.ts new file mode 100644 index 0000000..37ac521 --- /dev/null +++ b/packages/w3c-issuer/src/did-key/keyPair.ts @@ -0,0 +1,43 @@ +import { generateKeyPair } from '../did-web/keyPair'; +import { CryptoSuite, VerificationContext, VerificationType } from '../lib/types'; +import { multibaseToDidKey } from './parse'; +import { GeneratedDidKey } from './types'; + +/** + * Generate a fresh did:key DID + private key pair for one of the supported + * Multikey cryptosuites (BBS-2023 → BLS12-381 G2, ECDSA-SD-2023 → P-256). + * Because both libraries produce `publicKeyMultibase` strings that are already + * multibase+multicodec-encoded, the did:key DID is exactly `did:key:`. + * @param {CryptoSuite} cryptosuite - Either `bbs-2023` or `ecdsa-sd-2023`. + * @param {object} [options] - Optional generation options. + * @param {string} [options.seedBase58] - Optional seed for BBS-2023 (ignored for ECDSA-SD-2023). + * @returns {Promise} The new DID and a Multikey private key pair scoped to that DID. + */ +export const generateDidKeyPair = async ( + cryptosuite: CryptoSuite, + options?: { seedBase58?: string }, +): Promise => { + const kp = await generateKeyPair({ + type: cryptosuite, + seedBase58: options?.seedBase58, + }); + + if (!kp.publicKeyMultibase || !kp.secretKeyMultibase) { + throw new Error(`generateKeyPair did not return a Multikey key pair for ${cryptosuite}`); + } + + const info = multibaseToDidKey(kp.publicKeyMultibase); + + return { + did: info.did, + didKeyPairs: { + '@context': VerificationContext[VerificationType.Multikey], + id: info.verificationMethodId, + type: VerificationType.Multikey, + controller: info.did, + publicKeyMultibase: kp.publicKeyMultibase, + secretKeyMultibase: kp.secretKeyMultibase, + ...(kp.seedBase58 && { seedBase58: kp.seedBase58 }), + }, + }; +}; diff --git a/packages/w3c-issuer/src/did-key/parse.test.ts b/packages/w3c-issuer/src/did-key/parse.test.ts new file mode 100644 index 0000000..109e84e --- /dev/null +++ b/packages/w3c-issuer/src/did-key/parse.test.ts @@ -0,0 +1,146 @@ +import { base58btc } from 'multiformats/bases/base58'; +import { describe, expect, it } from 'vitest'; +import { VerificationType } from '../lib/types'; +import { + buildDidKeyDocument, + isDidKey, + multibaseToDidKey, + parseDidKey, + publicKeyToDidKey, +} from './parse'; +import { encodeVarint } from './varint'; + +const encodeDidKey = (codec: number, keyBytes: Uint8Array): string => { + const prefix = encodeVarint(codec); + const combined = new Uint8Array(prefix.length + keyBytes.length); + combined.set(prefix, 0); + combined.set(keyBytes, prefix.length); + return `did:key:${base58btc.encode(combined)}`; +}; + +const P256_PUBLIC_KEY_MULTIBASE = 'zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc'; +const BLS_PUBLIC_KEY_MULTIBASE = + 'zUC7HnpncVAkTjtL6B8prX6bQM2WA5sJ7rXFeCqyrvPnrzoFBjYsVUTNwzhhPUazja73tWwPeEBWCUgq5qBSrtrXiYhVvBCgZPTCiWANj7TSiZJ6SnyC3pkt94GiuChhAvmRRbt'; + +describe('did:key parse', () => { + describe('isDidKey', () => { + it('returns true for did:key URIs', () => { + expect(isDidKey('did:key:zDna...')).toBe(true); + }); + + it('returns false for non-did:key URIs', () => { + expect(isDidKey('did:web:example.com')).toBe(false); + expect(isDidKey('https://example.com')).toBe(false); + expect(isDidKey('')).toBe(false); + }); + }); + + describe('parseDidKey', () => { + it('parses a P-256 did:key (zDna prefix)', () => { + const did = `did:key:${P256_PUBLIC_KEY_MULTIBASE}`; + const info = parseDidKey(did); + expect(info.did).toBe(did); + expect(info.keyType).toBe('P-256'); + expect(info.publicKeyMultibase).toBe(P256_PUBLIC_KEY_MULTIBASE); + expect(info.verificationMethodId).toBe(`${did}#${P256_PUBLIC_KEY_MULTIBASE}`); + expect(info.publicKey.length).toBe(33); // P-256 compressed + }); + + it('parses a BLS12-381 G2 did:key (zUC7 prefix)', () => { + const did = `did:key:${BLS_PUBLIC_KEY_MULTIBASE}`; + const info = parseDidKey(did); + expect(info.keyType).toBe('Bls12381G2'); + expect(info.publicKeyMultibase).toBe(BLS_PUBLIC_KEY_MULTIBASE); + expect(info.publicKey.length).toBe(96); // BLS12-381 G2 compressed + }); + + it('parses a did:key URL with fragment and strips it from `did`', () => { + const did = `did:key:${P256_PUBLIC_KEY_MULTIBASE}`; + const vmUrl = `${did}#${P256_PUBLIC_KEY_MULTIBASE}`; + const info = parseDidKey(vmUrl); + expect(info.did).toBe(did); + expect(info.verificationMethodId).toBe(vmUrl); + }); + + it('rejects non-did:key inputs', () => { + expect(() => parseDidKey('did:web:example.com')).toThrow(/Not a did:key/); + }); + + it('rejects non-base58btc multibase', () => { + expect(() => parseDidKey('did:key:mAbc')).toThrow(/base58btc/); + }); + + it('rejects unsupported multicodec (Ed25519)', () => { + // z6Mk... is the canonical Ed25519 did:key prefix. + expect(() => parseDidKey('did:key:z6MkpTHR8VNsBxYAAWHut2Geo2LdLLChG4Wkw5kJp9TQq7Pe')).toThrow( + /Unsupported did:key multicodec/, + ); + }); + + it('rejects a P-256 did:key with wrong public key length', () => { + // 32 bytes instead of the canonical 33 (missing the compressed-point tag byte). + const tampered = encodeDidKey(0x1200, new Uint8Array(32)); + expect(() => parseDidKey(tampered)).toThrow(/Invalid P-256 public key length/); + }); + + it('rejects a BLS12-381 G2 did:key with wrong public key length', () => { + // 95 bytes instead of the canonical 96. + const tampered = encodeDidKey(0xeb, new Uint8Array(95)); + expect(() => parseDidKey(tampered)).toThrow(/Invalid Bls12381G2 public key length/); + }); + + it('rejects an over-long varint prefix that would collide via 32-bit truncation', () => { + // 5-byte varint encoding a value > 2^32 whose low 32 bits equal 0xeb. + // Without the prefix-length cap this could be misread as the BLS12-381 G2 codec. + const overlong = new Uint8Array([0xeb, 0x80, 0x80, 0x80, 0x10]); + const combined = new Uint8Array(overlong.length + 96); + combined.set(overlong, 0); + const tampered = `did:key:${base58btc.encode(combined)}`; + expect(() => parseDidKey(tampered)).toThrow(/Varint exceeds maximum length/); + }); + }); + + describe('publicKeyToDidKey', () => { + it('round-trips with parseDidKey', () => { + const original = parseDidKey(`did:key:${P256_PUBLIC_KEY_MULTIBASE}`); + const rebuilt = publicKeyToDidKey(original.keyType, original.publicKey); + expect(rebuilt.did).toBe(original.did); + expect(rebuilt.publicKeyMultibase).toBe(original.publicKeyMultibase); + }); + + it('round-trips for BLS12-381 G2', () => { + const original = parseDidKey(`did:key:${BLS_PUBLIC_KEY_MULTIBASE}`); + const rebuilt = publicKeyToDidKey(original.keyType, original.publicKey); + expect(rebuilt.did).toBe(original.did); + }); + }); + + describe('multibaseToDidKey', () => { + it('produces canonical id/controller from a publicKeyMultibase', () => { + const info = multibaseToDidKey(P256_PUBLIC_KEY_MULTIBASE); + expect(info.did).toBe(`did:key:${P256_PUBLIC_KEY_MULTIBASE}`); + expect(info.verificationMethodId).toBe( + `did:key:${P256_PUBLIC_KEY_MULTIBASE}#${P256_PUBLIC_KEY_MULTIBASE}`, + ); + }); + }); + + describe('buildDidKeyDocument', () => { + it('builds a DID document with a Multikey verification method', () => { + const info = parseDidKey(`did:key:${P256_PUBLIC_KEY_MULTIBASE}`); + const doc = buildDidKeyDocument(info); + expect(doc.id).toBe(info.did); + expect(doc.verificationMethod).toHaveLength(1); + expect(doc.verificationMethod?.[0]).toEqual({ + id: info.verificationMethodId, + type: VerificationType.Multikey, + controller: info.did, + publicKeyMultibase: info.publicKeyMultibase, + }); + expect(doc.assertionMethod).toContain(info.verificationMethodId); + expect(doc.authentication).toContain(info.verificationMethodId); + expect(doc.capabilityInvocation).toContain(info.verificationMethodId); + expect(doc.capabilityDelegation).toContain(info.verificationMethodId); + }); + }); +}); diff --git a/packages/w3c-issuer/src/did-key/parse.ts b/packages/w3c-issuer/src/did-key/parse.ts new file mode 100644 index 0000000..8eca74d --- /dev/null +++ b/packages/w3c-issuer/src/did-key/parse.ts @@ -0,0 +1,130 @@ +import { base58btc } from 'multiformats/bases/base58'; +import { DIDDocument } from 'did-resolver'; +import { VerificationType } from '../lib/types'; +import { DidWellKnownDocument } from '../did-web/wellKnown/types'; +import { DidKeyInfo, DidKeyType } from './types'; +import { decodeVarint, encodeVarint } from './varint'; + +// Multicodec code points (varint values, not raw byte sequences). +// https://github.com/multiformats/multicodec/blob/master/table.csv +const MULTICODEC_BLS12381_G2_PUB = 0xeb; +const MULTICODEC_P256_PUB = 0x1200; + +// Canonical raw public-key byte lengths. +// P-256 compressed SEC1 point: 1 byte tag + 32 byte x = 33 bytes. +// BLS12-381 G2 compressed: 96 bytes. +const P256_PUBLIC_KEY_LENGTH = 33; +const BLS12381_G2_PUBLIC_KEY_LENGTH = 96; + +/** + * Returns true when the given URI is a `did:key:` DID (possibly with fragment). + * @param {string} uri - The URI to check. + * @returns {boolean} Whether `uri` is a did:key DID. + */ +export const isDidKey = (uri: string): boolean => { + return typeof uri === 'string' && uri.startsWith('did:key:'); +}; + +/** + * Parses a `did:key:` DID (with or without a fragment) and extracts key info. + * @param {string} didKey - The did:key DID or DID URL. + * @returns {DidKeyInfo} Decoded key info: DID, verification method id, raw public key, key type. + */ +export const parseDidKey = (didKey: string): DidKeyInfo => { + const did = didKey.split('#')[0]; + if (!did.startsWith('did:key:')) { + throw new Error(`Not a did:key: ${didKey}`); + } + const multibase = did.slice('did:key:'.length); + if (!multibase.startsWith(base58btc.prefix)) { + throw new Error(`did:key must use base58btc multibase (z prefix): ${didKey}`); + } + const decoded = base58btc.decode(multibase); + const { value: codec, bytesRead } = decodeVarint(decoded); + const publicKey = decoded.slice(bytesRead); + + let keyType: DidKeyType; + let expectedLength: number; + if (codec === MULTICODEC_BLS12381_G2_PUB) { + keyType = 'Bls12381G2'; + expectedLength = BLS12381_G2_PUBLIC_KEY_LENGTH; + } else if (codec === MULTICODEC_P256_PUB) { + keyType = 'P-256'; + expectedLength = P256_PUBLIC_KEY_LENGTH; + } else { + throw new Error(`Unsupported did:key multicodec: 0x${codec.toString(16)}`); + } + + if (publicKey.length !== expectedLength) { + throw new Error( + `Invalid ${keyType} public key length: expected ${expectedLength} bytes, got ${publicKey.length}`, + ); + } + + return { + did, + verificationMethodId: `${did}#${multibase}`, + publicKeyMultibase: multibase, + publicKey, + keyType, + }; +}; + +/** + * Encode raw public key bytes as a `did:key:` DID. + * @param {DidKeyType} keyType - Key type to use for the multicodec prefix. + * @param {Uint8Array} publicKey - The raw public key bytes (no multicodec prefix). + * @returns {DidKeyInfo} The resulting DID info including its canonical verification method id. + */ +export const publicKeyToDidKey = (keyType: DidKeyType, publicKey: Uint8Array): DidKeyInfo => { + const codec = keyType === 'Bls12381G2' ? MULTICODEC_BLS12381_G2_PUB : MULTICODEC_P256_PUB; + const prefix = encodeVarint(codec); + const combined = new Uint8Array(prefix.length + publicKey.length); + combined.set(prefix, 0); + combined.set(publicKey, prefix.length); + const multibase = base58btc.encode(combined); + const did = `did:key:${multibase}`; + return { + did, + verificationMethodId: `${did}#${multibase}`, + publicKeyMultibase: multibase, + publicKey, + keyType, + }; +}; + +/** + * Convenience: re-derive a did:key DID from an existing Multikey publicKeyMultibase + * (which already encodes the multicodec prefix, e.g. from `generateKeyPair`). + * @param {string} publicKeyMultibase - z-prefixed base58btc multibase. + * @returns {DidKeyInfo} The resulting DID info. + */ +export const multibaseToDidKey = (publicKeyMultibase: string): DidKeyInfo => { + return parseDidKey(`did:key:${publicKeyMultibase}`); +}; + +/** + * Build the W3C DID Document for a `did:key:` DID. + * The same Multikey verification method is referenced under every relationship. + * @param {DidKeyInfo} info - DID info from `parseDidKey` / `publicKeyToDidKey`. + * @returns {DidWellKnownDocument} A DID document compatible with the resolver shape. + */ +export const buildDidKeyDocument = (info: DidKeyInfo): DidWellKnownDocument => { + const verificationMethod: DIDDocument['verificationMethod'] = [ + { + id: info.verificationMethodId, + type: VerificationType.Multikey, + controller: info.did, + publicKeyMultibase: info.publicKeyMultibase, + }, + ]; + return { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/multikey/v1'], + id: info.did, + verificationMethod, + authentication: [info.verificationMethodId], + assertionMethod: [info.verificationMethodId], + capabilityInvocation: [info.verificationMethodId], + capabilityDelegation: [info.verificationMethodId], + } as DidWellKnownDocument; +}; diff --git a/packages/w3c-issuer/src/did-key/types.ts b/packages/w3c-issuer/src/did-key/types.ts new file mode 100644 index 0000000..d73a0b4 --- /dev/null +++ b/packages/w3c-issuer/src/did-key/types.ts @@ -0,0 +1,18 @@ +import { Bbs2023PrivateKeyPair, EcdsaSd2023PrivateKeyPair } from '../did-web/wellKnown/types'; + +export type DidKeyType = 'Bls12381G2' | 'P-256'; + +export interface DidKeyInfo { + did: string; + verificationMethodId: string; + publicKeyMultibase: string; + publicKey: Uint8Array; + keyType: DidKeyType; +} + +export type DidKeyPrivateKeyPair = Bbs2023PrivateKeyPair | EcdsaSd2023PrivateKeyPair; + +export interface GeneratedDidKey { + did: string; + didKeyPairs: DidKeyPrivateKeyPair; +} diff --git a/packages/w3c-issuer/src/did-key/varint.ts b/packages/w3c-issuer/src/did-key/varint.ts new file mode 100644 index 0000000..55b030e --- /dev/null +++ b/packages/w3c-issuer/src/did-key/varint.ts @@ -0,0 +1,44 @@ +/** + * Minimal unsigned varint codec, sized for the multicodec prefixes used by + * did:key (all current key types fit in <= 3 bytes). Implemented inline because + * multiformats v9 does not export varint as a public subpath. + */ + +// Multicodec prefixes used by did:key all fit in <= 3 bytes. Capping here +// rejects non-canonical longer encodings of the same numeric value (e.g. a +// 5-byte prefix whose 32-bit-truncated value collides with a supported codec). +const MAX_PREFIX_BYTES = 3; + +export const decodeVarint = ( + bytes: Uint8Array, + offset = 0, +): { value: number; bytesRead: number } => { + let value = 0; + let multiplier = 1; + let i = offset; + while (i < bytes.length) { + if (i - offset >= MAX_PREFIX_BYTES) { + throw new Error('Varint exceeds maximum length for did:key multicodec prefix'); + } + const b = bytes[i]; + value += (b & 0x7f) * multiplier; + i++; + if (!Number.isSafeInteger(value)) throw new Error('Varint too large'); + if ((b & 0x80) === 0) return { value, bytesRead: i - offset }; + multiplier *= 128; + } + throw new Error('Truncated varint'); +}; + +export const encodeVarint = (value: number): Uint8Array => { + if (!Number.isInteger(value) || value < 0) + throw new Error('Varint must be a non-negative integer'); + const out: number[] = []; + let v = value; + while (v >= 0x80) { + out.push((v & 0x7f) | 0x80); + v = Math.floor(v / 128); + } + out.push(v); + return new Uint8Array(out); +}; 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 4747245..b4ff7e4 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/query.test.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/query.test.ts @@ -75,6 +75,28 @@ describe('query', () => { `); expect(did).toBe('did:web:trustvc.github.io:did:1'); }); + + it('should resolve a did:key bare DID without network access', async () => { + const didKey = 'did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc'; + const { did, wellKnownDid } = await queryDidDocument({ did: didKey }); + expect(did).toBe(didKey); + expect(wellKnownDid?.id).toBe(didKey); + expect(wellKnownDid?.verificationMethod?.[0]).toMatchObject({ + id: `${didKey}#zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc`, + type: 'Multikey', + controller: didKey, + publicKeyMultibase: 'zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc', + }); + }); + + it('should resolve a did:key DID URL with fragment', async () => { + const didKey = 'did:key:zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc'; + const vmUrl = `${didKey}#zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc`; + const { did, wellKnownDid } = await queryDidDocument({ did: vmUrl }); + // queryDidDocument strips the fragment from `did` for did:key (matches did:web behaviour). + expect(did).toBe(didKey); + expect(wellKnownDid?.verificationMethod?.find((vm) => vm.id === vmUrl)).toBeDefined(); + }); }); describe('resolve', () => { diff --git a/packages/w3c-issuer/src/did-web/wellKnown/query.ts b/packages/w3c-issuer/src/did-web/wellKnown/query.ts index 9c6a009..45b5c65 100644 --- a/packages/w3c-issuer/src/did-web/wellKnown/query.ts +++ b/packages/w3c-issuer/src/did-web/wellKnown/query.ts @@ -1,6 +1,7 @@ import { Resolver, DIDResolutionResult, DIDResolutionOptions } from 'did-resolver'; import { getResolver as webGetResolver } from 'web-did-resolver'; import { getDomain } from '../../lib'; +import { buildDidKeyDocument, isDidKey, parseDidKey } from '../../did-key'; import { DidWellKnownDocument, QueryDidDocument, QueryDidDocumentOption } from './types'; const SUPPORTED_CONTENT_TYPES = ['application/did+json', 'application/did+ld+json']; @@ -25,6 +26,15 @@ export const queryDidDocument = async ({ throw new Error('Missing domain'); } + if (did && isDidKey(did)) { + // did:key is self-certifying: the DID document is derived from the identifier itself. + const info = parseDidKey(did); + return { + did: info.did, + wellKnownDid: buildDidKeyDocument(info), + }; + } + if (!did && domain) { let domainHostname = getDomain(domain); diff --git a/packages/w3c-issuer/src/index.ts b/packages/w3c-issuer/src/index.ts index 5b6dc46..f85c81b 100644 --- a/packages/w3c-issuer/src/index.ts +++ b/packages/w3c-issuer/src/index.ts @@ -1,3 +1,4 @@ export * from './did-web'; +export * from './did-key'; export * from './lib'; export * from './lib/types'; diff --git a/packages/w3c-vc/README.md b/packages/w3c-vc/README.md index 21837cb..8607ea0 100644 --- a/packages/w3c-vc/README.md +++ b/packages/w3c-vc/README.md @@ -26,6 +26,7 @@ npm install @trustvc/w3c-vc ## 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 +- **DID Method Agnostic**: Sign and verify with `did:web` issuers (hosted DID document) or `did:key` issuers (self-certifying, no hosting). See [`@trustvc/w3c-issuer`](../w3c-issuer/README.md) for issuance. - **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 @@ -41,6 +42,8 @@ npm install @trustvc/w3c-vc ## Usage +> **A note on issuer DIDs**: the examples below use `did:web` issuers, but the same `signCredential` / `verifyCredential` / `deriveCredential` calls work unchanged for `did:key` issuers — just replace the keyPair's `id` / `controller` and the credential's `issuer` with the `did:key:zXxx#zXxx` / `did:key:zXxx` form. The default document loader from [`@trustvc/w3c-context`](../w3c-context/README.md) resolves `did:key` URIs in-memory (no network call), so verification works out of the box. See [`@trustvc/w3c-issuer`'s did:key guide](../w3c-issuer/src/did-key/README.md) for the full flow. + ### 1. Signing a Credential The `signCredential` function allows you to sign a Verifiable Credential using modern cryptographic signature schemes. **Default cryptosuite is `ecdsa-sd-2023`**. diff --git a/packages/w3c-vc/src/lib/__fixtures__/key-pairs.ts b/packages/w3c-vc/src/lib/__fixtures__/key-pairs.ts index 6735f9d..1032d93 100644 --- a/packages/w3c-vc/src/lib/__fixtures__/key-pairs.ts +++ b/packages/w3c-vc/src/lib/__fixtures__/key-pairs.ts @@ -32,3 +32,27 @@ export const bbs2023KeyPair: Bbs2023PrivateKeyPair = { 'zUC7HnpncVAkTjtL6B8prX6bQM2WA5sJ7rXFeCqyrvPnrzoFBjYsVUTNwzhhPUazja73tWwPeEBWCUgq5qBSrtrXiYhVvBCgZPTCiWANj7TSiZJ6SnyC3pkt94GiuChhAvmRRbt', secretKeyMultibase: 'z488ur1KSFDd3Y1L6pXcPrZRjE18PNBhgzwJvMeoSxKPNysj', }; + +// did:key variants reuse the same key material as the did:web fixtures above — +// only the DID, controller, and verification-method id differ (the multibase +// public key IS the DID after `did:key:`). +export const ECDSA_DID_KEY_ISSUER = `did:key:${ecdsa2023KeyPair.publicKeyMultibase}`; +export const BBS_DID_KEY_ISSUER = `did:key:${bbs2023KeyPair.publicKeyMultibase}`; + +export const ecdsa2023DidKeyPair: EcdsaSd2023PrivateKeyPair = { + '@context': 'https://w3id.org/security/multikey/v1', + id: `${ECDSA_DID_KEY_ISSUER}#${ecdsa2023KeyPair.publicKeyMultibase}`, + type: VerificationType.Multikey, + controller: ECDSA_DID_KEY_ISSUER, + publicKeyMultibase: ecdsa2023KeyPair.publicKeyMultibase, + secretKeyMultibase: ecdsa2023KeyPair.secretKeyMultibase, +}; + +export const bbs2023DidKeyPair: Bbs2023PrivateKeyPair = { + '@context': 'https://w3id.org/security/multikey/v1', + id: `${BBS_DID_KEY_ISSUER}#${bbs2023KeyPair.publicKeyMultibase}`, + type: VerificationType.Multikey, + controller: BBS_DID_KEY_ISSUER, + publicKeyMultibase: bbs2023KeyPair.publicKeyMultibase, + secretKeyMultibase: bbs2023KeyPair.secretKeyMultibase, +}; diff --git a/packages/w3c-vc/src/lib/__fixtures__/test-scenarios.ts b/packages/w3c-vc/src/lib/__fixtures__/test-scenarios.ts index 5001357..901e9c9 100644 --- a/packages/w3c-vc/src/lib/__fixtures__/test-scenarios.ts +++ b/packages/w3c-vc/src/lib/__fixtures__/test-scenarios.ts @@ -1,5 +1,13 @@ import { CryptoSuiteName, SignedVerifiableCredential, VerifiableCredential } from '../types'; -import { bbs2020KeyPair, bbs2023KeyPair, ecdsa2023KeyPair } from './key-pairs'; +import { + BBS_DID_KEY_ISSUER, + bbs2020KeyPair, + bbs2023DidKeyPair, + bbs2023KeyPair, + ECDSA_DID_KEY_ISSUER, + ecdsa2023DidKeyPair, + ecdsa2023KeyPair, +} from './key-pairs'; import { modernCredentialV1_1, modernCredentialV2_0 } from './modern-credentials'; import { bbs2020CredentialV1_1, @@ -82,4 +90,37 @@ export const modernCryptosuiteTestScenarios: ModernCryptosuiteTestScenario[] = [ dateField: 'validFrom', dateValue: '2024-04-01T12:19:52Z', }, + // did:key variants — same cryptosuites, but issuer is a self-certifying did:key DID. + { + cryptosuite: 'ecdsa-sd-2023' as CryptoSuiteName, + keyPair: ecdsa2023DidKeyPair, + version: 'v1.1 (did:key)', + credential: { ...modernCredentialV1_1, issuer: ECDSA_DID_KEY_ISSUER }, + dateField: 'issuanceDate', + dateValue: '2024-04-01T12:19:52Z', + }, + { + cryptosuite: 'ecdsa-sd-2023' as CryptoSuiteName, + keyPair: ecdsa2023DidKeyPair, + version: 'v2.0 (did:key)', + credential: { ...modernCredentialV2_0, issuer: ECDSA_DID_KEY_ISSUER }, + dateField: 'validFrom', + dateValue: '2024-04-01T12:19:52Z', + }, + { + cryptosuite: 'bbs-2023' as CryptoSuiteName, + keyPair: bbs2023DidKeyPair, + version: 'v1.1 (did:key)', + credential: { ...modernCredentialV1_1, issuer: BBS_DID_KEY_ISSUER }, + dateField: 'issuanceDate', + dateValue: '2024-04-01T12:19:52Z', + }, + { + cryptosuite: 'bbs-2023' as CryptoSuiteName, + keyPair: bbs2023DidKeyPair, + version: 'v2.0 (did:key)', + credential: { ...modernCredentialV2_0, issuer: BBS_DID_KEY_ISSUER }, + dateField: 'validFrom', + dateValue: '2024-04-01T12:19:52Z', + }, ]; diff --git a/packages/w3c-vc/src/lib/w3c-vc.test.ts b/packages/w3c-vc/src/lib/w3c-vc.test.ts index 68a07d1..c465d97 100644 --- a/packages/w3c-vc/src/lib/w3c-vc.test.ts +++ b/packages/w3c-vc/src/lib/w3c-vc.test.ts @@ -1,9 +1,12 @@ import { describe, expect, it } from 'vitest'; +import { CryptoSuite, generateDidKeyPair, parseDidKey } from '@trustvc/w3c-issuer'; import { deriveCredential, signCredential, verifyCredential } from './w3c-vc'; +import { CryptoSuiteName } from './types'; import { modernCryptosuiteTestScenarios, bbs2020TestScenarios, } from './__fixtures__/test-scenarios'; +import { modernCredentialV2_0 } from './__fixtures__/modern-credentials'; /** * W3C Verifiable Credentials Test Suite @@ -119,6 +122,72 @@ describe('W3C Verifiable Credentials', () => { ); }); + describe('Freshly-generated did:key end-to-end', () => { + const cases: Array<{ name: string; cryptosuite: CryptoSuite; expectedKeyType: string }> = [ + { + name: 'ECDSA-SD-2023 (P-256)', + cryptosuite: CryptoSuite.EcdsaSd2023, + expectedKeyType: 'P-256', + }, + { + name: 'BBS-2023 (BLS12-381 G2)', + cryptosuite: CryptoSuite.Bbs2023, + expectedKeyType: 'Bls12381G2', + }, + ]; + + it.each(cases)( + 'generateDidKeyPair → sign → derive → verify round-trip for $name', + async ({ cryptosuite, expectedKeyType }) => { + const { did, didKeyPairs } = await generateDidKeyPair(cryptosuite); + + // Sanity: the generated DID actually parses to the expected key type + // and its verification method id matches the keypair id. + const parsed = parseDidKey(did); + expect(parsed.keyType).toBe(expectedKeyType); + expect(didKeyPairs.id).toBe(parsed.verificationMethodId); + expect(didKeyPairs.controller).toBe(did); + + // Build a credential issued by the new did:key. + const credential = { + ...modernCredentialV2_0, + issuer: did, + validFrom: '2024-04-01T12:19:52Z', + }; + + // Sign with the freshly-generated keypair. + const signed = await signCredential( + credential, + didKeyPairs, + cryptosuite as CryptoSuiteName, + ); + expect(signed.error).toBeUndefined(); + expect(signed.signed).toBeDefined(); + expect(signed.signed?.proof?.verificationMethod).toBe(didKeyPairs.id); + + // Derive (required before verifying base credentials for these cryptosuites). + const derived = await deriveCredential(signed.signed, [ + '/credentialSubject/billOfLadingName', + ]); + expect(derived.error).toBeUndefined(); + expect(derived.derived).toBeDefined(); + + // Verify — this exercises the did:key dispatch inside queryDidDocument: + // the document loader receives `did:key:zXxx#zXxx`, queryDidDocument + // synthesises the DID document in memory, and jsonld-signatures extracts + // publicKeyMultibase to verify the proof. No network call required. + const verification = await verifyCredential(derived.derived); + expect(verification.error).toBeUndefined(); + expect(verification.verified).toBe(true); + + // Negative case: tampering after verification still trips. + const tampered = { ...derived.derived, validFrom: new Date().toISOString() }; + const tamperedResult = await verifyCredential(tampered); + expect(tamperedResult.verified).toBe(false); + }, + ); + }); + describe('Modern Cryptosuites (ECDSA-SD-2023 & BBS-2023)', () => { describe.each(modernCryptosuiteTestScenarios)( '$cryptosuite $version Credential Operations',