Skip to content

european-epc-competence-center/key-service

Repository files navigation

Test and Build

Key Service

A secure NestJS-based cryptographic signing service for W3C Verifiable Credentials (VC Data Model 2.0) and Verifiable Presentations. The service implements multiple signature formats with enterprise-grade key management and multi-layer encryption.

Features

  • Multiple Signature Formats: JWT-VC, Data Integrity proofs (Ed25519Signature2020), and SD-JWT
  • Cryptographic Algorithms: Ed25519, ES256, PS256 support
  • Secure Key Management: Automatic key pair generation with PostgreSQL storage
  • Multi-Layer Encryption: Keys encrypted with service + key access secrets
  • W3C Standards Compliance: Full VC Data Model 2.0 and DID Core support
  • Input Validation: Comprehensive security-focused validation using class-validator
  • Failed Attempt Protection: Rate limiting with cooldown mechanisms
  • Health Checks: Kubernetes-ready liveness and readiness probes
  • RESTful API: Clean HTTP endpoints with comprehensive error handling
  • TypeScript: Fully typed with extensive type definitions
  • Testing: Comprehensive unit and integration test coverage
  • Docker Support: Production-ready containers with PostgreSQL integration

Local Testing

# Install dependencies
npm install

# Run in development mode (generates docker/signing-key if missing)
npm run dev

# Run in production mode locally (same signing key setup)
npm start

The service includes comprehensive test coverage with both unit tests (using mocks) and end-to-end (E2E) tests (using a real PostgreSQL database).

Quick Test Commands

# Run all tests (E2E + unit)
npm test

# Run all tests with coverage
npm run test:coverage

Unit Tests

Run unit tests with mocked dependencies (no database required):

# Run all unit tests
npm run test:unit

# Run unit tests in watch mode
npm run test:unit:watch

# Run unit tests with coverage
npm run test:unit:coverage

End-to-End (E2E) Tests

Run E2E tests with a real PostgreSQL database. These tests validate the full application stack including database operations, API endpoints, and integrations.

# Run E2E tests (requires database)
npm run test:e2e

# Run E2E tests in watch mode
npm run test:e2e:watch

# Run E2E tests with coverage
npm run test:e2e:coverage

Test Database Management

# Start PostgreSQL test database
npm run test:db:start

# Stop and remove test database
npm run test:db:stop

# Run unit tests with database (start + test + stop)
npm run test:unit:with-db

The test database runs PostgreSQL 17 in Docker and is automatically configured with test credentials.

Test Configuration

  • E2E Tests: apps/app/test/jest-e2e.json
  • Unit Tests: apps/app/test/jest-unit.json
  • Test Setup: apps/app/test/test-setup.ts
  • Test Database: docker/docker-compose.test.yml

Test Coverage

The test suite includes:

  • ✅ Key generation and storage
  • ✅ Multi-secret encryption/decryption
  • ✅ JWT-VC signing
  • ✅ Data Integrity signing
  • ✅ Input validation (comprehensive security tests)
  • ✅ Failed attempt protection
  • ✅ Health check endpoints
  • ✅ Error handling and edge cases

Database Setup

This service uses PostgreSQL to store encrypted keys. You can run PostgreSQL alone or as part of the full Docker Compose stack (see Docker).

# PostgreSQL only (from project root)
docker compose -f docker/docker-compose.yml up -d postgres

# Wait for database to be ready
docker compose -f docker/docker-compose.yml logs -f postgres

When running the app with npm run dev against this database, set TLS env vars (certificates are created by npm run docker:certs):

npm run docker:certs
docker compose -f docker/docker-compose.yml up -d postgres
export DB_SSL=true DB_SSL_MODE=verify-full
export DB_SSL_CA=./docker/certs/postgres/ca.crt
export DB_SSL_CERT=./docker/certs/postgres/client.crt
export DB_SSL_KEY=./docker/certs/postgres/client.key
npm run dev

npm run docker:up generates certificates automatically and starts the full mTLS stack.

Environment Variables

The service is configured using environment variables. Create a .env file in the project root:

Required Variables

# Database Configuration
DB_HOST=localhost              # PostgreSQL host
DB_PORT=5432                   # PostgreSQL port
DB_USERNAME=postgres           # Database username
DB_PASSWORD=postgres           # Database password
DB_NAME=key_service           # Database name
DB_SSL=false                  # Enable TLS: true (Docker Compose sets true with mTLS)
# When DB_SSL=true, also set:
# DB_SSL_MODE=verify-full     # verify-ca | verify-full | require | disable (default: verify-full)
# DB_SSL_CA=/path/to/ca.crt   # CA bundle for server verification (required for verify-* modes)
# DB_SSL_CERT=/path/to/client.crt   # Client cert (mTLS — required by Docker Compose postgres)
# DB_SSL_KEY=/path/to/client.key    # Client key (mTLS)

# Application Configuration
NODE_ENV=development          # Environment: development, production
PORT=3000                     # Application port (default: 3000)
SIGNING_KEY_PATH=/run/secrets/signing-key  # Path to signing key file

Optional Variables

# CORS Configuration
CORS_ENABLED=true             # Enable/disable CORS (default: true)
CORS_ORIGINS=https://example.com,https://app.example.com  # Allowed origins (comma-separated, default: all)
CORS_METHODS=GET,POST,PUT,DELETE  # Allowed methods (default: GET,HEAD,PUT,PATCH,POST,DELETE)
CORS_CREDENTIALS=false        # Allow credentials (default: false)
CORS_MAX_AGE=86400           # Preflight cache duration in seconds (default: 86400)

# Security Configuration
PBKDF2_ITERATIONS=100000     # PBKDF2 iterations for key derivation (default: 100000)

Production Recommendations

  • Set NODE_ENV=production in production environments
  • Always configure CORS_ORIGINS with specific trusted domains in production
  • Use strong database passwords
  • Enable database TLS/mTLS for production (DB_SSL=true, DB_SSL_MODE=verify-full, CA + client cert paths — see .cursor/plans/postgresql-mtls.md)
  • Do not use DB_SSL_MODE=require in production — it encrypts traffic but does not authenticate the server (see plan for details)
  • Provide a cryptographically random signing key (minimum 32 characters) via SIGNING_KEY_PATH — never commit it to version control (see Signing key)
  • For Kubernetes deployments, use the Helm chart and mount secrets from Vault or equivalent — see helm/README.md

Manual Database Setup

If you prefer to set up PostgreSQL manually:

  1. Install PostgreSQL
  2. Create a database named key_service
  3. Create a user with appropriate permissions
  4. Update the environment variables accordingly

The application will automatically create the required tables on startup when NODE_ENV is not set to production.

Database Migrations

The service uses TypeORM migrations for database schema management:

# Generate a new migration from entity changes
npm run migration:generate -- migrations/MigrationName

# Create a blank migration file
npm run migration:create -- migrations/MigrationName

# Run pending migrations
npm run migration:run

# Revert the last migration
npm run migration:revert

Migrations are stored in the migrations/ directory and are automatically run on application startup in development mode.

Docker

The service includes production-ready Docker containers with PostgreSQL integration.

Signing key (required for Docker Compose)

The service reads a service signing key from a file (default path: /run/secrets/signing-key, overridable via SIGNING_KEY_PATH). This key is combined with user-provided secrets to derive encryption keys for stored key material. The file is not stored in this repository.

Docker Compose mounts a host file into the container:

# docker/docker-compose.yml
volumes:
  - ./signing-key:/run/secrets/signing-key:ro

Choose one of the following:

Local development / testing

Generate a random key locally (creates gitignored docker/signing-key):

npm run docker:signing-key

Or let the convenience script create it on first start:

npm run docker:up

If you change or regenerate the signing key, existing encrypted keys in the local database will no longer decrypt. Reset local data with:

docker compose -f docker/docker-compose.yml down -v

Production or staging (Docker Compose)

Create a production-grade secret outside the repository and mount it read-only:

openssl rand -base64 64 > /secure/path/signing-key
chmod 600 /secure/path/signing-key

Point the Compose volume at that file (edit docker/docker-compose.yml or use an override file):

volumes:
  - /secure/path/signing-key:/run/secrets/signing-key:ro

Store and rotate this key using your organization's secret management process. For Kubernetes, prefer the Helm chart with Vault or Kubernetes secrets rather than a host file — see helm/README.md.

Using Docker Compose (Recommended)

The easiest way to run the full stack locally:

# Start PostgreSQL + Key Service (generates a local signing key if missing)
npm run docker:up

# Or manually: create signing key first, then start
npm run docker:signing-key
docker compose -f docker/docker-compose.yml up -d

# View logs
docker compose -f docker/docker-compose.yml logs -f

# Stop all services
docker compose -f docker/docker-compose.yml down

# Stop and remove volumes (cleans database)
docker compose -f docker/docker-compose.yml down -v

The Docker Compose setup includes:

  • PostgreSQL 17 database with persistent storage
  • Key Service with health checks (NODE_ENV=production)
  • Network isolation
  • Read-only volume mount for the signing key (see Signing key)

Building Docker Image Manually

# Build production image (--pull recommended: refreshes distroless base, including OpenSSL)
docker build --pull -t key-service:latest -f docker/Dockerfile .

# Run with environment variables and signing key mounted
docker run -p 3000:3000 \
  -v /secure/path/signing-key:/run/secrets/signing-key:ro \
  -e NODE_ENV=production \
  -e DB_HOST=postgres \
  -e DB_NAME=key_service \
  -e DB_USERNAME=postgres \
  -e DB_PASSWORD=postgres \
  key-service:latest

For local testing, use /path/to/docker/signing-key after running npm run docker:signing-key.

Docker Health Checks

The container includes health checks that verify:

  • Application is running
  • HTTP server is responding
  • Health endpoint returns 200 OK

Docker Configuration

  • Dockerfile: docker/Dockerfile - Multi-stage production build
  • Compose: docker/docker-compose.yml - Full stack with PostgreSQL
  • Test Compose: docker/docker-compose.test.yml - Test database only

API Endpoints

Sign Verifiable Credential

Sign a W3C Verifiable Credential with the specified signature format.

POST /sign/vc/:type

Parameters:

  • type: Signature type - jwt, data-integrity, or sd-jwt

Request Body:

{
  "verifiable": {
    "@context": ["https://www.w3.org/2018/credentials/v1"],
    "type": ["VerifiableCredential"],
    "issuer": "did:example:123",
    "issuanceDate": "2023-01-01T00:00:00Z",
    "credentialSubject": {
      "id": "did:example:456",
      "name": "John Doe"
    }
  },
  "secrets": ["user-secret-key"],
  "identifier": "key-identifier"
}

Response:

Returns the signed credential in the format specified by the signature type.

Sign Verifiable Presentation

Sign a W3C Verifiable Presentation with the specified signature format.

POST /sign/vp/:type

Parameters:

  • type: Signature type - jwt, data-integrity, or sd-jwt

Request Body:

{
  "verifiable": {
    "@context": ["https://www.w3.org/2018/credentials/v1"],
    "type": ["VerifiablePresentation"],
    "holder": "did:example:123",
    "verifiableCredential": [...]
  },
  "secrets": ["user-secret-key"],
  "identifier": "key-identifier",
  "challenge": "nonce-value",
  "domain": "example.com"
}

Response:

Returns the signed presentation in the format specified by the signature type.

Proof of possession (OpenID4VCI JWT or linked-data VP)

Use this for credential-request key proofs (OpenID4VCI Appendix F.1 JWT) or a Data Integrity VP without going through POST /sign/vp/jwt with OID4VCI typ.

POST /sign/pop/:type

Parameters:

  • type: Same values as POST /sign/vp/:type (jwt, data-integrity, sd-jwt). For POP, use jwt (OpenID4VCI Appendix F.1 proof JWT) or data-integrity (same as POST /sign/vp/data-integrity). sd-jwt returns 400.

Request body: SignRequestDtosecrets, identifier, optional verifiable, domain (required for both PoP types: Credential Issuer Identifier — OpenID4VCI F.1 JWT aud, F.2 di_vp proof domain), optional challenge (F.1 JWT nonce / F.2 proof challenge when the issuer uses c_nonce). For POST /sign/vc and POST /sign/vp, verifiable is required (validated in the service). For POST /sign/pop/jwt, verifiable is optional and ignored. For POST /sign/pop/data-integrity, the service always builds a minimal VP shell per F.2 and signs it via the same path as POST /sign/vp/data-integrity; request verifiable is ignored (use POST /sign/vp/data-integrity for a custom VP). JWT PoP uses JOSE typ openid4vci-proof+jwt and a minimal F.1 JWT body, not a VC.

Generate Key Pair

Generate a new cryptographic key pair and store it encrypted in the database.

POST /generate

Request Body:

{
  "secrets": ["user-secret-key"],
  "identifier": "key-identifier",
  "signatureType": "Ed25519",
  "keyType": "JWK"
}

Parameters:

  • secrets: Array of 1-10 secrets for multi-layer encryption
  • identifier: Unique identifier for the key (alphanumeric, -_:. allowed)
  • signatureType: Algorithm - Ed25519, ES256, or PS256
  • keyType: Key format - JWK or VerificationKey2020

Response:

Returns success confirmation without exposing the private key.

Health Check

The service provides comprehensive health check endpoints optimized for Kubernetes:

/health

General health check endpoint that includes database connectivity check.

/health/liveness

Liveness probe endpoint for Kubernetes. Returns 200 if the application is running.

/health/readiness

Readiness probe endpoint for Kubernetes. Returns 200 if the application is ready to serve traffic (includes database connectivity check).

Response Format:

{
  "status": "ok",
  "info": {
    "database": {
      "status": "up"
    }
  },
  "error": {},
  "details": {
    "database": {
      "status": "up"
    }
  }
}

Sample Requests

Generate a Key Pair

First, generate a key pair that will be used for signing:

curl -X POST http://localhost:3000/generate \
  -H "Content-Type: application/json" \
  -d '{
    "secrets": ["my-secret-key"],
    "identifier": "my-signing-key",
    "signatureType": "Ed25519",
    "keyType": "JWK"
  }'

Sign a Verifiable Credential (JWT)

curl -X POST http://localhost:3000/sign/vc/jwt \
  -H "Content-Type: application/json" \
  -d '{
    "verifiable": {
      "@context": ["https://www.w3.org/2018/credentials/v1"],
      "type": ["VerifiableCredential"],
      "issuer": "did:example:123",
      "issuanceDate": "2023-01-01T00:00:00Z",
      "credentialSubject": {
        "id": "did:example:456",
        "name": "John Doe"
      }
    },
    "secrets": ["my-secret-key"],
    "identifier": "my-signing-key"
  }'

Sign a Verifiable Credential (Data Integrity)

curl -X POST http://localhost:3000/sign/vc/data-integrity \
  -H "Content-Type: application/json" \
  -d '{
    "verifiable": {
      "@context": ["https://www.w3.org/2018/credentials/v1"],
      "type": ["VerifiableCredential"],
      "issuer": "did:example:123",
      "issuanceDate": "2023-01-01T00:00:00Z",
      "credentialSubject": {
        "id": "did:example:456",
        "name": "John Doe"
      }
    },
    "secrets": ["my-secret-key"],
    "identifier": "my-signing-key"
  }'

Sign a Verifiable Presentation

curl -X POST http://localhost:3000/sign/vp/jwt \
  -H "Content-Type: application/json" \
  -d '{
    "verifiable": {
      "@context": ["https://www.w3.org/2018/credentials/v1"],
      "type": ["VerifiablePresentation"],
      "holder": "did:example:123",
      "verifiableCredential": []
    },
    "secrets": ["my-secret-key"],
    "identifier": "my-signing-key",
    "challenge": "random-nonce-12345",
    "domain": "example.com"
  }'

