Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.env
coverage/
.fastembed_cache/
4 changes: 4 additions & 0 deletions .mocharc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
spec: 'test/**/*.test.js',
timeout: 5000
};
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# vc-api-mock

A mock server and test suite for the VCALM (Verifiable Credential API
Lifecycle Management) HTTP API, based on the
[W3C VC API](https://w3c-ccg.github.io/vc-api/) and
[W3C VC Data Model 2.0](https://www.w3.org/TR/vc-data-model-2.0/).

Use it to develop against VCALM APIs without a real implementation, or as a
learning tool for understanding the full VC lifecycle.

## Installation

```sh
npm install
```

## Quick Start

```sh
node src/index.js
```

The server listens on port 3000 by default. Set `PORT` to override.

## Usage

### Issue a credential

```sh
curl -X POST http://localhost:3000/credentials/issue \
-H 'Content-Type: application/json' \
-d '{
"credential": {
"@context": ["https://www.w3.org/ns/credentials/v2"],
"type": ["VerifiableCredential"],
"issuer": "did:example:alice",
"validFrom": "2024-01-01T00:00:00Z",
"credentialSubject": {"id": "did:example:bob", "name": "Bob"}
}
}'
```

### Verify a credential

```sh
curl -X POST http://localhost:3000/credentials/verify \
-H 'Content-Type: application/json' \
-d '{"verifiableCredential": <vc>}'
```

### Derive (selective disclosure)

```sh
curl -X POST http://localhost:3000/credentials/derive \
-H 'Content-Type: application/json' \
-d '{
"verifiableCredential": <vc>,
"options": {
"selectivePointers": ["/@context", "/type", "/issuer", "/validFrom"]
}
}'
```

## Endpoints

### Credentials

| Method | Path | Description |
|--------|------|-------------|
| POST | `/credentials/issue` | Issue a credential with a mock proof |
| POST | `/credentials/verify` | Verify a credential |
| POST | `/credentials/derive` | Selective disclosure derivation |
| POST | `/credentials/status` | Update a credential's status |
| GET | `/credentials/:id` | Retrieve a stored credential |
| DELETE | `/credentials/:id` | Soft-delete a credential |

### Presentations

| Method | Path | Description |
|--------|------|-------------|
| POST | `/presentations` | Create a presentation |
| POST | `/presentations/verify` | Verify a presentation |
| GET | `/presentations` | List stored presentations |
| GET | `/presentations/:id` | Retrieve a presentation |
| DELETE | `/presentations/:id` | Soft-delete a presentation |

### Challenges & Status Lists

| Method | Path | Description |
|--------|------|-------------|
| POST | `/challenges` | Issue a single-use nonce |
| POST | `/status-lists` | Create a StatusList2021 credential |
| GET | `/status-lists/:id` | Retrieve a status list (public, no auth) |

### Workflows & Exchanges

Exchanges are stateful. State transitions: `pending → active → complete` (or
`invalid` on error). See [Design Notes](#design-notes) for the full state
machine.

| Method | Path | Description |
|--------|------|-------------|
| POST | `/workflows` | Create a workflow |
| GET | `/workflows/:id` | Retrieve a workflow |
| POST | `/workflows/:id/exchanges` | Create an exchange |
| GET | `/workflows/:id/exchanges/:id` | Get exchange state |
| POST | `/workflows/:id/exchanges/:id` | Participate in an exchange |
| GET | `/workflows/:id/exchanges/:id/protocols` | Get supported protocol URLs |

### Interactions

| Method | Path | Description |
|--------|------|-------------|
| GET | `/interactions/:id?iuv=1` | Get interaction protocols (`iuv=1` required) |

## Options

**`PORT`** — TCP port to listen on (default: `3000`)

## Running Tests

```sh
npm test # unit + integration + conformance tests
npm run test:conformance # conformance suite only (supports VCALM_BASE_URL)
npm run lint # lint src and test
npm run typecheck # JSDoc type checking via tsc
```

To run the conformance suite against a real VCALM server:

```sh
VCALM_BASE_URL=https://your-server.example npm run test:conformance
```

The full OpenAPI 3.0 spec is at [`spec/oas.yaml`](spec/oas.yaml).

## Design Notes

- **No real cryptography.** Proofs are deterministic SHA-256 hashes prefixed with `mock-proof-`. Verification checks the prefix — sufficient for round-trip tests, not for production use.
- **In-memory store.** All state is lost on restart.
- **Single-use challenges.** Nonces issued via `POST /challenges` are consumed on first use at `/presentations/verify`.
- **`redirectUrl` is not supported.** Returns `400` per spec guidance that implementations should reject it.
- **Unsigned presentation `holder` field is ignored.** A warning is logged when `holder` is present but no presentation-level proof exists.

## Contributing

See [the contribute file](https://github.com/digitalbazaar/bedrock/blob/master/CONTRIBUTING.md)!

PRs accepted.

If editing the Readme, please conform to the
[standard-readme](https://github.com/RichardLitt/standard-readme) specification.

## License

[New BSD License (3-clause)](LICENSE) © Digital Bazaar
148 changes: 148 additions & 0 deletions USE_CASES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# VCALM Use Cases

A reference for real-world applications of the Verifiable Credential API Lifecycle Management (VCALM) protocol. Use this to guide mock server design, test fixture selection, and exchange flow implementation.

---

## Core Concepts

### Roles
- **Issuer** — signs and issues credentials (university, government agency, employer)
- **Holder** — stores credentials in a wallet and presents them
- **Verifier** — requests and verifies presentations

The key paradigm shift from traditional identity: the issuer is **not in the loop at verification time**. The holder carries the credential; the verifier checks the cryptographic proof directly.

### Single-Step vs. Multi-Step Exchanges
Most use cases are single-step: holder presents a credential, verifier accepts or rejects. Multi-step exchanges hold state across multiple round-trips, enabling progressive disclosure, step-up auth, and credential chaining.

---

## Single-Step Use Cases

### Digital Identity / Government IDs
Mobile driver's licenses, passport credentials. A holder presents to a bar, airport, or car rental without handing over a physical document. With BBS+ selective disclosure, the verifier receives only what they need (e.g., age verification) — not address, license number, or other claims.

**Flow:** `POST /workflows/{id}/exchanges` → holder presents → verifier accepts

---

### Employee Credentials
An employer issues a "works at Acme Corp" credential to the employee's DID. The employee presents it to a SaaS vendor for an enterprise discount, or to a background check service — without the vendor contacting HR directly.

**Flow:** Single presentation, no credential issued in return

---

### Education / Diplomas
A university issues a degree credential. A job applicant presents it directly to an employer. The employer verifies cryptographically — no transcript requests, no phone calls to the registrar.

**Flow:** Single presentation against an employer's verification endpoint

---

### Healthcare Records
A provider issues vaccination records or prescription credentials to a patient's wallet. The patient presents to any other provider. No faxing records between hospitals or manual release-of-information forms.

**Flow:** Single presentation; verifier may return an access token or confirmation

---

### KYC (Know Your Customer)
A bank verifies a user's identity once and issues a KYC credential. The user presents it to other fintechs to skip redundant verification — significant friction reduction in crypto and DeFi onboarding.

**Flow:** Single presentation; verifier grants account access on success

---

### Supply Chain Provenance
A factory issues a "certified organic" or "conflict-free materials" credential on a product batch. The credential travels with the product and any party in the supply chain can verify it independently.

**Flow:** Single presentation at each handoff point

---

## Multi-Step Use Cases

Multi-step exchanges use the VCALM workflow/exchange protocol to hold state across multiple round-trips. The verifier does not front-load its requirements — each step reveals the minimum needed for that stage.

---

### Step-Up Authentication
Progressive access based on what the holder can prove.

1. Present a "verified human" credential → receive read access
2. Present an employee credential → receive write access
3. Present an MFA or hardware key credential → receive admin access

Each step is only triggered when the user requests higher access. Most users never reach step 3, so their additional credentials are never disclosed unnecessarily.

**VCALM endpoints:** `POST /workflows/{id}/exchanges/{exchangeId}` (repeated per step)

---

### Credential Chaining (Present-to-Get)
Presenting one credential to receive another.

1. Present a university diploma credential
2. Issuer validates and returns an "alumni discount eligible" credential
3. Holder presents the new credential to the actual service

Common in government contexts: present a state ID → receive a federal benefits credential → present to a specific agency program.

**VCALM endpoints:** Exchange returns a new VC in the response body at step 2

---

### Progressive Disclosure with BBS+
Using selective disclosure across multiple steps within a single exchange. (For an overview of BBS+ and other cryptosuites, see [Cryptosuite Reference](#cryptosuite-reference) below.)

1. Present minimal claims (over 18, US resident)
2. Verifier responds: insufficient, also need proof of employment
3. Holder presents employment credential in the same exchange

The exchange retains state so the verifier can reason across all presented credentials together. BBS+ ensures each step reveals only the claims required for that step.

**VCALM endpoints:** `POST /presentations/derive` to generate each selective disclosure proof

---

### KYC Tiering
Incremental account upgrades driven by the holder's needs, not upfront demands.

1. Present basic identity → open a low-limit account
2. Present proof of address + income → upgrade account limits
3. Present accredited investor credential → unlock investment products

Each step is triggered when the user wants more access. Privacy is preserved because the holder never discloses more than necessary for their current goal.

**VCALM endpoints:** Each tier upgrade initiates a new exchange on the same workflow

---

## Cryptosuite Reference

| Suite | Key Type | Selective Disclosure | Notes |
|---|---|---|---|
| `Ed25519Signature2020` | Ed25519 | No | Most common; fast, small signatures |
| `eddsa-rdfc-2022` | Ed25519 / Ed448 | No | Newer RDF canonicalization variant |
| `ecdsa-rdfc-2019` | P-256 / P-384 | No | Required for some government/enterprise contexts |
| `ecdsa-sd-2023` | P-256 | Yes (linkable) | ECDSA-based selective disclosure via JSON Pointers; requires `/presentations/derive` |
| `bbs-2023` | BBS+ (BLS12-381) | Yes (unlinkable) | Unlinkable selective disclosure; required for privacy-preserving `/presentations/derive` |

> **Important:** Both `ecdsa-sd-2023` and `bbs-2023` support selective disclosure and require `POST /presentations/derive` to generate a derived (holder) proof. The key difference is **linkability**: `ecdsa-sd-2023` derived proofs are linkable (each disclosure uses the same base signature and public key, so multiple presentations of the same credential can be correlated), while `bbs-2023` derived proofs are unlinkable (each presentation is cryptographically independent). Use `ecdsa-sd-2023` when you need ECDSA compatibility (e.g., government or enterprise environments that mandate P-256 keys) and linkability is acceptable. Use `bbs-2023` when holder privacy and unlinkability are the primary concern.

### How `ecdsa-sd-2023` Selective Disclosure Works

`ecdsa-sd-2023` implements selective disclosure using a two-phase proof model defined in the [W3C VC Data Integrity ECDSA specification](https://w3c.github.io/vc-di-ecdsa/#selective-disclosure-functions):

1. **Base proof (issuer → holder):** The issuer signs the credential with a long-term key and also generates a per-proof ephemeral P-256 key pair. The issuer signs each individual statement (N-Quad) in the credential with the ephemeral private key, producing a separate signature per statement. Mandatory claims (specified via JSON Pointers) are hashed together; the rest remain individually signed but undisclosed. The base proof is given only to the holder — it is never sent to a verifier.

2. **Derived proof (holder → verifier):** The holder calls `POST /presentations/derive`, supplying the base proof and a set of JSON Pointers indicating which additional (non-mandatory) claims to reveal. The derived proof includes only the signatures for the selected statements, a label map (mapping canonical blank node IDs to HMAC-randomized IDs), and the mandatory claim indexes. The verifier receives only the revealed claims and can independently verify each statement's signature and the mandatory claim hash.

**Key technical properties:**
- Uses **HMAC-based blank node relabeling** to prevent information leakage from blank node ordering
- Mandatory claims are always disclosed; selective claims are opt-in by the holder
- Proof values are CBOR-encoded and Multibase (base64url-no-pad) encoded
- Canonicalization uses RDF Dataset Canonicalization (RDFC-1.0) with SHA-256
- Does **not** provide unlinkable disclosure — see `bbs-2023` if unlinkability is required
33 changes: 33 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import eslintConfig from '@digitalbazaar/eslint-config';

export default [
...eslintConfig,
{
languageOptions: {
globals: {
// Node.js globals
process: 'readonly',
console: 'readonly',
crypto: 'readonly'
}
},
rules: {
'no-console': 'off',
'@stylistic/max-len': ['error', {code: 80, ignoreComments: true}]
}
},
{
// Test files — add mocha globals
files: ['test/**/*.js'],
languageOptions: {
globals: {
describe: 'readonly',
it: 'readonly',
before: 'readonly',
after: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly'
}
}
}
];
Loading
Loading