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 link cgo native dependencies (CGO_ENABLED=1,
netgo,osusergo) for the host GOOS/GOARCH (Linux is assumed;
override BUILD_OS/BUILD_ARCH only for the pure-Go developer tools,
since local cgo builds refuse a host/target mismatch). Output goes to
./bin/. The Docker builds under dockerfiles/worker produce
dynamically linked linux/amd64 and linux/arm64 images
(distroless/cc-debian12 supplies glibc + libstdc++6).
Every service is compiled with the following native code paths always present. Which of them actually fires is decided by the deployment's runtime configuration; there is no compile-time opt-in flag.
| Feature | Native lib (staged under third_party/) |
Activated by (config) | Services that can activate it |
|---|---|---|---|
Blind BBS issuance (bbsnative) |
zk-cred-bbs β make bbs-native-lib |
issuer.bbs block |
issuer |
PKCS#11 HSM signing (pkcs11) |
pkg/pki cgo bindings (github.com/miekg/pkcs11, vendored) β dynamically loads the configured HSM module at runtime |
a pkcs11 object in a signer key config |
any worker that loads a signing key |
Native ZK/PPID (zknative) |
zk-cred-longfellow + zk-cred-vega |
zk_verifier block |
verifier (Longfellow linked in; Vega runs in a subprocess) |
Build prerequisites. Every worker's build fetches and stages
zk-cred-bbsbecausepkg/openid4vcilinkspkg/bbstransitively. The verifier additionally fetches and stageszk-cred-longfellowandzk-cred-vega. The Dockerfile does this inside the builder stage (Rust + cmake preinstalled), so a baredocker buildneeds nothing from the host. Localmake build-*(andmake build-verifier-zknative/make build-zkvegaverifyworker) auto-stage on first run β callmake bbs-native-lib/make zk-native-lib/make zk-native-lib-vegaexplicitly only to force a re-fetch after bumping the ref in the Makefile.
Multi-arch releases.
.github/workflows/docker-build-push.ymlbuilds each arch on its own native runner (ubuntu-latestfor amd64,ubuntu-24.04-armfor arm64) and then joins the two into a manifest list. No cross toolchain, no QEMU for the Rust/C++ compilations.
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-push |
Push all standard images to registry |
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 always compiled in via the zknative build
tag. The Docker runtime-verifier stage ships them; the plain runtime
stage (used by every other worker) does not. Activation at runtime is
gated by the zk_verifier config block.
- 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-bbsnative |
Test the pkg/bbs native cgo path (requires make bbs-native-lib) |
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-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