Sample Responses

Signed JWT Credential

GS1 GO Sample

eyJraWQiOiJkaWQ6d2ViOmNicHZzdmlwLXZjLmdzMXVzLm9yZyNJeXIwZndUdmRsRVJrOEVCWFZ1SXM3NjgyeW45ZGpjQng2aEdtemNhb2FzIiwiYWxnIjoiRVMyNTYifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3JlZi5nczEub3JnL2dzMS92Yy9saWNlbnNlLWNvbnRleHQiXSwiaWQiOiJodHRwczovL2NicHZzdmlwLXZjLWFwaS5nczF1cy5vcmcvY3JlZGVudGlhbHMvMDgxMDE1OTU1IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkdTMUNvbXBhbnlQcmVmaXhMaWNlbnNlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6d2ViOmNicHZzdmlwLXZjLmdzMXVzLm9yZyIsIm5hbWUiOiJHUzEgVVMifSwibmFtZSI6IkdTMSBDb21wYW55IFByZWZpeCBMaWNlbnNlIiwiZGVzY3JpcHRpb24iOiJUSElTIEdTMSBESUdJVEFMIExJQ0VOU0UgQ1JFREVOVElBTCBJUyBGT1IgVEVTVElORyBQVVJQT1NFUyBPTkxZLiBBIEdTMSBDb21wYW55IFByZWZpeCBMaWNlbnNlIGlzIGlzc3VlZCBieSBhIEdTMSBNZW1iZXIgT3JnYW5pemF0aW9uIG9yIEdTMSBHbG9iYWwgT2ZmaWNlIGFuZCBhbGxvY2F0ZWQgdG8gYSB1c2VyIGNvbXBhbnkgb3IgdG8gaXRzZWxmIGZvciB0aGUgcHVycG9zZSBvZiBnZW5lcmF0aW5nIHRpZXIgMSBHUzEgaWRlbnRpZmljYXRpb24ga2V5cy4iLCJ2YWxpZEZyb20iOiIyMDI0LTAxLTI1VDEyOjMwOjAwLjAwMFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDp3ZWI6aGVhbHRoeXRvdHMubmV0Iiwib3JnYW5pemF0aW9uIjp7ImdzMTpwYXJ0eUdMTiI6IjA4MTAxNTk1NTAwMDAiLCJnczE6b3JnYW5pemF0aW9uTmFtZSI6IkhlYWx0aHkgVG90cyJ9LCJleHRlbmRzQ3JlZGVudGlhbCI6Imh0dHBzOi8vaWQuZ3MxLm9yZy92Yy9saWNlbnNlL2dzMV9wcmVmaXgvMDgiLCJsaWNlbnNlVmFsdWUiOiIwODEwMTU5NTUiLCJhbHRlcm5hdGl2ZUxpY2Vuc2VWYWx1ZSI6IjgxMDE1OTU1In0sImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2lkLmdzMS5vcmcvdmMvc2NoZW1hL3YxL2NvbXBhbnlwcmVmaXgiLCJ0eXBlIjoiSnNvblNjaGVtYSJ9LCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly9jYnB2c3ZpcC12Yy1hcGkuZ3MxdXMub3JnL3N0YXR1cy84MDFjNmNjNi00ZmM0LTRhYTMtYTM0Ny0zYjMxYTE3NWFjMTQjMTAwMTAiLCJ0eXBlIjoiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMDAxMCIsInN0YXR1c0xpc3RDcmVkZW50aWFsIjoiaHR0cHM6Ly9jYnB2c3ZpcC12Yy1hcGkuZ3MxdXMub3JnL3N0YXR1cy84MDFjNmNjNi00ZmM0LTRhYTMtYTM0Ny0zYjMxYTE3NWFjMTQvIn19.qP5k8uO9yuQdIWSl9ws5FlRFmUOzxgNgtWs8VFifVEjLWTUx1Hc1m4fIet7EF0njlelNy8G5SZarW7DDaWiJDA

