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',