Skip to content
SUNETPublic

About

European Union identity suite for verifiable credential

Topics

Resources

Stars

14 stars

Watchers

6 watching

Forks

Latest commit

 

History

3,388 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VC

Go Reference

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.

Quick Start

# 1. Generate development PKI certificates
make pki

# 2. Start all services (MongoDB + microservices)
make start

# 3. Verify everything is running
docker compose ps

The 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

Configuration

The main configuration file is config.yaml. See docs/CONFIGURATION.md for details.

Prerequisites

  • Docker and Docker Compose
  • GNU Make

Services

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

Architecture

Credential Verification

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)        │
                                         └──────────────────────────┘

Credential Issuance — Two Paths

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)     │  │              │
                     └──────────────┘  └──────────────┘

Full System Overview

                                                                ┌────────────┐
                                                                │ 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) —

Capabilities

Verifier Dynamic Client Registration Authorization

The verifier exposes OAuth 2.0 Dynamic Client Registration at POST /register. You can protect this endpoint with an initial access token policy.

Supported modes:

  • open (default): registration is open (rate-limited)
  • static: requires a fixed bearer token loaded from local file
  • jwt: requires a signed JWT bearer token validated with configured issuer, audience, and jwks_uri

jwks_uri and issuer must be https. The key set is the trust root for every token accepted in jwt mode, so over plaintext anyone on the network path can substitute the keys and mint a token the verifier accepts. Private and loopback addresses are allowed, so an in-cluster issuer works, as long as it is reached over TLS.

A request carrying no bearer credentials is answered with 401, a bare WWW-Authenticate: Bearer challenge and an empty body (RFC 6750 section 3: no error code "or other error information", since none was presented). An Authorization header that does use the Bearer scheme but cannot be parsed — including one whose token falls outside RFC 6750's b64token syntax, such as Bearer abc def — is 400 invalid_request; a token that was checked and rejected is 401 invalid_token.

If the key set itself cannot be retrieved, the answer is 503 temporarily_unavailable rather than 401: no verdict on the token was reached, so reporting it as a bad credential would send a caller off to mint a new one and file a local outage as ordinary auth noise. "Cannot be retrieved" covers a 200 that carries nothing usable as well as a refused connection or an error status — a body that is not a JSON key set, {"keys":[]}, and a key set holding only keys this verifier cannot use all leave it with nothing to judge a token against. "Cannot use" is decided by PARSING each key with the same library the verifier loads them with, not by reading its kty and alg: an RSA entry with no n or e looks usable and is not.

allowed_signing_algs is asymmetric-only, enforced at startup. The key set behind jwks_uri is published, so configuring HS256 and serving an oct key would hand the signing secret to everyone who can fetch it; none is the same hole with no key at all. Unrecognised algorithms are refused rather than forwarded, and a fetched key set is judged the same way — an oct key, or one whose alg is symmetric, does not count towards "a key set arrived".

In static mode the token file must hold a single b64token on one line (letters, digits, and - . _ ~ + /, with optional trailing =). Anything else is refused at startup, since no Authorization header could ever carry it.

Example (config.yaml):

verifier:
  outbound:
    oidc_provider:
      dynamic_registration_auth:
        mode: "static"
        static_bearer_token_file: "/run/secrets/verifier_dcr_initial_access_token"
verifier:
  outbound:
    oidc_provider:
      dynamic_registration_auth:
        mode: "jwt"
        jwt:
          jwks_uri: "https://auth.example.com/.well-known/jwks.json"
          issuer: "https://auth.example.com"
          audience: "vc-verifier-register"
          allowed_signing_algs: ["RS256", "ES256"]
          # Omit for the 60s default; set 0 to turn the tolerance off.
          clock_skew_seconds: 60

Note: introspection is reserved for future implementation and is not enabled yet.

Docker release version

latest tracks the latest tag available and is built from branch main.

Branches

main is the stable development branch.

How to build

Build targets

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).

Native features

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-bbs because pkg/openid4vci links pkg/bbs transitively. The verifier additionally fetches and stages zk-cred-longfellow and zk-cred-vega. The Dockerfile does this inside the builder stage (Rust + cmake preinstalled), so a bare docker build needs nothing from the host. Local make build-* (and make build-verifier-zknative / make build-zkvegaverifyworker) auto-stage on first run — call make bbs-native-lib / make zk-native-lib / make zk-native-lib-vega explicitly only to force a re-fetch after bumping the ref in the Makefile.

Multi-arch releases. .github/workflows/docker-build-push.yml builds each arch on its own native runner (ubuntu-latest for amd64, ubuntu-24.04-arm for arm64) and then joins the two into a manifest list. No cross toolchain, no QEMU for the Rust/C++ compilations.

Docker

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).

Native ZK/PPID proof verification

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 standalone cmd/zkvegaverifyworker subprocess 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. See pkg/mdoc/zkvegaworker's package doc for the wire protocol and docs/ZK_VEGA_DIGESTID_WIRE_EXTENSION.md for 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-zknative

Configuration: 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).

Start, Stop & Restart

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

Testing

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)

Development Tools

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

Supported Signature Types

SD-JWT VC (dc+sd-jwt)

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

mDOC (mso_mdoc)

Uses COSE algorithm identifiers (pkg/mdoc/cose.go):

COSE ID Algorithm
-7 ES256
-35 ES384
-36 ES512
-8 EdDSA

Key Sources

  • Software keys: PEM files (PKCS#8, SEC1/EC, PKCS#1/RSA)
  • Hardware keys: HSM via PKCS#11 (requires pkcs11 build tag)

ECDSA signatures use IEEE P1363 format (fixed-size R||S) per JWT RFC 7518. RSA signatures use PKCS#1 v1.5.

Metadata

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.

Swagger

Endpoint

GET http://<apigw-url>/swagger/doc.json

or with web browser: http://<apigw-url>/swagger/index.html

About

European Union identity suite for verifiable credential

Topics

Resources

Stars

14 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages