A Go-based microservices backend for issuing and verifying digital credentials, originally created within the DC4EU (Digital Credentials for Europe) project.
The platform implements the OpenID4VCI and OpenID4VP protocols to issue and verify credentials in SD-JWT VC, W3C Verifiable Credentials 2.0, and ISO/IEC 18013-5 mdoc formats.
# 1. Generate development PKI certificates
make pki
# 2. Start all services (MongoDB + microservices)
make start
# 3. Verify everything is running
docker compose psThe services will be available on the internal Docker network (172.16.50.0/24):
| Service | Address |
|---|---|
| API Gateway | http://apigw.vc.docker:8080 |
| Issuer | http://issuer.vc.docker:8080 |
| Verifier | http://verifier.vc.docker:8080 |
| Registry | http://registry.vc.docker:8080 |
| MongoDB | mongodb://mongo.vc.docker:27017 |
To access a service from the host, use its container IP directly (e.g. http://172.16.50.2:8080 for apigw) or publish ports in docker-compose.yaml.
To stop everything: make stop
The main configuration file is config.yaml. See docs/CONFIGURATION.md for details.
- Docker and Docker Compose
- GNU Make
| Service | Description |
|---|---|
| apigw | API Gateway with optional SAML and OIDC RP support for relying party integration |
| issuer | Issues verifiable credentials via OpenID4VCI with VCTM schema validation |
| verifier | Verifies credential presentations via OpenID4VP, DCQL, and the W3C Digital Credentials API |
| registry | Credential registry and status list management |
A relying party (e.g. Keycloak) connects to the Verifier as a standard OIDC Provider. The Verifier translates the OIDC request into an OpenID4VP presentation request toward the wallet.
┌────────────┐ OIDC ┌──────────────────────────┐
│ Relying │ ──────────────────────► │ VERIFIER │
│ Party │ authorize, token, │ │
│ (Keycloak) │ userinfo │ OIDC Provider (OP) │
└────────────┘ ◄────────────────────── │ OpenID4VP Verifier │
└────────────┬─────────────┘
│
│ OpenID4VP
│ (present credential)
▼
┌──────────────────────────┐
│ WALLET │
│ (phone app) │
└──────────────────────────┘
PID issuance and non-PID issuance follow different authentication paths
determined by auth_method in the credential configuration.
Wallet requests credential
via OpenID4VCI (PAR → Authorize)
│
┌────────────┴────────────┐
│ │
auth_method: basic auth_method: openid4vp
│ │
┌──────────▼──────────┐ ┌──────────▼───────────────┐
│ PATH 1: PID │ │ PATH 2: OTHER │
│ (pid) │ │ (ehic, diploma, pda1, │
│ │ │ elm, eduid, micro...) │
│ User authenticates │ │ │
│ via external IdP: │ │ Wallet presents existing │
│ │ │ PID credential back to │
│ • SAML IdP │ │ APIGW via OpenID4VP │
│ • OIDC Provider │ │ │
│ • Username/Pass │ │ APIGW verifies PID, │
│ (dev/test) │ │ extracts identity, looks │
│ │ │ up document in datastore │
└──────────┬──────────┘ └──────────┬────────────────┘
│ │
└────────────┬────────────┘
│
▼
┌──────────────────────────┐
│ APIGW │
│ OAuth AS (OpenID4VCI) │
│ Consent → Token → │
│ Credential │
└─────┬──────────────┬──────┘
│ gRPC │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ ISSUER │ │ REGISTRY │
│ │ │ │
│ Signs cred │ │ Token │
│ (SD-JWT VC, │ │ Status List │
│ mdoc, │ │ (revocation)│
│ W3C VC) │ │ │
└──────────────┘ └──────────────┘
┌────────────┐
│ SAML IdP / │
┌────────────┐ │ OIDC IdP │
│ Relying │ │ (external) │
│ Party │ └──────┬─────┘
│ (Keycloak) │ │
└─────┬──────┘ SAML / OIDC RP │
│ OIDC (PID issuance auth) │
▼ │
┌──────────────────┐ OpenID4VP ┌──────────────┐ │
│ VERIFIER │ ◄─────────────────────── │ │ │
│ │ (present credential) │ │ │
│ OIDC Provider │ ───────────────────────► │ WALLET │ │
│ OpenID4VP │ (request presentation) │ (phone app) │ │
└──────────────────┘ │ │ │
│ │ │
OpenID4VCI │ │ │
(receive new credential) │ │ │
┌──────────────────────────────────────┤ │ │
│ OpenID4VP │ │ │
│ (present PID for non-PID issue) │ │ │
│ ┌───────────────────────────────────┤ │ │
│ │ └──────────────┘ │
▼ ▼ │
┌────────────────────────────────────────────────────────────────┐ │
│ APIGW │ │
│ │◄───┘
│ OAuth AS (OpenID4VCI) — issue credentials to wallet │
│ OpenID4VP Verifier — verify PID before non-PID issuance │
│ SAML SP (optional) — authenticate for PID issuance │
│ OIDC RP (optional) — authenticate for PID issuance │
└──────────┬──────────────────────────────┬──────────────────────┘
│ gRPC │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ ISSUER │ │ REGISTRY │
│ Signs creds │ │ Token Status │
│ (SD-JWT VC, │ │ List │
│ mdoc, W3C VC) │ │ (revocation) │
└──────────────────┘ └──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ UI │ │ MOCK AS │
│ Admin web │──────────►│ Test users & │
│ interface │ │ bootstrapping │
└──────────────────┘ └──────────────────┘
| Component | Server Role | Client Role |
|---|---|---|
| Verifier | OIDC Provider (OP) toward relying parties | OpenID4VP verifier toward wallets |
| APIGW | OAuth AS (OpenID4VCI) toward wallets | SAML SP + OIDC RP toward external IdPs (PID issuance) |
| OpenID4VP verifier (non-PID issuance auth) | ||
| Issuer | gRPC credential signing service | — |
| Registry | Token Status List (revocation) | — |
- Issue verifiable credentials via OpenID4VCI 1.0
- Verify credential presentations via OpenID4VP 1.0 with
direct_postresponse mode - SD-JWT VC (draft-ietf-oauth-sd-jwt-vc-13), W3C Verifiable Credentials 2.0, and ISO/IEC 18013-5 mdoc credential formats
- Browser-native credential presentation via the W3C Digital Credentials API (
navigator.credentials.get()) - DCQL (Digital Credentials Query Language) for flexible presentation requests
- Credential revocation via Token Status List (draft-ietf-oauth-status-list) with JWT (RFC 7519) and CWT (RFC 8392) formats
- VCTM schema validation before credential issuance (draft-ietf-oauth-sd-jwt-vc-13 §6)
- PKCS#11 / HSM support for hardware-backed key protection
- SAML 2.0, OpenID Connect Core 1.0, and OAuth 2.0 Relying Party authentication with PKCE (RFC 7636), DPoP (RFC 9449), and JAR (RFC 9101)
- gRPC inter-service communication
- Kafka-based message brokering
- MongoDB storage backend
- OpenTelemetry distributed tracing
- Static Linux/amd64 binaries for containerized deployment
latest tracks the latest tag available and is built from branch main.
main is the stable development branch.
| Service | Command | Description |
|---|---|---|
| All | make build |
Build all services |
| apigw | make build-apigw |
API Gateway |
| issuer | make build-issuer |
Credential Issuer |
| verifier | make build-verifier |
Credential Verifier |
| registry | make build-registry |
Registry |
| vc20-test-server | make build-vc20-test-server |
W3C VC 2.0 test server |
All standard builds produce static binaries (CGO_ENABLED=0) for linux/amd64. Output goes to ./bin/.
Optional features are enabled via Go build tags. The following tags are available:
| Tag | Description | Affected service(s) | CGO | Make target |
|---|---|---|---|---|
saml |
SAML IdP support | apigw | static | make build-apigw-saml |
oidcrp |
OpenID Connect Relying Party | apigw | static | make build-apigw-oidcrp |
saml,oidcrp |
All optional apigw features | apigw | static | make build-apigw-all |
pkcs11 |
PKCS#11 HSM signing | issuer | dynamic | make build-issuer-hsm |
vc20 |
W3C Verifiable Credentials 2.0 | vc20-test-server | static | make build-vc20-test-server |
zknative |
Native ZK/PPID proof verification (mso_mdoc_zk) - Longfellow + Vega | verifier | dynamic | make build-verifier-zknative (+ make build-zkvegaverifyworker for Vega) |
Note: The
pkcs11andzknativetags require CGO (CGO_ENABLED=1) and produce dynamically linked binaries. See "Native ZK/PPID proof verification" below forzknativesetup.
For convenience all services can be built inside a Docker container.
| Command | Description |
|---|---|
make docker-build |
Build all Docker images |
make docker-build-<service> |
Build a specific service image |
make docker-build-apigw-saml |
apigw image with SAML support |
make docker-build-apigw-oidcrp |
apigw image with OIDC RP support |
make docker-build-apigw-all |
apigw image with all features |
make docker-build-issuer-hsm |
issuer image with PKCS#11 HSM support |
make docker-push |
Push all standard images to registry |
make docker-push-apigw-saml |
Push apigw SAML image |
make docker-push-apigw-oidcrp |
Push apigw OIDC RP image |
make docker-push-apigw-all |
Push apigw all-features image |
make docker-push-issuer-hsm |
Push issuer HSM image |
make docker-tag |
Tag all images |
Set the image version with VERSION=x.x.x (default: latest).
vc-verifier can verify "mso_mdoc_zk" presentations natively for two
independent ZK systems, both opt-in via the zknative build tag (the
default make build-verifier/Docker build stays CGO_ENABLED=0 and
fully static, exactly like the pkcs11 tag above):
- Longfellow - zero-knowledge proof-of-possession with an optional
pairwise pseudonym, via a cgo binding to
zk-cred-longfellow's
plain C-ABI Go verifier build, linked directly into the main verifier
process (
pkg/mdoc/zknative). - Vega - a second, general-purpose ZK circuit backend
(zk-cred-vega),
currently only publishing an mdoc circuit but designed for a wider
range of provable statements over time. Unlike Longfellow, Vega's cgo
binding (
pkg/mdoc/zknative_vega) is never linked into the main verifier process - it's linked only into a small standalonecmd/zkvegaverifyworkersubprocess binary, which the verifier execs per-call over stdin/stdout. This isolates the actual cgo call touching attacker-controlled proof bytes: a memory-safety fault in the native library takes down one worker process, not the verifier serving other in-flight requests. Seepkg/mdoc/zkvegaworker's package doc for the wire protocol anddocs/ZK_VEGA_DIGESTID_WIRE_EXTENSION.mdfor a real wire-format addition Vega's verification needed (flagged there, not yet raised with multipaz upstream).
Setup:
# 1. Clone + build both crates' `go-cabi` targets, staging the resulting
# shared libraries + headers under third_party/ (gitignored - never
# committed). Override ZK_CRED_LONGFELLOW_REPO/_REF or
# ZK_CRED_VEGA_REPO/_REF to build a fork or pin a specific tag, branch,
# OR commit SHA - each target fetches whichever ref is given directly
# (`git fetch --depth 1 origin $(REF)`), which works for all three ref
# types.
make zk-native-lib # Longfellow
make zk-native-lib-vega # Vega
# 2. Build the verifier (links Longfellow only - see above for why Vega
# doesn't need to be here) and, separately, the Vega worker subprocess.
make build-verifier-zknative
make build-zkvegaverifyworker
# 3. Run the verifier. Both shared libraries need to be on the loader
# path - the verifier binary links Longfellow, and it execs
# zkvegaverifyworker, which links Vega and inherits this environment.
# The worker's binary also needs to be on PATH (or point
# ZkVerifierConfig.VegaWorkerPath at it directly).
LD_LIBRARY_PATH=$(pwd)/third_party/zk-cred-longfellow/lib:$(pwd)/third_party/zk-cred-vega/lib \
PATH=$(pwd)/bin:$PATH \
./bin/vc_verifier-zknative
# ...or run pkg/mdoc's zknative-tagged tests directly (covers both
# systems; make test-zknative sets the loader path for both libraries
# itself):
make test-zknativeConfiguration: the circuit catalog service vc-verifier fetches circuits
from is configurable via verifier.zk_circuits.sources in YAML config
(defaults to the live https://zk-circuits.fly.dev service if unset) -
see docs/CONFIGURATION.md's zk_circuits section. This applies to both
systems; Vega additionally resolves a verifier-key catalog entry from the
wallet-declared prover-key entry via the manifest (see
getOrLoadVegaVerifierKey's doc comment in pkg/mdoc/zk_native_cgo_vega.go).
See docs/ZK_PPID_VERIFICATION_PLAN.md for the full Longfellow design
writeup: what this verifies, the confirmed
verifier_context/pseudonym-derivation wire formula, and exactly what's
still out of scope (the W3C Digital Credentials API and older OpenID4VP
session-transcript variants - both apply to Vega too, which has no
pseudonym concept of its own yet either).
| Command | Description |
|---|---|
make start |
Start all services via docker-compose |
make stop |
Stop all services |
make restart |
Restart all services |
make pki |
Generate PKI infrastructure |
make pki-clean |
Remove PKI material |
| Command | Description |
|---|---|
make test |
Run all service tests |
make test-saml |
Test with saml build tag |
make test-oidcrp |
Test with oidcrp build tag |
make test-vc20 |
Test with vc20 build tag |
make test-pkcs11 |
Test with pkcs11 build tag (requires make test-env) |
make test-zknative |
Test with zknative build tag (requires make zk-native-lib zk-native-lib-vega) |
make test-all-tags |
Test with all build tags |
make test-env |
Install test dependencies (softhsm2, opensc) |
| Command | Description |
|---|---|
make install-tools |
Install protoc, swag, and Go gRPC plugins |
make vscode |
Full VS Code dev environment setup |
make proto |
Regenerate protobuf files |
make swagger |
Regenerate Swagger documentation |
make gosec |
Run security scanner |
make staticcheck |
Run static analysis |
make vulncheck |
Run vulnerability checker |
The signing algorithm is auto-detected from the loaded key (pkg/pki/keyloader.go):
| Key Type | Curve / Size | Algorithm |
|---|---|---|
| ECDSA | P-256 | ES256 |
| ECDSA | P-384 | ES384 |
| ECDSA | P-521 | ES512 |
| RSA | < 3072 bits | RS256 |
| RSA | >= 3072 bits | RS384 |
| RSA | >= 4096 bits | RS512 |
Uses COSE algorithm identifiers (pkg/mdoc/cose.go):
| COSE ID | Algorithm |
|---|---|
| -7 | ES256 |
| -35 | ES384 |
| -36 | ES512 |
| -8 | EdDSA |
- Software keys: PEM files (PKCS#8, SEC1/EC, PKCS#1/RSA)
- Hardware keys: HSM via PKCS#11 (requires
pkcs11build tag)
ECDSA signatures use IEEE P1363 format (fixed-size R||S) per JWT RFC 7518. RSA signatures use PKCS#1 v1.5.
The issuer advertises supported algorithms via:
/.well-known/jwt-vc-issuer/.well-known/openid-configuration
Default configured algorithms: ["ES256", "ES384"]. See config.yaml under apigw.issuer_metadata.credential_signing_alg_values_supported.
GET http://<apigw-url>/swagger/doc.json
or with web browser: http://<apigw-url>/swagger/index.html