Signed Data Integrity Credential

{
  "signedCredential": {
    "@context": ["https://www.w3.org/2018/credentials/v1"],
    "type": ["VerifiableCredential"],
    "issuer": "did:example:123",
    "issuanceDate": "2023-01-01T00:00:00Z",
    "credentialSubject": {
      "id": "did:example:456",
      "name": "John Doe"
    },
    "proof": {
      "type": "Ed25519Signature2020",
      "created": "2023-01-01T00:00:00Z",
      "verificationMethod": "did:example:123#key-1",
      "proofPurpose": "assertionMethod",
      "proofValue": "z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz"
    }
  }
}

Security Features

Input Validation

The service implements comprehensive input validation to prevent injection attacks and buffer overflows:

  • Global ValidationPipe: Enabled with security-focused configuration
  • DTO Validation: All API requests validated using class-validator decorators
  • Security Constraints:
    • Identifier max length: 500 characters
    • Secret max length: 1000 characters
    • Array size limits: 1-10 secrets per request
    • Pattern matching: Identifiers only allow alphanumeric characters and -_:.
    • Enum validation for signature and key types

Multi-Layer Encryption

Keys are encrypted using multiple secrets to ensure security even if one secret is compromised:

  • Service Secret: Application-level encryption key
  • Key Access Secret: Per-key encryption secret
  • User Secrets: User-provided secrets (1-10 per key)

Failed Attempt Protection

  • Rate limiting on key access attempts
  • Configurable cooldown mechanisms
  • Protection against brute force attacks

Zero-Knowledge Architecture

  • Private keys never exposed in decrypted form
  • Keys only decrypted in memory during signing operations
  • Secure memory clearing after use

Key Dependencies

Core Framework

  • @nestjs/core & @nestjs/common: NestJS framework
  • @nestjs/typeorm: TypeORM integration for database operations
  • @nestjs/terminus: Health check endpoints

Cryptography & Verifiable Credentials

  • @digitalbazaar/vc: W3C Verifiable Credentials implementation
  • @digitalbazaar/ed25519-signature-2020: Ed25519 signature suite for Data Integrity proofs
  • @digitalbazaar/ed25519-verification-key-2020: Ed25519 key handling
  • jose: JWT operations (JWT-VC signing)
  • @noble/curves: Cryptographic curve operations
  • jsonld-signatures: JSON-LD signature support

Validation & Security

  • class-validator: DTO validation decorators
  • class-transformer: Request payload transformation
  • node-cache: In-memory caching for failed attempts

Database

  • typeorm: TypeScript ORM
  • pg: PostgreSQL client

Project Structure

key-service/
├── apps/app/src/
│   ├── config/              # Configuration (CORS, database, rate limiting)
│   ├── filters/             # Global exception handling
│   ├── health/              # Health check endpoints
│   ├── key-services/        # Key management services
│   │   ├── key.service.ts   # Core key operations
│   │   ├── key-storage.service.ts
│   │   ├── secret.service.ts
│   │   └── failed-attempts-cache.service.ts
│   ├── signing-services/    # Credential signing services
│   │   ├── jwt-signing.service.ts
│   │   └── data-integrity-signing.service.ts
│   ├── types/               # TypeScript type definitions
│   ├── utils/               # Utility functions and logging
│   └── main.ts              # Application entry point
├── docker/                  # Docker configuration
├── migrations/              # Database migrations
├── docs/                    # Documentation
└── scripts/                 # Build and release scripts

License

Copyright 2025 European EPC Competence Center GmbH (EECC). Corresponding Author: Christian Fries christian.fries@eecc.de

AGPLV3

All code published in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

Contributing

This is a security-critical component. Please open issues to discuss any proposed changes. PRs are welcome but will be critically reviewed by the maintainers. Please first discuss changes before opening PRs.

Further Documentation

About

A cryptographic key service for storing private keys and using them to sign verifiable credentials and presentations

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors