From 98c70d1440e306fefbcf894bf88e10407a82e3a9 Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Fri, 9 Oct 2026 09:46:30 +0200 Subject: [PATCH 1/7] docs(reference): note build-time generation and PDP requirement on reference index Co-Authored-By: Claude Sonnet 5.5 --- docs/sirosid/reference/index.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sirosid/reference/index.md b/docs/sirosid/reference/index.md index 07d9fd5..202ad6a 100644 --- a/docs/sirosid/reference/index.md +++ b/docs/sirosid/reference/index.md @@ -14,8 +14,12 @@ Technical reference documentation including standards, credential types, and con ## Configuration Reference -Generated, always-current configuration reference for each SIROS ID component: +Configuration reference for each SIROS ID component. The VC reference is generated from [SUNET/vc](https://github.com/SUNET/vc) `main` at site build time, so it tracks the latest code: - [VC Configuration Reference](/sirosid/reference/vc-configuration) - [Wallet Backend Configuration Reference](/wallet/wallet-backend-configuration) - [Go-Trust Configuration Reference](/sirosid/trust/go-trust-configuration) + +:::warning A trust PDP is required for production +A Policy Decision Point (AuthZEN, e.g. [Go-Trust](/sirosid/trust/go-trust-configuration)) configured through `trust.pdp_url` is required for production use of the VC components. Without it, key resolution is limited to self-contained local DID methods (`did:key`, `did:jwk`), so resolving other `did:` methods and trust-framework lookups is unsupported, and trust evaluation runs in "allow all" mode: any resolved key is treated as trusted. Run without a PDP only for development and testing. +::: From 276f7d8aec409007993e2599b256cfe019faa74b Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Fri, 9 Oct 2026 09:50:37 +0200 Subject: [PATCH 2/7] docs(issuers): fix deploy composes, endpoints, transform->derivations, SAML/OIDC examples Co-Authored-By: Claude Sonnet 5.5 --- docs/sirosid/issuers/api-integration.md | 40 ++-- docs/sirosid/issuers/concepts.md | 96 +++++----- docs/sirosid/issuers/deployment.md | 167 ++++++++++++----- docs/sirosid/issuers/issuer.md | 233 ++++++++++++++++-------- docs/sirosid/issuers/keycloak_issuer.md | 136 +++++++------- docs/sirosid/issuers/oidc-op.md | 149 ++++++++------- docs/sirosid/issuers/saml-idp.md | 174 ++++++++++++------ 7 files changed, 614 insertions(+), 381 deletions(-) diff --git a/docs/sirosid/issuers/api-integration.md b/docs/sirosid/issuers/api-integration.md index 79e9d9f..d4a921d 100644 --- a/docs/sirosid/issuers/api-integration.md +++ b/docs/sirosid/issuers/api-integration.md @@ -426,19 +426,19 @@ apigw: ### Credential Offer Format -The deep link contains a credential offer with pre-authorized code grant: +The offer carries a pre-authorized code grant. `credential_configuration_ids` holds the credential **scope** (here `diploma`), and the offer is valid for 5 minutes. The `tx_code` object is present only when `enable_pin` is on: ```json { "credential_issuer": "https://issuer.example.org", - "credential_configuration_ids": ["urn:eudi:diploma:1"], + "credential_configuration_ids": ["diploma"], "grants": { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": "oaKazRN8I0IbtZ...", "tx_code": { "input_mode": "numeric", "length": 6, - "description": "Enter the PIN sent to your email" + "description": "Enter the PIN provided by the issuer" } } } @@ -449,21 +449,9 @@ The deep link contains a credential offer with pre-authorized code grant: ## Revocation -Credential revocation is managed via [Token Status Lists](/sirosid/reference/token-status-lists). When a credential needs to be revoked, delete the underlying document and the issuer updates the status list entry: +Credential status is published as [Token Status Lists](/sirosid/reference/token-status-lists) by the registry service. Deleting a document through `DELETE /api/v1/datastore` removes the stored document so no further credentials can be issued from it, but it does **not** change the status of credentials that were already issued: apigw never calls the registry to update a status. -```bash -DELETE /api/v1/datastore -Content-Type: application/json -Authorization: Bearer - -{ - "authentic_source": "hr.example.org", - "scope": "diploma", - "document_id": "diploma-2025-001234" -} -``` - -Verifiers checking the Token Status List will see the credential as revoked. +To revoke an issued credential, update its status entry in the registry, either through the registry admin GUI (`registry.admin_gui`, `POST /admin/status`) or by calling the registry's `TokenStatusListUpdateStatus` gRPC method directly. Verifiers checking the Token Status List then see the credential as revoked. --- @@ -478,7 +466,11 @@ service IssuerService { rpc MakeSDJWT (MakeSDJWTRequest) returns (MakeSDJWTReply) {} rpc MakeMDoc (MakeMDocRequest) returns (MakeMDocReply) {} rpc MakeVC20 (MakeVC20Request) returns (MakeVC20Reply) {} + rpc MakeJWP (MakeJWPRequest) returns (MakeJWPReply) {} rpc JWKS (Empty) returns (JwksReply) {} + rpc SignMetadata (SignMetadataRequest) returns (SignMetadataReply) {} + rpc GetIACAs (Empty) returns (GetIACAsReply) {} + rpc Status (v1.status.StatusRequest) returns (v1.status.StatusReply) {} } ``` @@ -499,22 +491,25 @@ message MakeSDJWTRequest { ```protobuf message MakeMDocRequest { string scope = 1; // Credential scope - string doc_type = 2; // e.g., "org.iso.18013.5.1.mDL" + reserved 2; // was: string doc_type; the doctype now comes from mddl bytes document_data = 3; // JSON encoded mDL data bytes device_public_key = 4; // CBOR encoded COSE_Key string device_key_format = 5; // "cose", "jwk", or "x509" + bytes mddl = 6; // Raw MDDL (mso_mdoc) schema JSON bytes } ``` ### Connection +The generated Go stubs live under `internal/` in the [vc repository](https://github.com/SUNET/vc) and cannot be imported from other modules; generate your own client from `proto/v1-issuer.proto`. The gRPC server listens on `issuer.grpc_server.addr` (default `:8090`). + ```go import ( "google.golang.org/grpc" - pb "vc/internal/gen/issuer/apiv1_issuer" + pb "example.org/yourmodule/gen/issuer/apiv1_issuer" // generated from proto/v1-issuer.proto ) -conn, err := grpc.Dial("issuer.example.org:9090", grpc.WithTransportCredentials(creds)) +conn, err := grpc.Dial("issuer.example.org:8090", grpc.WithTransportCredentials(creds)) client := pb.NewIssuerServiceClient(conn) reply, err := client.MakeSDJWT(ctx, &pb.MakeSDJWTRequest{ @@ -604,6 +599,9 @@ apigw: enable: true issuer_url: "https://auth.example.com" audience: "vc-issuer" + # client_id and redirect_uri are required whenever oidc.enable is true + client_id: "vc-apigw" + redirect_uri: "https://issuer.example.org/ui/callback" ``` When SPOCP rules are configured, each request is evaluated as a **six-part s-expression** (all six parts are mandatory and must appear in this exact order): @@ -714,7 +712,7 @@ a string. 3. **mTLS for gRPC**: Use mutual TLS for the internal Issuer gRPC service (configurable via `grpc_server.tls`) 4. **Input Validation**: The issuer validates all document data against VCTM schemas 5. **Audit Logging**: All API operations are logged for compliance (configurable webhook destinations) -6. **Rate Limiting**: Implement rate limiting at the reverse proxy level to prevent abuse +6. **Rate Limiting**: apigw has built-in per-IP rate limiting (`apigw.rate_limit`, including `datastore_requests_per_minute`); add reverse-proxy limits on top if needed --- diff --git a/docs/sirosid/issuers/concepts.md b/docs/sirosid/issuers/concepts.md index 528b851..ba8405d 100644 --- a/docs/sirosid/issuers/concepts.md +++ b/docs/sirosid/issuers/concepts.md @@ -15,7 +15,7 @@ Compatible wallets include: - **SIROS ID Credential Manager** (based on [wwWallet](/opensource#wwwallet-project)) – used in examples throughout this documentation - **EUDI Reference Wallet** – the EU Digital Identity reference implementation - **Native mobile wallets** – iOS and Android applications with OID4VCI support -- **Third-party wallets** – any wallet implementing OID4VCI and supported formats (SD-JWT VC, mDL, JWT VC) +- **Third-party wallets** – any wallet implementing OID4VCI and supported formats (SD-JWT VC, mDL/mdoc, W3C VC 2.0) :::tip Multi-Wallet Support A single issuer deployment can issue credentials to multiple different wallet implementations. The OID4VCI protocol ensures consistent behavior across all compliant wallets. @@ -116,7 +116,8 @@ The SIROS ID Issuer supports multiple credential formats to meet different use c |--------|----------|----------|---------------------| | **SD-JWT VC** | [IETF SD-JWT VC](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/) | EU Digital Identity, general use | ✅ Yes | | **mDL/mDoc** | [ISO 18013-5](https://www.iso.org/standard/69084.html) | Mobile driving licenses | ✅ Yes | -| **JWT VC** | [W3C VC Data Model](https://www.w3.org/TR/vc-data-model/) | Legacy systems | ❌ No | +| **W3C VC 2.0** (`ldp_vc`) | [W3C VC Data Model 2.0](https://www.w3.org/TR/vc-data-model-2.0/) with Data Integrity proofs | Linked-data ecosystems | Depends on cryptosuite | +| **JWP** (blind BBS) | [IETF JSON Web Proof](https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-proof/) | Unlinkable presentations | ✅ Yes | :::tip Recommended Format **SD-JWT VC** is the recommended format for new deployments. It provides selective disclosure (users can share only necessary claims) and is the format specified by the EU Digital Identity Wallet Architecture Reference Framework (ARF). @@ -134,7 +135,7 @@ A **credential type** defines the schema and semantics of a credential. Each typ ```mermaid graph LR subgraph "Credential Type Definition (VCTM)" - VCT["VCT Identifier
urn:eudi:pid:arf-1.8:1"] + VCT["VCT Identifier
urn:eudi:pid:1"] Claims["Claim Schema
given_name, family_name, ..."] Display["Display Rules
Labels, logos, templates"] end @@ -155,9 +156,9 @@ Each credential type is defined by a **VCTM file** that specifies: ```json { - "vct": "urn:eudi:pid:arf-1.8:1", + "vct": "urn:eudi:pid:1", "name": "Person Identification Data", - "description": "EU Person Identification Data credential (ARF 1.8)", + "description": "EU Person Identification Data credential", "display": [ { "lang": "en-US", @@ -178,24 +179,23 @@ Each credential type is defined by a **VCTM file** that specifies: } ``` -#### Built-in Credential Types +#### Shipped Credential Types -SIROS ID includes pre-configured types based on EU standards: +The [vc repository `metadata/` directory](https://github.com/SUNET/vc/tree/main/metadata) ships type metadata for common types, which you reference from `common.credential_metadata`: -| Type | VCT | Description | -|------|-----|-------------| -| **PID (ARF 1.5)** | `urn:eudi:pid:arf-1.5:1` | Person Identification Data (ARF 1.5) | -| **PID (ARF 1.8)** | `urn:eudi:pid:arf-1.8:1` | Person Identification Data (ARF 1.8+) | -| **EHIC** | `urn:eudi:ehic:1` | European Health Insurance Card | -| **PDA1** | `urn:eudi:pda1:1` | Portable Document A1 | -| **Diploma** | `urn:eudi:diploma:1` | Educational credentials | -| **ELM** | `urn:eudi:elm:1` | European Learning Model | -| **Microcredential** | `urn:eudi:micro_credential:1` | Short learning achievements | -| **OpenBadge** | `urn:eudi:openbadge_complete:1` | Open Badges 3.0 | +| Type | VCT / doctype | File | +|------|---------------|------| +| **PID** | `urn:eudi:pid:1` | `vctm_pid.json` | +| **EHIC** | `urn:eudi:ehic:1` | `vctm_ehic.json` | +| **PDA1** | `urn:eudi:pda1:1` | `vctm_pda1.json` | +| **Diploma** | `urn:eudi:diploma:1` | `vctm_diploma.json` | +| **ELM** | `urn:eudi:elm:1` | `vctm_elm.json` | +| **Microcredential** | `urn:eudi:micro_credential:1` | `vctm_microcredential.json` | +| **eduID** | `urn:credential:eduid:1` | `vctm_eduid.json` | +| **PID (mdoc)** | `eu.europa.ec.eudi.pid.1` | `pid_mdoc.mdoc.json` | +| **mDL (mdoc)** | `org.iso.18013.5.1.mDL` | `mdl.mdoc.json` | -:::note ARF Version Selection -SIROS ID supports both ARF 1.5 and ARF 1.8 PID schemas. Which one a scope uses is determined by the VCTM it points at in `common.credential_metadata`. The generic VCT `urn:eudi:pid:1` is accepted for compatibility but maps to a configured ARF version. -::: +Which schema a scope uses is determined by the VCTM (or MDDL) file it points at in `common.credential_metadata`. ### Building the Claim Set @@ -208,7 +208,8 @@ configuration: from and which auth provider identifies the user. 3. `apigw.auth_providers..attribute_mapping` normalises the provider's own attribute names to canonical claim names, with optional - transforms and defaults. + defaults. Mapping is a rename-and-presence step only; value + transformations are `derivations` on the scope (see below). APIGW then validates the resulting claim set against the VCTM schema before sending it to the issuer for signing. @@ -217,7 +218,7 @@ sending it to the issuer for signing. common: credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" apigw: @@ -234,7 +235,6 @@ apigw: required: true "urn:oid:0.9.2342.19200300.100.1.3": claim: "email_address" - transform: "lowercase" data_sources: # The SAML assertion's claims ARE the credential data @@ -242,12 +242,21 @@ apigw: scopes: pid: auth_provider: saml + # Value transformations are derivations on the scope + derivations: + - lowercase: + input: email_address ``` -Supported `transform` values are `lowercase`, `uppercase`, `trim`, -`country_alpha2`, `country_alpha3` and `yyyymmdd_to_iso`. `default:` supplies a -value when the attribute is absent, and `as_array: true` wraps a scalar in a -single-element array. +`default:` supplies a value when the attribute is absent, and `as_array: true` +wraps a scalar in a single-element array. Available derivation primitives are +`age_over_thresholds`, `lowercase`, `uppercase`, `trim`, `country_alpha2`, +`country_alpha3`, `yyyymmdd_to_iso`, `swamid_highest_assurance_level` and +`random`. Derivations are supported on `datastore`, `assertion`, `external_api` +and `presentation` scopes; each takes an `input` claim name and, where +applicable, an `output` (default: in place). See the +[VC Configuration Reference](/sirosid/reference/vc-configuration) for each +primitive's parameters. :::note Removed configuration Earlier releases had a top-level `credential_constructor` section. It no longer @@ -309,7 +318,7 @@ flowchart TB | **SAML SP** | Service Provider for SAML federations | SAML 2.0 | | **OIDC RP** | Relying Party for OIDC providers | OpenID Connect | | **Session Manager** | OAuth2 session and state management | OAuth 2.0 | -| **Issuer API** | Core credential operations | gRPC + REST | +| **Issuer API** | Core credential operations (called by apigw; the REST datastore API is served by apigw) | gRPC | | **Credential Constructor** | Builds credentials from claims | Internal | | **Signing Service** | Cryptographic signing (SW or HSM) | JWS, COSE | | **Status Lists** | Revocation status tracking | Token Status List | @@ -317,24 +326,7 @@ flowchart TB ### Service Architecture -The issuer can run as a single process or as separate microservices: - -
-
- -**Single Process Mode** - -```mermaid -flowchart TB - Combined[apigw + issuer + registry] -``` - -All components in one binary—simplest deployment for development and small-scale production. - -
-
- -**Microservices Mode** +The issuer runs as three separate services (`apigw`, `issuer` and `registry`, each its own binary and container image) that talk gRPC to each other and share a MongoDB: ```mermaid flowchart TB @@ -343,13 +335,11 @@ flowchart TB Registry[Registry Service] APIGW -->|gRPC| Issuer + APIGW -->|gRPC| Registry Issuer -->|gRPC| Registry ``` -Separate services for independent scaling and high availability. - -
-
+All three can run on one host (for example with Docker Compose, see [Deployment](./deployment)) or be scaled independently for high availability. ## Deployment Models @@ -465,12 +455,12 @@ flowchart LR ### Model 4: Standalone Issuer -Minimal deployment for testing or air-gapped environments. +Minimal single-host deployment for testing or air-gapped environments: the same three services and MongoDB, run together on one machine. ```mermaid flowchart LR subgraph "Single Server" - All[Issuer + Registry
Embedded MongoDB] + All[apigw + issuer + registry
+ MongoDB] end TestWallet[Test Wallet] @@ -479,7 +469,7 @@ flowchart LR | Aspect | Details | |--------|---------| -| **Setup** | Minutes – single Docker container | +| **Setup** | Minutes – one Docker Compose file | | **Maintenance** | Minimal | | **Data location** | Local | | **Customization** | Development/testing | diff --git a/docs/sirosid/issuers/deployment.md b/docs/sirosid/issuers/deployment.md index a7ab02d..5df410b 100644 --- a/docs/sirosid/issuers/deployment.md +++ b/docs/sirosid/issuers/deployment.md @@ -13,9 +13,11 @@ The issuer deployment uses the following container images: | Image | Purpose | Required | |-------|---------|:--------:| -| `ghcr.io/sirosfoundation/vc/issuer` | Credential issuer service (includes SAML & OIDC support) | ✅ | +| `ghcr.io/sirosfoundation/vc/apigw` | Public OpenID4VCI/OAuth endpoints and user authentication (OIDC, SAML) | ✅ | +| `ghcr.io/sirosfoundation/vc/issuer` | Credential signing service (gRPC) | ✅ | +| `ghcr.io/sirosfoundation/vc/registry` | Token Status Lists | ✅ | | `mongo:7` | Database for sessions and state | ✅ | -| `ghcr.io/sirosfoundation/go-trust` | Trust evaluation (AuthZEN) | Optional | +| `ghcr.io/sirosfoundation/go-trust` | Trust evaluation (AuthZEN PDP); required for production | ✅ (production) | For complete image documentation, see [Docker Images](/sirosid/operations/docker-images). @@ -28,8 +30,8 @@ issuer/ ├── docker-compose.yaml # Container orchestration ├── config.yaml # Main issuer configuration ├── pki/ -│ ├── issuer_key.pem # Credential signing key -│ └── issuer_cert.pem # Signing certificate (if using X.509) +│ ├── signing_ec_private.pem # Credential signing key +│ └── signing_ec_chain.pem # Signing certificate chain (X.509) ├── metadata/ │ ├── vctm_pid.json # PID credential type metadata │ ├── vctm_ehic.json # EHIC credential type metadata @@ -46,31 +48,38 @@ issuer/ The main configuration file that controls all issuer behavior. -Issuance is split across two services: **apigw** terminates OpenID4VCI and -authenticates the user, **issuer** signs the credential. +Issuance is split across three services: **apigw** terminates OpenID4VCI and +authenticates the user, **issuer** signs the credential, and **registry** +serves Token Status Lists. They all read the same `config.yaml`. | Section | Purpose | |---------|---------| | `issuer.api_server` / `issuer.grpc_server` | Issuer service listeners | | `issuer.issuer_url` | Issuer identifier URL | | `issuer.key_config` | Credential signing key (file or PKCS#11) | +| `issuer.jwt_attribute` | Required issuer-side JWT settings (`issuer`, `verifiable_credential_type`) | +| `apigw.key_config` | APIGW signing key (metadata and tokens) | +| `apigw.issuer_client` / `apigw.registry_client` | gRPC addresses of the issuer and registry | | `apigw.api_server` | APIGW HTTP server settings (port, TLS) | | `apigw.public_url` | Public URL of the issuance front end | | `apigw.auth_providers` | User authentication backend (`oidc`, `saml`, `preauth`) | | `apigw.data_sources` | Binds credential scopes to auth providers and data sources | | `apigw.delivery.openid4vci` | OpenID4VCI clients and token endpoint | -| `apigw.trust` | Trust evaluation endpoint (go-trust) | +| `apigw.delivery.credential_offers` | Offer `issuer_url` (must equal `apigw.public_url`) and wallet list | +| `apigw.trust` | Trust evaluation endpoint (go-trust PDP) | +| `registry.token_status_lists` | Status list signing key | | `common.credential_metadata` | Credential type definitions (VCTM path + format) | | `common.mongo` | MongoDB connection settings | ```yaml -# Minimal config.yaml structure +# Minimal config.yaml (passes startup validation for issuer, apigw and registry) common: + production: true mongo: uri: mongodb://mongo:27017 credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" issuer: @@ -79,27 +88,80 @@ issuer: addr: :8080 grpc_server: addr: :8090 + registry_client: + addr: registry:8090 key_config: - private_key_path: "/pki/issuer_key.pem" - chain_path: "/pki/issuer_chain.pem" + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" + jwt_attribute: + issuer: "https://issuer.example.org" + verifiable_credential_type: "urn:eudi:pid:1" apigw: api_server: addr: :8080 public_url: "https://issuer.example.org" + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" issuer_client: addr: issuer:8090 + registry_client: + addr: registry:8090 auth_providers: oidc: enable: true - # ... see the OIDC Provider integration guide + issuer_url: "https://your-idp.example.com" + redirect_uri: "https://issuer.example.org/oidcrp/callback" + registration: + preconfigured: + enable: true + client_id: "issuer-client" + client_secret: "the-client-secret" + scopes: + - openid + - profile data_sources: assertion: scopes: pid: auth_provider: oidc + delivery: + openid4vci: + token_endpoint: "https://issuer.example.org/token" + clients: + "1003": + type: "public" + redirect_uri: "https://wallet.example.com" + scopes: + - "pid" + credential_offers: + issuer_url: "https://issuer.example.org" + wallets: + my_wallet: + label: "My Wallet" + redirect_uri: "https://wallet.example.com/credential-offer" + trust: + pdp_url: "http://go-trust:6001" + +registry: + api_server: + addr: :8080 + public_url: "https://registry.example.org" + grpc_server: + addr: :8090 + token_status_lists: + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" ``` +The `jwt_attribute` block is required by the issuer service, and `apigw.delivery.credential_offers.issuer_url` must be byte-identical to `apigw.public_url`. Configure the OIDC secret inline (or via the file named by `common.secret_file_path`); the config file is not environment-interpolated. + +:::warning A PDP is required for production +A PDP (AuthZEN, such as go-trust) configured through `apigw.trust.pdp_url` is required for production use. Without it, key resolution is limited to the self-contained local DID methods (`did:key`, `did:jwk`) and trust evaluation is "allow all". Omit it only for development and testing. +::: + :::note No `credential_constructor` The old `credential_constructor` section was removed from vc. Credential types are now declared in `common.credential_metadata`, and how a scope is @@ -124,7 +186,7 @@ Verifiable Credential Type Metadata files define each credential type's schema, ```json { - "vct": "urn:eudi:pid:arf-1.8:1", + "vct": "urn:eudi:pid:1", "name": "Person Identification Data", "display": [ { @@ -149,8 +211,8 @@ Example VCTM files are available in the [vc repository](https://github.com/SUNET | File | Purpose | Format | |------|---------|--------| -| `issuer_key.pem` | Credential signing private key | PEM (EC P-256 or RSA) | -| `issuer_cert.pem` | Signing certificate (optional) | PEM X.509 | +| `signing_ec_private.pem` | Credential signing private key | PEM (EC P-256 or RSA) | +| `signing_ec_chain.pem` | Signing certificate chain | PEM X.509 | | `sp-cert.pem` | SAML SP certificate | PEM X.509 | | `sp-key.pem` | SAML SP private key | PEM | @@ -158,21 +220,35 @@ Example VCTM files are available in the [vc repository](https://github.com/SUNET ```bash # EC P-256 key (recommended for SD-JWT VC) -openssl ecparam -name prime256v1 -genkey -noout -out pki/issuer_key.pem +openssl ecparam -name prime256v1 -genkey -noout -out pki/signing_ec_private.pem # RSA 2048 key (alternative) -openssl genrsa -out pki/issuer_key.pem 2048 +openssl genrsa -out pki/signing_ec_private.pem 2048 ``` ## docker-compose.yaml ```yaml services: + apigw: + image: ghcr.io/sirosfoundation/vc/apigw:latest + restart: always + ports: + - "8080:8080" # public OpenID4VCI / OAuth endpoints + volumes: + - ./config.yaml:/config.yaml:ro + - ./pki:/pki:ro + - ./metadata:/metadata:ro + environment: + - VC_CONFIG_YAML=config.yaml + depends_on: + - issuer + - registry + - mongo + issuer: image: ghcr.io/sirosfoundation/vc/issuer:latest restart: always - ports: - - "8080:8080" volumes: - ./config.yaml:/config.yaml:ro - ./pki:/pki:ro @@ -180,8 +256,31 @@ services: environment: - VC_CONFIG_YAML=config.yaml depends_on: + - registry - mongo + registry: + image: ghcr.io/sirosfoundation/vc/registry:latest + restart: always + ports: + - "8081:8080" # Token Status List HTTP endpoint + volumes: + - ./config.yaml:/config.yaml:ro + - ./pki:/pki:ro + environment: + - VC_CONFIG_YAML=config.yaml + depends_on: + - mongo + + go-trust: + image: ghcr.io/sirosfoundation/go-trust:latest + restart: always + volumes: + - ./trust-config.yaml:/config.yaml:ro + # go-trust binds 127.0.0.1 by default; listen on all interfaces so + # apigw can reach it on the compose network. + command: ["--host", "0.0.0.0", "--config", "/config.yaml"] + mongo: image: mongo:7 restart: always @@ -192,14 +291,15 @@ volumes: mongo-data: ``` +The same `config.yaml` is mounted into all three vc services; each reads only its own section (`apigw`, `issuer`, `registry`) plus `common`. The apigw is the only service that needs to be reachable by wallets; the issuer and registry gRPC ports (`8090`) only need to be reachable from the other vc services. See [Go-Trust configuration](/sirosid/trust/go-trust-configuration) for `trust-config.yaml`. + ### With SAML Support -For SAML IdP authentication, mount the SAML SP certificate and key: +For SAML IdP authentication, also mount the SAML SP certificate and key into the **apigw** service (the SAML SP runs in apigw): ```yaml services: - issuer: - image: ghcr.io/sirosfoundation/vc/issuer:latest + apigw: volumes: - ./config.yaml:/config.yaml:ro - ./pki:/pki:ro @@ -208,25 +308,6 @@ services: # ... rest of configuration ``` -### With Trust Evaluation - -For issuer trust validation, add the go-trust service: - -```yaml -services: - issuer: - # ... issuer configuration - - go-trust: - image: ghcr.io/sirosfoundation/go-trust:latest - restart: always - ports: - - "6001:6001" - volumes: - - ./trust-config.yaml:/config.yaml:ro - command: ["--config", "/config.yaml"] -``` - ## Environment Variables | Variable | Purpose | Default | @@ -250,12 +331,12 @@ manager. The file's permissions are checked at startup unless Before deploying, ensure you have: -- [ ] Generated signing keys (`pki/issuer_key.pem`) +- [ ] Generated signing keys (`pki/signing_ec_private.pem`) - [ ] Created VCTM files for each credential type (`metadata/*.json`) - [ ] Configured your IdP to allow the issuer as a client - [ ] Set up MongoDB (or have connection details for existing instance) - [ ] Configured external URL and TLS termination -- [ ] (Optional) Configured trust evaluation via go-trust +- [ ] Configured trust evaluation via a go-trust PDP (required for production) ## Next Steps diff --git a/docs/sirosid/issuers/issuer.md b/docs/sirosid/issuers/issuer.md index d99bb1b..8019d3a 100644 --- a/docs/sirosid/issuers/issuer.md +++ b/docs/sirosid/issuers/issuer.md @@ -16,7 +16,7 @@ After reading this guide, you will understand how to: ## Endpoints -The SIROS ID issuer exposes standard OID4VCI endpoints. For a self-hosted or on-premise deployment at `issuer.example.org`: +The OID4VCI and OAuth endpoints are served by the **apigw** service (the `vc/apigw` image), which fronts the issuer. The `issuer` service itself only speaks gRPC to apigw and exposes `GET /health` and `/swagger/*` over HTTP. For a self-hosted or on-premise deployment where apigw is published at `issuer.example.org`: | Endpoint | URL | |----------|-----| @@ -45,8 +45,8 @@ Each tenant has isolated configuration and its own credential types and signing | Option | Best For | Requirements | |--------|----------|-------------| | **SIROS ID Hosted** | Quick start, SaaS model | API credentials only | -| **Self-Hosted (Docker)** | On-premise, data sovereignty | Docker, MongoDB | -| **Self-Hosted (Binary)** | Custom infrastructure | Go 1.25+, MongoDB | +| **Self-Hosted (Docker)** | On-premise, data sovereignty | Docker, MongoDB (services: apigw, issuer, registry) | +| **Self-Hosted (Binary)** | Custom infrastructure | Go 1.27+, MongoDB | :::tip Recommendation Start with the hosted service for development and testing. Move to self-hosted when you need data sovereignty or custom integrations. @@ -119,11 +119,14 @@ apigw: saml: enable: true entity_id: "https://issuer.example.org/sp" - acs_endpoint: "https://issuer.example.org/saml/acs" + acs_endpoint: "https://issuer.example.org/samlsp/acs" certificate_path: "/pki/sp-cert.pem" private_key_path: "/pki/sp-key.pem" - # Use MDQ for federation metadata lookup - mdq_server: "https://mds.swamid.se/md" + # Use MDQ for federation metadata lookup. MDQ metadata must be + # signature-verified, so metadata_signing_cert_path is required + # (or allow_unsigned_metadata: true for testing only). + mdq_server: "https://mds.swamid.se/entities/" + metadata_signing_cert_path: "/pki/mdq-signing-cert.pem" attribute_mapping: "urn:oid:2.5.4.42": claim: "given_name" @@ -161,26 +164,28 @@ SIROS ID supports issuing credentials in multiple formats: |--------|-------------|---------------|----------| | **SD-JWT VC** | SD-JWT Verifiable Credential | [draft-ietf-oauth-sd-jwt-vc](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/) | EU Digital Identity, general VCs | | **mDL/mDoc** | ISO 18013-5 mobile document | [ISO/IEC 18013-5:2021](https://www.iso.org/standard/69084.html) | Mobile driving licenses | -| **JWT VC** | JWT-encoded credential | [W3C VC Data Model](https://www.w3.org/TR/vc-data-model/) | Legacy systems | +| **W3C VC 2.0 (Data Integrity)** | `ldp_vc` / `vc+ld+json` credential with a Data Integrity proof | [W3C VC Data Model 2.0](https://www.w3.org/TR/vc-data-model-2.0/) | Linked-data ecosystems | +| **JWP (blind BBS)** | `jwp` credential, enabled by `issuer.bbs` (see [Blind BBS Issuance](#blind-bbs-issuance)) | [draft-ietf-jose-json-web-proof](https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-proof/) | Unlinkable presentations | -### Built-in Credential Types +JWT-encoded W3C credentials (`jwt_vc_json`) are not issued. -The SIROS ID platform includes preconfigured schemas for common EU credential types based on the [EUDI Wallet Architecture Reference Framework](https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework): +### Credential Type Metadata Shipped with vc -| Credential | VCT | Description | -|------------|-----|-------------| -| **PID (ARF 1.5)** | `urn:eudi:pid:arf-1.5:1` | Person Identification Data (ARF 1.5) | -| **PID (ARF 1.8)** | `urn:eudi:pid:arf-1.8:1` | Person Identification Data (ARF 1.8+) | -| **EHIC** | `urn:eudi:ehic:1` | European Health Insurance Card | -| **PDA1** | `urn:eudi:pda1:1` | Portable Document A1 | -| **Diploma** | `urn:eudi:diploma:1` | Educational credentials | -| **ELM** | `urn:eudi:elm:1` | [European Learning Model](https://europa.eu/europass/elm-browser/index.html) | -| **Microcredential** | `urn:eudi:micro_credential:1` | Short learning achievements | -| **OpenBadge** | `urn:eudi:openbadge_complete:1` | Open Badges 3.0 (complete) | +The [vc repository `metadata/` directory](https://github.com/SUNET/vc/tree/main/metadata) ships type metadata files for common credential types, several based on the [EUDI Wallet Architecture Reference Framework](https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework). Point a `common.credential_metadata` entry at one of them (or at your own file): -:::note Credential Type Aliases -For backwards compatibility with some systems, the generic VCT `urn:eudi:pid:1` may be accepted and mapped to the appropriate ARF version based on configuration. -::: +| File | VCT / doctype | Description | +|------|---------------|-------------| +| `vctm_pid.json` | `urn:eudi:pid:1` | Person Identification Data (SD-JWT VC) | +| `vctm_pid_urn.json` | `urn:eudi:pid:1` | PID variant | +| `vctm_ehic.json` | `urn:eudi:ehic:1` | European Health Insurance Card | +| `vctm_pda1.json` | `urn:eudi:pda1:1` | Portable Document A1 | +| `vctm_diploma.json` | `urn:eudi:diploma:1` | Educational credentials | +| `vctm_elm.json` | `urn:eudi:elm:1` | [European Learning Model](https://europa.eu/europass/elm-browser/index.html) | +| `vctm_microcredential.json` | `urn:eudi:micro_credential:1` | Short learning achievements | +| `vctm_eduid.json` | `urn:credential:eduid:1` | eduID | +| `vctm_eduid_age_verification.json` | `urn:credential:eduid_age_verification:1` | eduID age verification | +| `pid_mdoc.mdoc.json` | `eu.europa.ec.eudi.pid.1` | PID as mdoc | +| `mdl.mdoc.json` | `org.iso.18013.5.1.mDL` | Mobile driving licence (mdoc) | ## Integration Steps @@ -190,7 +195,7 @@ Configure your IdP to allow the issuer as a client: **For OIDC IdPs:** 1. Register a new OIDC client -2. Set redirect URI to: `https://issuer.example.org/callback` +2. Set redirect URI to: `https://issuer.example.org/oidcrp/callback` 3. Enable required scopes (openid, profile, email, etc.) **For SAML IdPs:** @@ -247,15 +252,15 @@ The `auth_provider` field in each data source scope determines how the user is a - Scenarios requiring multiple credential types for identity matching (configurable via `auth_scopes`) When using `openid4vp` auth_provider, you must also configure: -- **`auth_scopes`**: List of acceptable credential types that can be presented for authentication (e.g., `["pid_1_5", "pid_1_8"]`) -- **`auth_claims`**: List of claims required from the presented credential (e.g., `["given_name", "birth_date", "family_name"]`) +- **`auth_scopes`**: A map of acceptable credential types that can be presented for authentication, keyed by `credential_metadata` scope name (e.g. `pid`). Each key must name a configured `common.credential_metadata` scope or startup fails. +- **`auth_claims`** (inside each `auth_scopes` entry): List of claims required from the presented credential (e.g., `["given_name", "birth_date", "family_name"]`). The top-level `auth_claims` is not used with `openid4vp`. ```yaml common: credential_metadata: # Credential type definitions (VCTM path + format) pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" ehic: vctm_file_path: "/metadata/vctm_ehic.json" @@ -277,8 +282,9 @@ apigw: scopes: ehic: auth_provider: openid4vp - auth_scopes: ["pid"] - auth_claims: ["given_name", "family_name", "birth_date"] + auth_scopes: + pid: + auth_claims: ["given_name", "family_name", "birth_date"] # External API: data fetched from a remote API external_api: @@ -294,8 +300,11 @@ The VCTM file defines the credential schema, including claim definitions, displa ### Step 3: Configure Trust -Point APIGW at a Go-Trust PDP; the trust frameworks themselves are configured -in Go-Trust, not in the issuer: +`apigw.trust` configures trust evaluation for credentials and keys that are +presented *to* apigw, for example when a scope uses the `openid4vp` +auth provider or wallet attestation. It does not register the issuer in any +trust framework. Point it at a Go-Trust PDP; the trust frameworks themselves are +configured in Go-Trust, not in the issuer: ```yaml apigw: @@ -303,6 +312,10 @@ apigw: pdp_url: "http://go-trust:6001" ``` +:::warning A PDP is required for production +A PDP (AuthZEN, such as [Go-Trust](../trust/)) configured through `trust.pdp_url` is required for production use. Without it, key resolution is limited to the self-contained local DID methods (`did:key`, `did:jwk`), other `did:` methods cannot be resolved, and trust evaluation runs in "allow all" mode where every resolved key is treated as trusted. That mode is for development and testing only. +::: + See [Trust Services](../trust/) for details on: - ETSI TSL registration @@ -310,7 +323,7 @@ See [Trust Services](../trust/) for details on: - X.509 certificate chains :::tip Accepting wallets without pre-registering each one -The steps above cover how the issuer establishes *its own* trust with the ecosystem. Separately, you can let the issuer accept **any wallet whose provider is trusted** — instead of maintaining a static client map — via [Wallet Attestation](../trust/wallet-attestation.md). See the [Attestation-Based Authentication how-to](../../howto/attestation-based-authentication.md) for a full worked setup. +You can let the issuer accept **any wallet whose provider is trusted** — instead of maintaining a static client map — via [Wallet Attestation](../trust/wallet-attestation.md). See the [Attestation-Based Authentication how-to](../../howto/attestation-based-authentication.md) for a full worked setup. ::: ### Step 4: Test the Integration @@ -385,17 +398,10 @@ certificate must chain to. ## Credential Offer Methods -### QR Code - -Generate a QR code containing a credential offer: - -``` -openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.example.org%2Fcredential-offer%2F -``` - -### Deep Link +### QR Code / Deep Link -For mobile apps, use a deep link: +Wallet-initiated and UI-initiated offers are served by reference. Encode the +following URI as a QR code, or use it as a deep link on mobile: ``` openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.example.org%2Fcredential-offer%2F @@ -419,12 +425,12 @@ curl -X POST https://issuer.example.org/api/v1/datastore/preauth_offer \ -d '{ "authentic_source": "hr.example.org", "scope": "diploma", "document_id": "..." }' ``` -Turn the transaction-code (PIN) requirement on with +The reply carries the offer inline (`openid-credential-offer://?credential_offer=`) rather than by reference. Turn the transaction-code (PIN) requirement on with `apigw.auth_providers.preauth.enable_pin`. ## API Reference -The issuer exposes OpenID4VCI-compliant endpoints: +apigw exposes the following OpenID4VCI and OAuth endpoints (see also `/op/par`, `/jwks`, `/.well-known/jwt-vc-issuer` and `/type-metadata/{scope}`): | Endpoint | Description | |----------|-------------| @@ -439,10 +445,7 @@ The issuer exposes OpenID4VCI-compliant endpoints: ### Swagger Documentation -Full API documentation is available at: -``` -https://issuer.example.org/swagger/index.html -``` +The issuer service serves its (currently empty) Swagger UI at `/swagger/index.html` on its own HTTP port; the REST surface that matters is apigw's, see [API Integration](./api-integration). ## Audit Logging @@ -529,7 +532,7 @@ the schema opting in is what drives it. ## Security Considerations 1. **Key Management**: The issuer signs credentials with keys managed in secure HSMs, configured via `issuer.key_config.pkcs11` (the BBS key excepted, above) -2. **Revocation**: Configure status lists for credential revocation +2. **Revocation**: Credential status is published as Token Status Lists by the registry service; see [Token Status Lists](../reference/token-status-lists) 3. **Audit Logging**: See [Audit Logging](#audit-logging) above ## Self-Hosted Deployment @@ -538,11 +541,18 @@ If you need to run the issuer in your own infrastructure, you can deploy it usin ### Docker Deployment (Recommended) -The issuer is available as a Docker image: +A working issuer deployment consists of three vc services plus MongoDB: + +| Service | Image | Role | +|---------|-------|------| +| apigw | `ghcr.io/sirosfoundation/vc/apigw` | Public OpenID4VCI/OAuth endpoints, user authentication (OIDC, SAML), datastore API | +| issuer | `ghcr.io/sirosfoundation/vc/issuer` | Credential signing (gRPC) | +| registry | `ghcr.io/sirosfoundation/vc/registry` | Token Status Lists (revocation) | ```bash -# Pull the issuer image (includes SAML, OIDC, and all credential formats) +docker pull ghcr.io/sirosfoundation/vc/apigw:latest docker pull ghcr.io/sirosfoundation/vc/issuer:latest +docker pull ghcr.io/sirosfoundation/vc/registry:latest ``` #### Docker Compose @@ -551,11 +561,25 @@ Create a `docker-compose.yaml`: ```yaml services: + apigw: + image: ghcr.io/sirosfoundation/vc/apigw:latest + restart: always + ports: + - "8080:8080" # public OpenID4VCI / OAuth endpoints + volumes: + - ./config.yaml:/config.yaml:ro + - ./pki:/pki:ro + - ./metadata:/metadata:ro + environment: + - VC_CONFIG_YAML=config.yaml + depends_on: + - issuer + - registry + - mongo + issuer: image: ghcr.io/sirosfoundation/vc/issuer:latest restart: always - ports: - - "8080:8080" volumes: - ./config.yaml:/config.yaml:ro - ./pki:/pki:ro @@ -563,49 +587,76 @@ services: environment: - VC_CONFIG_YAML=config.yaml depends_on: + - registry - mongo + registry: + image: ghcr.io/sirosfoundation/vc/registry:latest + restart: always + ports: + - "8081:8080" # Token Status List HTTP endpoint + volumes: + - ./config.yaml:/config.yaml:ro + - ./pki:/pki:ro + environment: + - VC_CONFIG_YAML=config.yaml + depends_on: + - mongo + + go-trust: + image: ghcr.io/sirosfoundation/go-trust:latest + restart: always + volumes: + - ./trust-config.yaml:/config.yaml:ro + # go-trust binds 127.0.0.1 by default; listen on all interfaces so + # apigw can reach it on the compose network. + command: ["--host", "0.0.0.0", "--config", "/config.yaml"] + mongo: image: mongo:7 restart: always volumes: - mongo-data:/data/db - ports: - - "27017:27017" volumes: mongo-data: ``` -#### Issuer Configuration +The same `config.yaml` is mounted into all three vc services; each reads only its own section (`apigw`, `issuer`, `registry`) plus `common`. The apigw is the only service that needs to be reachable by wallets; the issuer and registry gRPC ports (`8090`) only need to be reachable from the other vc services. See [Go-Trust configuration](/sirosid/trust/go-trust-configuration) for `trust-config.yaml`. + +#### Configuration Create `config.yaml`: ```yaml common: + production: true mongo: uri: mongodb://mongo:27017 - production: true - credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" issuer: - issuer_url: "https://issuer.example.com" + issuer_url: "https://issuer.example.org" api_server: addr: :8080 grpc_server: addr: :8090 + registry_client: + addr: registry:8090 key_config: private_key_path: "/pki/signing_ec_private.pem" chain_path: "/pki/signing_ec_chain.pem" + jwt_attribute: + issuer: "https://issuer.example.org" + verifiable_credential_type: "urn:eudi:pid:1" apigw: api_server: addr: :8080 - public_url: "https://issuer.example.com" + public_url: "https://issuer.example.org" key_config: private_key_path: "/pki/signing_ec_private.pem" chain_path: "/pki/signing_ec_chain.pem" @@ -617,7 +668,7 @@ apigw: oidc: enable: true issuer_url: "https://your-idp.example.com" - redirect_uri: "https://issuer.example.com/oidcrp/callback" + redirect_uri: "https://issuer.example.org/oidcrp/callback" registration: preconfigured: enable: true @@ -633,15 +684,36 @@ apigw: auth_provider: oidc delivery: openid4vci: - token_endpoint: "https://issuer.example.com/token" + token_endpoint: "https://issuer.example.org/token" clients: "1003": type: "public" redirect_uri: "https://wallet.example.com" scopes: - "pid" + credential_offers: + issuer_url: "https://issuer.example.org" + wallets: + my_wallet: + label: "My Wallet" + redirect_uri: "https://wallet.example.com/credential-offer" + trust: + pdp_url: "http://go-trust:6001" + +registry: + api_server: + addr: :8080 + public_url: "https://registry.example.org" + grpc_server: + addr: :8090 + token_status_lists: + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" ``` +The `jwt_attribute` block is required by the issuer service, and `apigw.delivery.credential_offers.issuer_url` must be byte-identical to `apigw.public_url`. Configure the OIDC secret inline (or via the file named by `common.secret_file_path`); the config file is not environment-interpolated. + #### Start the Service ```bash @@ -649,51 +721,56 @@ apigw: docker compose up -d # Check logs -docker compose logs -f issuer +docker compose logs -f apigw issuer registry -# Verify health -curl http://localhost:8080/health +# Verify the public endpoint serves issuer metadata +curl https://issuer.example.org/.well-known/openid-credential-issuer ``` ### Binary Deployment -For non-Docker environments: +For non-Docker environments, build and run each service: ```bash # Clone the repository git clone https://github.com/SUNET/vc.git cd vc -# Build the issuer -make build-issuer +# Build the services +make build-apigw build-issuer build-registry -# Run +# Run each service (in separate processes) against the same config export VC_CONFIG_YAML=config.yaml -./bin/vc_issuer +./bin/vc_registry & +./bin/vc_issuer & +./bin/vc_apigw ``` ### Kubernetes Deployment -For production Kubernetes deployments: +Deploy apigw, issuer and registry as three Deployments sharing one config +ConfigMap, with a Service for each gRPC port (`issuer:8090`, `registry:8090`). +Only apigw needs an Ingress. The example below shows the apigw Deployment; the +issuer and registry follow the same pattern with their own image: ```yaml apiVersion: apps/v1 kind: Deployment metadata: - name: issuer + name: apigw spec: replicas: 2 selector: matchLabels: - app: issuer + app: apigw template: metadata: labels: - app: issuer + app: apigw spec: containers: - - name: issuer - image: ghcr.io/sirosfoundation/vc/issuer:latest + - name: apigw + image: ghcr.io/sirosfoundation/vc/apigw:latest ports: - containerPort: 8080 env: @@ -716,10 +793,10 @@ spec: volumes: - name: config configMap: - name: issuer-config + name: vc-config - name: pki secret: - secretName: issuer-pki + secretName: vc-pki ``` ## Next Steps diff --git a/docs/sirosid/issuers/keycloak_issuer.md b/docs/sirosid/issuers/keycloak_issuer.md index f6f1c1a..a8e49c1 100644 --- a/docs/sirosid/issuers/keycloak_issuer.md +++ b/docs/sirosid/issuers/keycloak_issuer.md @@ -84,7 +84,7 @@ Configure the redirect URLs for your issuer: | Setting | Value | |---------|-------| | **Root URL** | `https://issuer.example.org` | -| **Valid redirect URIs** | `https://issuer.example.org/callback` | +| **Valid redirect URIs** | `https://issuer.example.org/oidcrp/callback` | | **Valid post logout redirect URIs** | `https://issuer.example.org` | | **Web origins** | `https://issuer.example.org` | @@ -224,7 +224,7 @@ common: uri: mongodb://mongo:27017 credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" apigw: @@ -267,30 +267,7 @@ the callback at `/oidcrp/callback`, so `redirect_uri` must be that path on your ### Docker Compose with Keycloak -```yaml -services: - issuer: - image: ghcr.io/sirosfoundation/vc/issuer:latest - restart: always - ports: - - "8080:8080" - environment: - - VC_CONFIG_YAML=config.yaml - volumes: - - ./config.yaml:/config.yaml:ro - - ./pki:/pki:ro - depends_on: - - mongo - - mongo: - image: mongo:7 - restart: always - volumes: - - mongo-data:/data/db - -volumes: - mongo-data: -``` +A working issuer needs the `apigw`, `issuer` and `registry` services plus MongoDB; the OIDC client to Keycloak lives in apigw. Use the Compose file from [Deployment](./deployment#docker-composeyaml) unchanged and put the Keycloak client credentials in `config.yaml` as shown above. The config file is not environment-interpolated, so keep the client secret inline or in the file named by `common.secret_file_path`. ## Step 5: Configure Credential Types @@ -302,7 +279,7 @@ Map Keycloak claims to specific credential types. common: credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" apigw: @@ -311,18 +288,21 @@ apigw: scopes: pid: auth_provider: oidc + # Value transformations are derivations on the scope + derivations: + - country_alpha2: + input: resident_country auth_providers: oidc: attribute_mapping: - # Required PID claims — only entries that need renaming or a - # transform; standard OIDC claim names pass through unchanged. + # Required PID claims — only entries that need renaming; + # standard OIDC claim names pass through unchanged. birthdate: claim: "birth_date" # Optional PID claims released by Keycloak under a different name country: claim: "resident_country" - transform: "country_alpha2" # A claim every credential should carry, even if the IdP omits it issuing_country: @@ -352,8 +332,15 @@ apigw: scopes: ehic: auth_provider: oidc - # Claims used to look the person up in the datastore - auth_claims: ["given_name", "family_name", "birthdate"] + # Claims used to look the person up in the datastore. Names are + # matched verbatim against the stored identity fields. + auth_claims: ["given_name", "family_name", "birth_date"] + auth_providers: + oidc: + attribute_mapping: + # Keycloak releases `birthdate`; the stored field is `birth_date` + birthdate: + claim: "birth_date" ``` The institution then pushes each person's EHIC document through the @@ -372,19 +359,14 @@ The institution then pushes each person's EHIC document through the ### Direct Testing -Test the OIDC flow manually: - -```bash -# 1. Start authorization (opens browser) -open "https://issuer.example.org/authorize?client_id=wallet&redirect_uri=https://id.siros.org/callback&scope=openid%20pid&response_type=code" - -# 2. After authentication, check the issued credential -``` +The wallet-facing authorization flow is OpenID4VCI (pushed authorization request at `/op/par`, then `/authorize`), so it cannot be exercised with a bare browser URL. Create a credential offer from `https://issuer.example.org/offers/pid` and open it in a wallet. ### Verify Claim Mapping Check that Keycloak claims appear correctly: +This check uses the password grant, which requires **Direct access grants** to be enabled on the client in Keycloak (turn it on temporarily on a test client; the production client JSON below leaves it off). + ```bash # Get a token from Keycloak directly TOKEN=$(curl -s -X POST \ @@ -415,7 +397,7 @@ apigw: saml: enable: true entity_id: "https://issuer.example.com/sp" - acs_endpoint: "https://issuer.example.com/saml/acs" + acs_endpoint: "https://issuer.example.com/samlsp/acs" certificate_path: "/pki/sp-cert.pem" private_key_path: "/pki/sp-key.pem" attribute_mapping: @@ -552,7 +534,7 @@ curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \ **Symptoms**: Credential contains incorrect or default values **Solutions**: -1. Check claim mapping JSONPath expressions +1. Check the `attribute_mapping` entries (a flat claim-name to canonical-name rename; there is no JSONPath) 2. Verify Keycloak attribute names match mapper configuration 3. Enable issuer debug logging to see raw claims 4. Test token claims directly from Keycloak @@ -579,7 +561,7 @@ Import this client configuration: "enabled": true, "clientAuthenticatorType": "client-secret", "redirectUris": [ - "https://issuer.example.com/callback" + "https://issuer.example.com/oidcrp/callback" ], "webOrigins": [ "https://issuer.example.com" @@ -599,47 +581,51 @@ Import this client configuration: } ``` -### Complete Issuer Configuration +### Complete Configuration + +A complete `config.yaml` for the apigw, issuer and registry services with Keycloak as the OIDC provider (as in [Deployment](./deployment)): ```yaml common: + production: true mongo: uri: mongodb://mongo:27017 - production: true credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" -# Signing service issuer: - issuer_url: "https://issuer.example.com" + issuer_url: "https://issuer.example.org" api_server: addr: :8080 grpc_server: addr: :8090 + registry_client: + addr: registry:8090 key_config: - private_key_path: "/pki/issuer_key.pem" - chain_path: "/pki/issuer_chain.pem" + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" + jwt_attribute: + issuer: "https://issuer.example.org" + verifiable_credential_type: "urn:eudi:pid:1" -# OpenID4VCI front end apigw: api_server: addr: :8080 - tls: - enable: false - - public_url: "https://issuer.example.com" - + public_url: "https://issuer.example.org" + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" issuer_client: addr: issuer:8090 - - # Keycloak authentication + registry_client: + addr: registry:8090 auth_providers: oidc: enable: true issuer_url: "https://keycloak.example.com/realms/myrealm" - redirect_uri: "https://issuer.example.com/oidcrp/callback" + redirect_uri: "https://issuer.example.org/oidcrp/callback" registration: preconfigured: enable: true @@ -650,16 +636,42 @@ apigw: - profile - email - credential-claims - + attribute_mapping: + birthdate: + claim: "birth_date" data_sources: assertion: scopes: pid: auth_provider: oidc - - # Trust configuration (optional) + delivery: + openid4vci: + token_endpoint: "https://issuer.example.org/token" + clients: + "1003": + type: "public" + redirect_uri: "https://wallet.example.com" + scopes: + - "pid" + credential_offers: + issuer_url: "https://issuer.example.org" + wallets: + my_wallet: + label: "My Wallet" + redirect_uri: "https://wallet.example.com/credential-offer" trust: pdp_url: "http://go-trust:6001" + +registry: + api_server: + addr: :8080 + public_url: "https://registry.example.org" + grpc_server: + addr: :8090 + token_status_lists: + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" ``` Credential validity is taken from the VCTM / issuance logic, not from a diff --git a/docs/sirosid/issuers/oidc-op.md b/docs/sirosid/issuers/oidc-op.md index 0d097f0..1666e36 100644 --- a/docs/sirosid/issuers/oidc-op.md +++ b/docs/sirosid/issuers/oidc-op.md @@ -69,7 +69,7 @@ OIDC Relying Party authentication is configured in the `apigw.auth_providers.oid common: credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" apigw: @@ -113,9 +113,15 @@ apigw: scopes: ehic: auth_provider: oidc - auth_claims: ["given_name", "family_name", "birthdate"] + # auth_claims are matched verbatim against the stored identity + # fields (given_name, family_name, birth_date, ...) + auth_claims: ["given_name", "family_name", "birth_date"] ``` +:::note Claim names for datastore lookups +For `datastore` scopes the `auth_claims` names are used as-is to look up the user's identity record, so they must be the stored field names (for example `birth_date`, not the OIDC `birthdate`). Map an OIDC claim onto the stored name with `attribute_mapping` (for example `birthdate: {claim: birth_date}`). +::: + ### Dynamic Client Registration For OIDC Providers supporting [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591): @@ -156,14 +162,14 @@ OpenID Connect defines standard claims: | OIDC Claim | Description | |------------|-------------| -| \`sub\` | Subject identifier | -| \`given_name\` | First name | -| \`family_name\` | Last name | -| \`email\` | Email address | -| \`email_verified\` | Email verification status | -| \`birthdate\` | Birth date (YYYY-MM-DD) | -| \`address\` | Address object | -| \`phone_number\` | Phone number | +| `sub` | Subject identifier | +| `given_name` | First name | +| `family_name` | Last name | +| `email` | Email address | +| `email_verified` | Email verification status | +| `birthdate` | Birth date (YYYY-MM-DD) | +| `address` | Address object | +| `phone_number` | Phone number | ### Attribute Mapping (Optional) @@ -179,8 +185,8 @@ apigw: registration: preconfigured: enable: true - client_id: "${OIDC_CLIENT_ID}" - client_secret: "${OIDC_CLIENT_SECRET}" + client_id: "your-client-id" + client_secret: "your-client-secret" scopes: - openid @@ -195,7 +201,6 @@ apigw: # sub: # claim: "subject_id" # required: true - # transform: "lowercase" ``` ### Attribute Configuration Options @@ -204,7 +209,14 @@ apigw: |--------|------|-------------| | `claim` | string | Canonical claim name to map to | | `required` | boolean | Whether the OIDC claim must be present | -| `transform` | string | Optional: `lowercase`, `uppercase`, `trim` | +| `default` | string | Value used when the claim is absent | +| `as_array` | boolean | Wrap a scalar value in a single-element array | + +Mapping only renames claims and checks presence. To transform values (lowercase, trim, country codes, ...), add `derivations` to the data source scope; see [Concepts](./concepts#building-the-claim-set). + +:::note Secrets +The config file is not environment-interpolated, so `${VAR}` placeholders are not expanded. Put client secrets inline, or keep them in the file named by `common.secret_file_path` (under `apigw.auth_providers.oidc.registration.preconfigured.client_secret`), mounted with restrictive permissions. +::: ## Provider-Specific Examples @@ -219,8 +231,8 @@ apigw: registration: preconfigured: enable: true - client_id: "${GOOGLE_CLIENT_ID}" - client_secret: "${GOOGLE_CLIENT_SECRET}" + client_id: "your-client-id" + client_secret: "your-client-secret" redirect_uri: "https://issuer.example.org/oidcrp/callback" scopes: - openid @@ -239,8 +251,8 @@ apigw: registration: preconfigured: enable: true - client_id: "${AZURE_CLIENT_ID}" - client_secret: "${AZURE_CLIENT_SECRET}" + client_id: "your-client-id" + client_secret: "your-client-secret" redirect_uri: "https://issuer.example.org/oidcrp/callback" scopes: - openid @@ -260,8 +272,8 @@ apigw: registration: preconfigured: enable: true - client_id: "${KEYCLOAK_CLIENT_ID}" - client_secret: "${KEYCLOAK_CLIENT_SECRET}" + client_id: "your-client-id" + client_secret: "your-client-secret" redirect_uri: "https://issuer.example.org/oidcrp/callback" scopes: - openid @@ -280,8 +292,8 @@ apigw: registration: preconfigured: enable: true - client_id: "${AUTH0_CLIENT_ID}" - client_secret: "${AUTH0_CLIENT_SECRET}" + client_id: "your-client-id" + client_secret: "your-client-secret" redirect_uri: "https://issuer.example.org/oidcrp/callback" scopes: - openid @@ -291,85 +303,94 @@ apigw: ## Docker Deployment -```yaml -services: - apigw: - image: ghcr.io/sirosfoundation/vc/apigw:latest - restart: always - ports: - - "8080:8080" - environment: - - OIDC_CLIENT_ID=${OIDC_CLIENT_ID} - - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET} - volumes: - - ./config.yaml:/config.yaml:ro - - ./pki:/pki:ro - depends_on: - - mongo - - issuer - - issuer: - image: ghcr.io/sirosfoundation/vc/issuer:latest - # ... - - mongo: - image: mongo:7 - volumes: - - mongo-data:/data/db - -volumes: - mongo-data: -``` +OIDC runs inside the **apigw** service, which also needs the issuer and registry services and MongoDB. See [Deployment](./deployment#docker-composeyaml) for the full Compose file. Credentials are not passed as environment variables; the only variables the vc services read are `VC_CONFIG_YAML` and `SSL_CERT_FILE`. ## Complete Configuration Example +A complete `config.yaml` for the apigw, issuer and registry services using OIDC (as in [Deployment](./deployment)); change `issuer_url` and the client credentials for your OP: + ```yaml common: + production: true mongo: uri: mongodb://mongo:27017 - credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" +issuer: + issuer_url: "https://issuer.example.org" + api_server: + addr: :8080 + grpc_server: + addr: :8090 + registry_client: + addr: registry:8090 + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" + jwt_attribute: + issuer: "https://issuer.example.org" + verifiable_credential_type: "urn:eudi:pid:1" + apigw: + api_server: + addr: :8080 public_url: "https://issuer.example.org" key_config: private_key_path: "/pki/signing_ec_private.pem" chain_path: "/pki/signing_ec_chain.pem" - + issuer_client: + addr: issuer:8090 + registry_client: + addr: registry:8090 auth_providers: oidc: enable: true - issuer_url: "https://accounts.google.com" + issuer_url: "https://your-idp.example.com" redirect_uri: "https://issuer.example.org/oidcrp/callback" registration: preconfigured: enable: true - client_id: "${OIDC_CLIENT_ID}" - client_secret: "${OIDC_CLIENT_SECRET}" + client_id: "issuer-client" + client_secret: "the-client-secret" scopes: - openid - profile - - email - session_duration: 300 - data_sources: assertion: scopes: pid: auth_provider: oidc - delivery: openid4vci: token_endpoint: "https://issuer.example.org/token" clients: "1003": type: "public" - redirect_uri: "https://dev.wallet.sunet.se" + redirect_uri: "https://wallet.example.com" scopes: - "pid" + credential_offers: + issuer_url: "https://issuer.example.org" + wallets: + my_wallet: + label: "My Wallet" + redirect_uri: "https://wallet.example.com/credential-offer" + trust: + pdp_url: "http://go-trust:6001" + +registry: + api_server: + addr: :8080 + public_url: "https://registry.example.org" + grpc_server: + addr: :8090 + token_status_lists: + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" ``` ## Troubleshooting @@ -377,7 +398,7 @@ apigw: ### Invalid Client **Solutions:** -1. Verify \`client_id\` and \`client_secret\` are correct +1. Verify `client_id` and `client_secret` are correct 2. Ensure the client is not expired or disabled at the OP 3. Check redirect URI exactly matches what's registered @@ -391,7 +412,7 @@ apigw: ### Token Signature Verification Failed **Solutions:** -1. Verify \`issuer_url\` matches the token's \`iss\` claim +1. Verify `issuer_url` matches the token's `iss` claim 2. Check JWKS endpoint is accessible from your issuer 3. Ensure server time is synchronized diff --git a/docs/sirosid/issuers/saml-idp.md b/docs/sirosid/issuers/saml-idp.md index 0fddf38..23b26c0 100644 --- a/docs/sirosid/issuers/saml-idp.md +++ b/docs/sirosid/issuers/saml-idp.md @@ -66,7 +66,7 @@ SAML authentication is configured in the `apigw.auth_providers.saml` section. Cr common: credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" diploma: vctm_file_path: "/metadata/vctm_diploma.json" @@ -82,7 +82,7 @@ apigw: entity_id: "https://issuer.example.org/sp" # Assertion Consumer Service endpoint (where IdP sends responses) - acs_endpoint: "https://issuer.example.org/saml/acs" + acs_endpoint: "https://issuer.example.org/samlsp/acs" # SP signing/encryption certificates certificate_path: "/pki/sp-cert.pem" @@ -101,7 +101,6 @@ apigw: claim: "birth_date" "urn:oid:0.9.2342.19200300.100.1.3": claim: "email_address" - transform: "lowercase" # Data sources bind credential scopes to auth providers data_sources: @@ -132,17 +131,17 @@ apigw: saml: enable: true entity_id: "https://issuer.example.org/sp" - acs_endpoint: "https://issuer.example.org/saml/acs" + acs_endpoint: "https://issuer.example.org/samlsp/acs" certificate_path: "/pki/sp-cert.pem" private_key_path: "/pki/sp-key.pem" # MDQ configuration - mdq_server: "https://mds.swamid.se/md" + mdq_server: "https://mds.swamid.se/entities/" - # Cache TTL in seconds (default: 86400) - metadata_cache_ttl: 86400 + # Cache TTL in seconds (default: 3600) + metadata_cache_ttl: 3600 - # Optional: verify metadata XML signatures (recommended for production) + # Verify metadata XML signatures (required for MDQ and URL metadata) metadata_signing_cert_path: "/pki/federation-signing.pem" ``` @@ -150,8 +149,8 @@ apigw: | Federation | MDQ Endpoint | |------------|--------------| -| **SWAMID** | \`https://mds.swamid.se/entities/\` | -| **InCommon** | \`https://mdq.incommon.org/entities/\` | +| **SWAMID** | `https://mds.swamid.se/entities/` | +| **InCommon** | `https://mdq.incommon.org/entities/` | :::note eduGAIN Metadata eduGAIN provides aggregate metadata intended for national federations. For individual entity queries, use your national federation's MDQ service. @@ -159,7 +158,7 @@ eduGAIN provides aggregate metadata intended for national federations. For indiv #### Metadata Signature Validation -When fetching IdP metadata from an MDQ service or a remote URL, you should verify that the metadata is signed by the federation operator. This prevents tampering with metadata in transit and ensures you only trust IdPs that are registered in the federation. +When fetching IdP metadata from an MDQ service or a remote URL, the issuer verifies that the metadata is signed by the federation operator. This prevents tampering with metadata in transit and ensures you only trust IdPs that are registered in the federation. Configure the `metadata_signing_cert_path` option to point to the federation's metadata signing certificate (PEM format): @@ -178,7 +177,8 @@ apigw: **Behavior:** - When `metadata_signing_cert_path` is set, the issuer validates the enveloped XML signature on every metadata document before parsing it. - Metadata without a valid signature from the configured certificate is **rejected**. -- When not set, metadata is accepted without signature verification (suitable for testing or environments where transport security is sufficient). +- MDQ and URL-based metadata **require** `metadata_signing_cert_path`: with neither it nor `allow_unsigned_metadata: true` set, apigw refuses to start. Only static metadata loaded from a local file (`metadata_path`) may be unsigned (a warning is logged). +- `allow_unsigned_metadata: true` lifts the requirement for MDQ/URL sources. It is insecure and meant for development only. :::tip Federation Signing Certificates Federation operators publish their metadata signing certificates. Common sources: @@ -188,8 +188,8 @@ Federation operators publish their metadata signing certificates. Common sources Download the certificate in PEM format and mount it into your container. ::: -:::caution Production Recommendation -Always configure `metadata_signing_cert_path` in production. Without signature verification, a compromised network path could inject malicious IdP metadata, redirecting authentication to an attacker-controlled IdP. +:::caution Never run unsigned in production +`allow_unsigned_metadata: true` exposes the issuer to a compromised network path injecting malicious IdP metadata, redirecting authentication to an attacker-controlled IdP. Use it for development only. ::: #### Option 2: Static IdP Metadata @@ -202,7 +202,7 @@ apigw: saml: enable: true entity_id: "https://issuer.example.org/sp" - acs_endpoint: "https://issuer.example.org/saml/acs" + acs_endpoint: "https://issuer.example.org/samlsp/acs" certificate_path: "/pki/sp-cert.pem" private_key_path: "/pki/sp-key.pem" @@ -251,7 +251,6 @@ apigw: claim: "family_name" "urn:oid:0.9.2342.19200300.100.1.3": claim: "email_address" - transform: "lowercase" "urn:oid:1.2.752.29.4.13": claim: "personal_administrative_number" "urn:oid:2.5.4.6": @@ -274,36 +273,58 @@ Each attribute mapping supports: |--------|------|-------------| | `claim` | string | Canonical claim name to map to | | `required` | boolean | Whether the attribute must be present | -| `transform` | string | Optional transformation: `lowercase`, `uppercase`, `trim` | +| `default` | string | Value used when the attribute is absent | +| `as_array` | boolean | Wrap a scalar value in a single-element array | -## Docker Deployment +Mapping only renames attributes and checks presence. Value transformations (lowercase, trim, country codes, ...) are `derivations` on the data source scope, for example: ```yaml -services: - apigw: - image: ghcr.io/sirosfoundation/vc/apigw:latest - restart: always - ports: - - "8080:8080" - volumes: - - ./config.yaml:/config.yaml:ro - - ./pki:/pki:ro - - ./metadata:/metadata:ro # For static IdP metadata - depends_on: - - mongo - - issuer - - issuer: - image: ghcr.io/sirosfoundation/vc/issuer:latest - # ... +apigw: + data_sources: + assertion: + scopes: + pid: + auth_provider: saml + derivations: + - lowercase: + input: email_address +``` - mongo: - image: mongo:7 - volumes: - - mongo-data:/data/db +See [Concepts](./concepts#building-the-claim-set) for the available primitives. -volumes: - mongo-data: +## Docker Deployment + +The SAML SP runs inside the **apigw** service, which also needs the issuer and registry services and MongoDB. See [Deployment](./deployment#docker-composeyaml) for the full Compose file, and mount the SP certificate, key and any static IdP metadata into apigw (`./pki:/pki:ro`, `./metadata:/metadata:ro`). + +### SP metadata and publication + +apigw serves its SAML SP metadata at `https://issuer.example.org/samlsp/metadata`. For federation acceptance (for example SWAMID) add a `metadata` block describing the organization, contacts and UI information; it is published in the SP metadata: + +```yaml +apigw: + auth_providers: + saml: + metadata: + organization: + name: "Example Org" + display_name: "Example Org" + url: "https://example.org" + lang: "en" + contact_persons: + - type: technical + given_name: "Ops" + email: "ops@example.org" + - type: administrative + email: "admin@example.org" + ui_info: + display_name: "Example Org Credential Issuer" + description: "Issues digital credentials to Example Org members" + information_url: "https://example.org/credentials" + privacy_statement_url: "https://example.org/privacy" + logo: + url: "https://example.org/logo.png" + height: 80 + width: 200 ``` ### Generate SP Certificates @@ -319,38 +340,55 @@ openssl req -x509 -newkey rsa:2048 \ ## Complete Configuration Example +A complete `config.yaml` for the apigw, issuer and registry services using SAML (as in [Deployment](./deployment)): + ```yaml common: + production: true mongo: uri: mongodb://mongo:27017 - credential_metadata: pid: - vctm_file_path: "/metadata/vctm_pid_arf_1_8.json" - format: "dc+sd-jwt" - diploma: - vctm_file_path: "/metadata/vctm_diploma.json" + vctm_file_path: "/metadata/vctm_pid.json" format: "dc+sd-jwt" +issuer: + issuer_url: "https://issuer.example.org" + api_server: + addr: :8080 + grpc_server: + addr: :8090 + registry_client: + addr: registry:8090 + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" + jwt_attribute: + issuer: "https://issuer.example.org" + verifiable_credential_type: "urn:eudi:pid:1" + apigw: + api_server: + addr: :8080 public_url: "https://issuer.example.org" key_config: private_key_path: "/pki/signing_ec_private.pem" chain_path: "/pki/signing_ec_chain.pem" - + issuer_client: + addr: issuer:8090 + registry_client: + addr: registry:8090 auth_providers: saml: enable: true entity_id: "https://issuer.example.org/sp" - acs_endpoint: "https://issuer.example.org/saml/acs" + acs_endpoint: "https://issuer.example.org/samlsp/acs" certificate_path: "/pki/sp-cert.pem" private_key_path: "/pki/sp-key.pem" # MDQ for SWAMID federation - mdq_server: "https://mds.swamid.se/md" - metadata_cache_ttl: 86400 - - # Verify metadata signatures (recommended for production) + mdq_server: "https://mds.swamid.se/entities/" + metadata_cache_ttl: 3600 metadata_signing_cert_path: "/pki/swamid-signing.pem" session_duration: 300 @@ -366,26 +404,42 @@ apigw: claim: "personal_administrative_number" "urn:oid:0.9.2342.19200300.100.1.3": claim: "email_address" - transform: "lowercase" - data_sources: assertion: scopes: pid: auth_provider: saml - diploma: - auth_provider: saml - + derivations: + - lowercase: + input: email_address delivery: openid4vci: token_endpoint: "https://issuer.example.org/token" clients: "1003": type: "public" - redirect_uri: "https://dev.wallet.sunet.se" + redirect_uri: "https://wallet.example.com" scopes: - "pid" - - "diploma" + credential_offers: + issuer_url: "https://issuer.example.org" + wallets: + my_wallet: + label: "My Wallet" + redirect_uri: "https://wallet.example.com/credential-offer" + trust: + pdp_url: "http://go-trust:6001" + +registry: + api_server: + addr: :8080 + public_url: "https://registry.example.org" + grpc_server: + addr: :8090 + token_status_lists: + key_config: + private_key_path: "/pki/signing_ec_private.pem" + chain_path: "/pki/signing_ec_chain.pem" ``` ## Troubleshooting @@ -408,7 +462,7 @@ apigw: ### MDQ Lookup Failed **Solutions:** -1. Verify MDQ server URL ends with \`/\` +1. Verify the MDQ server URL (for example `https://mds.swamid.se/entities/`); a trailing `/` is added automatically if missing 2. Check the IdP is registered in the federation 3. Verify network connectivity to MDQ service From 0f3043dae65291ca261cd14f2f5209e8e9cacf53 Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Fri, 9 Oct 2026 09:56:13 +0200 Subject: [PATCH 3/7] docs(issuers,reference): PDP wording, issuer stack deployment fixes Co-Authored-By: Claude Sonnet 5.5 --- docs/sirosid/issuers/deployment.md | 2 +- docs/sirosid/issuers/issuer.md | 2 +- docs/sirosid/reference/index.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sirosid/issuers/deployment.md b/docs/sirosid/issuers/deployment.md index 5df410b..29d8170 100644 --- a/docs/sirosid/issuers/deployment.md +++ b/docs/sirosid/issuers/deployment.md @@ -159,7 +159,7 @@ registry: The `jwt_attribute` block is required by the issuer service, and `apigw.delivery.credential_offers.issuer_url` must be byte-identical to `apigw.public_url`. Configure the OIDC secret inline (or via the file named by `common.secret_file_path`); the config file is not environment-interpolated. :::warning A PDP is required for production -A PDP (AuthZEN, such as go-trust) configured through `apigw.trust.pdp_url` is required for production use. Without it, key resolution is limited to the self-contained local DID methods (`did:key`, `did:jwk`) and trust evaluation is "allow all". Omit it only for development and testing. +A PDP (AuthZEN, such as go-trust) configured through `apigw.trust.pdp_url` is required for production use. Running without one is not supported (development and testing only). Some things may still work (local `did:key`/`did:jwk` resolution, "allow all" trust), but there are no guarantees and no support for such a setup. ::: :::note No `credential_constructor` diff --git a/docs/sirosid/issuers/issuer.md b/docs/sirosid/issuers/issuer.md index 8019d3a..33106dd 100644 --- a/docs/sirosid/issuers/issuer.md +++ b/docs/sirosid/issuers/issuer.md @@ -313,7 +313,7 @@ apigw: ``` :::warning A PDP is required for production -A PDP (AuthZEN, such as [Go-Trust](../trust/)) configured through `trust.pdp_url` is required for production use. Without it, key resolution is limited to the self-contained local DID methods (`did:key`, `did:jwk`), other `did:` methods cannot be resolved, and trust evaluation runs in "allow all" mode where every resolved key is treated as trusted. That mode is for development and testing only. +A PDP (AuthZEN, such as [Go-Trust](../trust/)) configured through `trust.pdp_url` is required for production use. Running without one is not supported (development and testing only). Some things may still work: the resolver handles `did:key` and `did:jwk` locally and trust evaluation runs in "allow all" mode, treating every resolved key as trusted, but other `did:` methods cannot be resolved, and there are no guarantees and no support for such a setup. ::: See [Trust Services](../trust/) for details on: diff --git a/docs/sirosid/reference/index.md b/docs/sirosid/reference/index.md index 202ad6a..8afeea9 100644 --- a/docs/sirosid/reference/index.md +++ b/docs/sirosid/reference/index.md @@ -21,5 +21,5 @@ Configuration reference for each SIROS ID component. The VC reference is generat - [Go-Trust Configuration Reference](/sirosid/trust/go-trust-configuration) :::warning A trust PDP is required for production -A Policy Decision Point (AuthZEN, e.g. [Go-Trust](/sirosid/trust/go-trust-configuration)) configured through `trust.pdp_url` is required for production use of the VC components. Without it, key resolution is limited to self-contained local DID methods (`did:key`, `did:jwk`), so resolving other `did:` methods and trust-framework lookups is unsupported, and trust evaluation runs in "allow all" mode: any resolved key is treated as trusted. Run without a PDP only for development and testing. +A Policy Decision Point (AuthZEN, e.g. [Go-Trust](/sirosid/trust/go-trust-configuration)) configured through `trust.pdp_url` is required for production use of the VC components. Running without one is not supported and is for development and testing only. Some things may still work without a PDP (the resolver handles self-contained `did:key` and `did:jwk` locally, and trust evaluation runs in "allow all" mode where any resolved key is treated as trusted), but other `did:` methods and trust-framework lookups cannot be resolved, and there are no guarantees and no support for such a setup. ::: From 143e96b6f900f689549ee1bdb1e631307976e54b Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Fri, 9 Oct 2026 09:56:13 +0200 Subject: [PATCH 4/7] docs(operations): admin API auth, docker image list and tags Co-Authored-By: Claude Sonnet 5.5 --- docs/sirosid/operations/admin-api.md | 59 ++++++++---- docs/sirosid/operations/docker-images.md | 115 ++++++++++------------- 2 files changed, 90 insertions(+), 84 deletions(-) diff --git a/docs/sirosid/operations/admin-api.md b/docs/sirosid/operations/admin-api.md index be93aa4..5de3ae9 100644 --- a/docs/sirosid/operations/admin-api.md +++ b/docs/sirosid/operations/admin-api.md @@ -49,6 +49,16 @@ http://localhost:8081/admin In production, this should only be accessible from your internal network. +### Authentication + +Every `/admin/*` endpoint except `GET /admin/status` requires a bearer token: + +``` +Authorization: Bearer +``` + +Requests without it get `401` (`{"error": "Authorization header required"}`). Set the token with `server.admin_token` (`WALLET_SERVER_ADMIN_TOKEN`) or read it from a file with `server.admin_token_path` (`WALLET_SERVER_ADMIN_TOKEN_PATH`). In development an unset token is auto-generated at startup; in production the server refuses to start without one. The examples below omit the header for brevity, except the `curl` workflow at the end. + ## Multi-Tenancy Model The wallet backend supports multiple tenants, each with complete isolation of: @@ -63,9 +73,8 @@ The wallet backend supports multiple tenants, each with complete isolation of: ### Tenant IDs Tenant IDs must: -- Be lowercase alphanumeric with hyphens -- Start with a letter -- Be 2-50 characters long +- Match `^[a-z0-9][a-z0-9_-]*$` (lowercase letters, digits, hyphens and underscores; start with a letter or digit) +- Be 1-63 characters long Examples: `acme-corp`, `university-demo`, `test-tenant` @@ -73,7 +82,7 @@ Examples: `acme-corp`, `university-demo`, `test-tenant` ### OpenAPI Specification -The complete OpenAPI 3.1.0 specification is available at: +The complete OpenAPI specification (version 1.1.0) is available at: - [openapi-admin.yaml](https://github.com/sirosfoundation/go-wallet-backend/blob/main/docs/openapi-admin.yaml) You can import this into tools like Swagger UI, Postman, or Insomnia for interactive exploration. @@ -333,7 +342,7 @@ Here's a complete example of setting up a new tenant with issuers and verifiers: ```bash # 1. Create the tenant -curl -X POST http://localhost:8081/admin/tenants \ +curl -X POST -H "Authorization: Bearer $WALLET_SERVER_ADMIN_TOKEN" http://localhost:8081/admin/tenants \ -H "Content-Type: application/json" \ -d '{ "id": "university-demo", @@ -343,7 +352,7 @@ curl -X POST http://localhost:8081/admin/tenants \ }' # 2. Add a credential issuer -curl -X POST http://localhost:8081/admin/tenants/university-demo/issuers \ +curl -X POST -H "Authorization: Bearer $WALLET_SERVER_ADMIN_TOKEN" http://localhost:8081/admin/tenants/university-demo/issuers \ -H "Content-Type: application/json" \ -d '{ "credential_issuer_identifier": "https://issuer.university.edu", @@ -352,7 +361,7 @@ curl -X POST http://localhost:8081/admin/tenants/university-demo/issuers \ }' # 3. Add a verifier -curl -X POST http://localhost:8081/admin/tenants/university-demo/verifiers \ +curl -X POST -H "Authorization: Bearer $WALLET_SERVER_ADMIN_TOKEN" http://localhost:8081/admin/tenants/university-demo/verifiers \ -H "Content-Type: application/json" \ -d '{ "name": "Student Portal", @@ -360,8 +369,8 @@ curl -X POST http://localhost:8081/admin/tenants/university-demo/verifiers \ }' # 4. List the configuration -curl http://localhost:8081/admin/tenants/university-demo/issuers -curl http://localhost:8081/admin/tenants/university-demo/verifiers +curl -H "Authorization: Bearer $WALLET_SERVER_ADMIN_TOKEN" http://localhost:8081/admin/tenants/university-demo/issuers +curl -H "Authorization: Bearer $WALLET_SERVER_ADMIN_TOKEN" http://localhost:8081/admin/tenants/university-demo/verifiers ``` --- @@ -369,22 +378,33 @@ curl http://localhost:8081/admin/tenants/university-demo/verifiers ## Security Considerations :::danger Important -The admin API has no authentication by default. It should **never** be exposed to the public internet. +The admin API is protected by a single shared bearer token, not per-user credentials. It should **never** be exposed to the public internet. `GET /metrics` is served on the admin port without authentication, so keep that port network-isolated. ::: **Recommended security measures:** -1. **Network isolation**: Run the admin API on an internal-only network -2. **Firewall rules**: Block external access to port 8081 -3. **VPN/Bastion**: Access admin API through VPN or bastion hosts -4. **Authentication proxy**: Add authentication middleware if needed +1. **Strong token**: Use a long random `admin_token` (or `admin_token_path`) and rotate it periodically +2. **Network isolation**: Run the admin API on an internal-only network +3. **Firewall rules**: Block external access to port 8081 +4. **VPN/Bastion**: Access admin API through VPN or bastion hosts + +### Configuration + +| Setting / variable | Default | Description | +|--------------------|---------|-------------| +| `server.admin_port` / `WALLET_SERVER_ADMIN_PORT` | `8081` | Port for the admin API; `0` disables the admin server | +| `server.admin_token` / `WALLET_SERVER_ADMIN_TOKEN` | auto-generated (dev only) | Bearer token required by `/admin/*` | +| `server.admin_token_path` / `WALLET_SERVER_ADMIN_TOKEN_PATH` | – | File containing the bearer token | + +:::caution Admin bind address +The admin server currently binds to `server.host` (default `0.0.0.0`) on the admin port. `server.admin_host` / `WALLET_SERVER_ADMIN_HOST` is parsed but has no effect, so it cannot be used to restrict the admin API to `127.0.0.1`. Restrict access with a firewall or network policy instead (setting `server.host` would also move the public API). +::: + +--- -### Environment Variables +## Additional Endpoints -| Variable | Default | Description | -|----------|---------|-------------| -| `WALLET_SERVER_ADMIN_PORT` | `8081` | Port for the admin API | -| `WALLET_SERVER_ADMIN_HOST` | `127.0.0.1` | Bind address (use `127.0.0.1` for local-only) | +The router also serves, under `/admin/tenants/{tenantId}`: `GET issuers/{issuerId}` and `GET verifiers/{verifierId}`; invites (`/invites`, `/invites/{inviteId}`: list, create, get, update, delete); wallet instances (`/instances`, `/instances/{instanceId}`, `PUT /instances/{instanceId}/status`, `DELETE /instances/{instanceId}`, and `/users/{userId}/instances`); plus `GET /admin/status` (unauthenticated health). See the OpenAPI specification for request and response schemas. The `wallet-admin` command-line tool in the same repository wraps these endpoints. --- @@ -405,6 +425,7 @@ All endpoints return consistent error responses: | `200` | Success | | `201` | Resource created | | `400` | Bad request (invalid input) | +| `401` | Missing or invalid admin bearer token | | `403` | Forbidden (e.g., cannot delete default tenant) | | `404` | Resource not found | | `409` | Conflict (e.g., duplicate tenant ID) | diff --git a/docs/sirosid/operations/docker-images.md b/docs/sirosid/operations/docker-images.md index 77d42cb..68e2230 100644 --- a/docs/sirosid/operations/docker-images.md +++ b/docs/sirosid/operations/docker-images.md @@ -23,17 +23,13 @@ The Verifiable Credentials (VC) platform provides multiple services. | Service | Image | |---------|-------| | **Verifier** | `ghcr.io/sirosfoundation/vc/verifier` | -| **Issuer** | `ghcr.io/sirosfoundation/vc/issuer` | -| **API Gateway** | `ghcr.io/sirosfoundation/vc/apigw` | -| **Registry** | `ghcr.io/sirosfoundation/vc/registry` | -| **Mock AS** | `ghcr.io/sirosfoundation/vc/mockas` | -| **UI** | `ghcr.io/sirosfoundation/vc/ui` | +| **Issuer** (credential signing, gRPC) | `ghcr.io/sirosfoundation/vc/issuer` | +| **API Gateway** (public OpenID4VCI/OAuth endpoints, user authentication) | `ghcr.io/sirosfoundation/vc/apigw` | +| **Registry** (Token Status Lists) | `ghcr.io/sirosfoundation/vc/registry` | All images include SAML 2.0 SP, OIDC RP, and all credential format support. -:::info Deprecated `-full` variants -Previous versions shipped separate `-full` images that included SAML, OIDC RP, and VC 2.0 support via Go build tags. As of the current release, **all features are included in the standard images** and the `-full` suffix is no longer needed. Existing `-full` image references will continue to work as aliases but should be updated. -::: +A credential issuer deployment needs the `issuer`, `apigw` and `registry` images together (plus MongoDB); the issuer image alone does not serve the wallet-facing endpoints. See [Issuer Deployment](/sirosid/issuers/deployment). ### Version Tags @@ -41,10 +37,14 @@ Images are tagged with multiple version identifiers: | Tag Pattern | Description | Example | |-------------|-------------|---------| -| `latest` | Latest build from main branch | `vc/verifier:latest` | -| `{major}.{minor}.{patch}` | Semantic version release | `vc/verifier:1.2.3` | +| `{major}.{minor}.{patch}` | Semantic version release | `vc/verifier:0.7.23` | +| `{major}.{minor}`, `{major}` | Moving tags for the latest patch release | `vc/verifier:0.7` | +| `latest` | Latest tagged release (not the head of `main`) | `vc/verifier:latest` | +| `` | A specific commit | `vc/verifier:3f2c1a9...` | + +SIROS-built images are additionally published with a `-sirosid.N` suffix (for example `0.7.20-sirosid.4`) and `dev-` tags; some per-architecture builds carry an `-amd64` or `-arm64` suffix. The [Container Image Catalog](/opensource/container-images) lists the tags currently published. -**Recommended for production:** Use semantic version tags (e.g., `1.2.3`) for reproducible deployments. +**Recommended for production:** Pin an exact version tag (e.g., `0.7.23` or `0.7.23-sirosid.0`) for reproducible deployments; do not track `latest`. ## Trust Service Images @@ -71,6 +71,9 @@ Backend service for the SIROS ID wallet application. | Image | Description | |-------|-------------| | `ghcr.io/sirosfoundation/go-wallet-backend` | Wallet backend service | +| `ghcr.io/sirosfoundation/go-wallet-registry` | Credential type registry role of the wallet backend binary | +| `ghcr.io/sirosfoundation/go-wallet-admin` | `wallet-admin` administration tool for the wallet backend | +| `ghcr.io/sirosfoundation/wallet-frontend` | SIROS ID wallet web frontend | **Tags:** Same tagging scheme as VC images (`latest`, `{major}.{minor}.{patch}`) @@ -78,6 +81,16 @@ Backend service for the SIROS ID wallet application. docker pull ghcr.io/sirosfoundation/go-wallet-backend:latest ``` +## Registry Tooling Images + +| Image | Description | +|-------|-------------| +| `ghcr.io/sirosfoundation/registry-cli` | [registry-cli](/sirosid/registry/registry-cli): builds and serves a credential type registry | + +## Other Images + +Further images (for example `go-spocp`, `goff`, `go-invite-op`, `go-grc`, `go-r2ps-service`, `facetec-api`) are listed in the [Container Image Catalog](/opensource/container-images). + ## Testing & Development Images ### mini-oidc @@ -133,8 +146,10 @@ echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin # Standard verifier (latest) docker pull ghcr.io/sirosfoundation/vc/verifier:latest -# Standard issuer +# Issuer stack docker pull ghcr.io/sirosfoundation/vc/issuer:latest +docker pull ghcr.io/sirosfoundation/vc/apigw:latest +docker pull ghcr.io/sirosfoundation/vc/registry:latest # Trust service docker pull ghcr.io/sirosfoundation/go-trust:latest @@ -165,7 +180,7 @@ Docker automatically selects the correct platform for your system. flowchart TD A[What are you deploying?] --> B{Service Type} B -->|Verifier| C[vc/verifier] - B -->|Issuer| D[vc/issuer] + B -->|Issuer| D[vc/apigw + vc/issuer + vc/registry] B -->|Trust| E[go-trust] B -->|Wallet Backend| F[go-wallet-backend] ``` @@ -174,16 +189,20 @@ flowchart TD All features (SAML, OIDC, SD-JWT VC, VC 2.0) are included in every image. -| Scenario | Verifier Image | Issuer Image | +| Scenario | Verifier Image | Issuer Images | |----------|---------------|--------------| -| Basic OID4VC deployment | `vc/verifier` | `vc/issuer` | -| Academic federation (eduGAIN/SAML) | `vc/verifier` | `vc/issuer` | -| Government identity (SAML) | `vc/verifier` | `vc/issuer` | -| Enterprise OIDC | `vc/verifier` | `vc/issuer` | +| Basic OID4VC deployment | `vc/verifier` | `vc/apigw`, `vc/issuer`, `vc/registry` | +| Academic federation (eduGAIN/SAML) | `vc/verifier` | `vc/apigw`, `vc/issuer`, `vc/registry` | +| Government identity (SAML) | `vc/verifier` | `vc/apigw`, `vc/issuer`, `vc/registry` | +| Enterprise OIDC | `vc/verifier` | `vc/apigw`, `vc/issuer`, `vc/registry` | ## Example Docker Compose -### Standard Deployment +### Issuer Stack + +A complete issuer deployment (apigw, issuer, registry, go-trust and MongoDB) with its `config.yaml` is described in [Issuer Deployment](/sirosid/issuers/deployment#docker-composeyaml). For SAML, mount the SP key material and IdP metadata into the `apigw` service. + +### Verifier ```yaml services: @@ -196,58 +215,20 @@ services: - ./config.yaml:/config.yaml:ro environment: - VC_CONFIG_YAML=config.yaml - - issuer: - image: ghcr.io/sirosfoundation/vc/issuer:latest - restart: always - ports: - - "8081:8080" - volumes: - - ./issuer-config.yaml:/config.yaml:ro - environment: - - VC_CONFIG_YAML=config.yaml + depends_on: + - trust + - mongo trust: image: ghcr.io/sirosfoundation/go-trust:latest restart: always ports: - "8082:6001" - - mongo: - image: mongo:7 - restart: always - volumes: - - mongo-data:/data/db - -volumes: - mongo-data: -``` - -### SAML Deployment - -```yaml -services: - verifier: - image: ghcr.io/sirosfoundation/vc/verifier:latest - restart: always - ports: - - "8080:8080" volumes: - - ./config.yaml:/config.yaml:ro - - ./saml-metadata:/saml-metadata:ro - environment: - - VC_CONFIG_YAML=config.yaml - - issuer: - image: ghcr.io/sirosfoundation/vc/issuer:latest - restart: always - ports: - - "8081:8080" - volumes: - - ./issuer-config.yaml:/config.yaml:ro - - ./idp-metadata:/idp-metadata:ro - environment: - - VC_CONFIG_YAML=config.yaml + - ./trust-config.yaml:/config.yaml:ro + # go-trust binds 127.0.0.1 by default; listen on all interfaces so the + # verifier can reach it on the compose network. + command: ["--host", "0.0.0.0", "--config", "/config.yaml"] mongo: image: mongo:7 @@ -259,11 +240,15 @@ volumes: mongo-data: ``` +:::warning A PDP is required for production +The trust service is a PDP (AuthZEN). A PDP referenced through `trust.pdp_url` is required for production use of the vc services. Running without one is not supported (development and testing only): local `did:key`/`did:jwk` resolution and "allow all" trust may work, with no guarantees and no support. +::: + ## Source Code & CI/CD | Component | Repository | Workflow | |-----------|------------|----------| -| VC Services | [sirosfoundation/vc](https://github.com/sirosfoundation/vc) | `docker-publish.yaml` | +| VC Services | [sirosfoundation/vc](https://github.com/sirosfoundation/vc) | `docker-build-push.yml` | | go-trust | [sirosfoundation/go-trust](https://github.com/sirosfoundation/go-trust) | `docker-publish.yml` | | go-wallet-backend | [sirosfoundation/go-wallet-backend](https://github.com/sirosfoundation/go-wallet-backend) | `docker-publish.yml` | | mini-oidc | [sirosfoundation/mini-oidc](https://github.com/sirosfoundation/mini-oidc) | `docker-publish.yml` | From e12e9f1671a90be08ebb3bbd87568472033e8bde Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Fri, 9 Oct 2026 09:56:13 +0200 Subject: [PATCH 5/7] docs(registry): registry-cli flags and registry roles Co-Authored-By: Claude Sonnet 5.5 --- docs/sirosid/reference/vctm-registry.md | 12 ++++++------ docs/sirosid/registry/index.md | 7 +++++-- docs/sirosid/registry/registry-cli.md | 17 +++++++++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/sirosid/reference/vctm-registry.md b/docs/sirosid/reference/vctm-registry.md index f9c442e..60b0ef5 100644 --- a/docs/sirosid/reference/vctm-registry.md +++ b/docs/sirosid/reference/vctm-registry.md @@ -15,20 +15,20 @@ The registry provides a publication platform for organizations to share their cr - **An aggregation service** that collects metadata from multiple sources :::note Important Distinction -The registry is a publication mechanism, not a resolution service. Credential type resolution (looking up metadata by VCT identifier) is implemented separately. See the [go-wallet-backend](https://github.com/sirosfoundation/go-wallet-backend) for a reference implementation that can use registry.siros.org as a backend. +The registry is a publication mechanism, not a resolution service. Credential type resolution (looking up metadata by VCT identifier or mdoc doctype) is done by the consumers: the VC platform (`common.credential_registry`, see [Resolving Credential Types from a Registry](../issuers/issuer#resolving-credential-types-from-a-registry)) and [go-wallet-backend](https://github.com/sirosfoundation/go-wallet-backend) can both use registry.siros.org as a backend. ::: :::caution Don't confuse the "registries" -The SIROS ecosystem has three components that use the word "registry" — they serve different purposes: +The SIROS ecosystem has four components that use the word "registry" — they serve different purposes: | Component | Role | Description | |-----------|------|-------------| | **[registry-cli](../registry/registry-cli)** | **Publisher** | CLI tool that builds the credential type catalogue (the static site at registry.siros.org). This is the _publishing side_. | -| **registry.siros.org** | **Catalogue** | The public static site produced by registry-cli, hosted on GitHub Pages. | -| **VC registry** (`vc/cmd/registry`) | **Token Status Lists** | A completely separate service in the [VC suite](https://github.com/SUNET/vc) that manages credential revocation via Token Status Lists. It has nothing to do with credential type metadata. | -| **go-wallet-backend registry** (`go-wallet-backend/cmd/registry`) | **Consumer/cache** | A service in go-wallet-backend that _fetches and caches_ credential type metadata from registry.siros.org (or any compatible source). This is the _consuming side_. | +| **registry.siros.org** | **Catalogue** | The public static site produced by registry-cli. | +| **VC registry** (`vc/registry`) | **Token Status Lists** | A completely separate service in the [VC suite](https://github.com/SUNET/vc) that manages credential revocation via Token Status Lists. It has nothing to do with credential type metadata. | +| **go-wallet-backend registry** (the registry role of the `go-wallet-backend` binary, image `go-wallet-registry`) | **Consumer/cache** | A service in go-wallet-backend that _fetches and caches_ credential type metadata from registry.siros.org (or any compatible source). This is the _consuming side_. | -In short: **registry-cli publishes** credential type metadata, **go-wallet-backend's registry consumes** it, and **VC's registry manages revocation** — an entirely unrelated concern. +In short: **registry-cli publishes** credential type metadata, **go-wallet-backend's registry (and the VC platform) consume** it, and **VC's registry manages revocation** — an entirely unrelated concern. ::: ## What is VCTM? diff --git a/docs/sirosid/registry/index.md b/docs/sirosid/registry/index.md index 1c286c0..f59e51c 100644 --- a/docs/sirosid/registry/index.md +++ b/docs/sirosid/registry/index.md @@ -50,6 +50,7 @@ flowchart LR Registry[registry.siros.org
Static catalogue + JSON API] subgraph Consumers + VCP[VC platform
issuer / apigw] WB[go-wallet-backend
Caches metadata for wallets] Wallet[Credential Manager
Renders credentials] Verifier[Verifier
Understands claim schemas] @@ -58,12 +59,14 @@ flowchart LR Org1 --> CLI Org2 --> CLI CLI --> Registry + Registry --> VCP Registry --> WB WB --> Wallet Registry --> Verifier ``` - **Issuers / credential designers** publish VCTM files in Git repositories. The registry discovers and aggregates them. +- **The VC platform** (apigw, issuer, verifier) can resolve credential types directly from the registry through `common.credential_registry`; see [Resolving Credential Types from a Registry](../issuers/issuer#resolving-credential-types-from-a-registry). - **go-wallet-backend** fetches and caches metadata from the registry so wallets can render credentials with the correct display names, logos, and claim labels. - **Verifiers** can look up claim schemas to understand what a presented credential contains. @@ -74,8 +77,8 @@ The SIROS ecosystem uses the word "registry" in several places: |-----------|------|-------------| | **registry-cli** | **Publisher** | CLI tool that builds the credential type catalogue. | | **registry.siros.org** | **Catalogue** | The public static site produced by registry-cli. | -| **go-wallet-backend registry** | **Consumer** | Service in go-wallet-backend that fetches and caches metadata from registry.siros.org. | -| **VC registry** (`vc/cmd/registry`) | **Token Status Lists** | A completely separate service managing credential revocation. Not related to credential type metadata. | +| **go-wallet-backend registry** | **Consumer** | The registry role of the go-wallet-backend binary (`--mode=registry`, image `go-wallet-registry`) that fetches and caches metadata from registry.siros.org. | +| **VC registry** (`vc/registry`) | **Token Status Lists** | A completely separate service managing credential revocation. Not related to credential type metadata. | ::: ## Further Reading diff --git a/docs/sirosid/registry/registry-cli.md b/docs/sirosid/registry/registry-cli.md index 29f5a42..78eee32 100644 --- a/docs/sirosid/registry/registry-cli.md +++ b/docs/sirosid/registry/registry-cli.md @@ -60,12 +60,14 @@ The registry is now available at `http://localhost:8080`. ```bash docker run -p 8080:8080 \ - -v ./sources:/data/sources:ro \ - -v ./output:/data/output \ + -v "$(pwd)/sources:/data/sources:ro" \ + -v "$(pwd)/output:/data/output" \ -e GITHUB_TOKEN \ ghcr.io/sirosfoundation/registry-cli:latest ``` +The container runs as an unprivileged user (uid 1000), so the bind-mounted `output` directory must be writable by it. The image also declares `/data/tokens` as a volume. + ## Static Site Generation To generate a static site (for hosting on GitHub Pages, Netlify, etc.) instead of running a live server: @@ -106,6 +108,8 @@ sources: - url: "git:https://github.com/org/repo.git" branch: main # override branch (default: repo default branch) organization: "My Org" # override organization display name + layout: default # repository plugin layout (default: "default") + options: {} # layout-specific string options ``` ### Subfolder path targeting {#path-targeting} @@ -156,7 +160,7 @@ Supported format names and aliases: |------|---------|-------------| | `vctm` | `sd-jwt`, `sdjwt` | `.vctm.json` | | `mddl` | `mdoc`, `mso_mdoc` | `.mdoc.json` | -| `w3c` | `jwt_vc_json` | `.vc.json` | +| `w3c` | – | `.vc.json` | | `jsonschema` | `json-schema`, `schema` | `.schema.json` | When `formats` is omitted or empty, all registered formats are generated. @@ -325,8 +329,12 @@ Sign API responses with JWS (RFC 7515). Supports ephemeral keys, SoftHSM, and ha | `--pattern` | `*.json` | Glob pattern for files to sign | | `--pkcs11-uri` | — | PKCS#11 URI (ephemeral key if omitted) | | `--key-label` | `registry-signing` | HSM key label | -| `--issuer` | `registry-cli` | JWT issuer | +| `--issuer` | — | JWT issuer; with the ephemeral signer an empty value becomes `registry-cli`, with `--pkcs11-uri` it stays empty | +| `--jku` | — | JWS Key URL (`jku` header) | | `--jwks-output` | — | Path for JWKS public key file | +| `--aggregate` | — | Path to write the aggregate JWS (for the schemas list) | +| `--previous-jwks` | — | Previous JWKS file, for key rotation | +| `--key-retention` | `720h` | How long previous signing keys stay in the JWKS after rotation | ## Custom Templates @@ -364,6 +372,7 @@ There is no static resource for an arbitrary *set* of organizations — fetch ea | Variable | Description | |----------|-------------| | `GITHUB_TOKEN` | GitHub personal access token for repository discovery and cloning | +| `PKCS11_PIN` | PKCS#11 PIN, used when none is given in the `--pkcs11-uri` | ## Installation from Source From 27f6b471739206544f9d1e5cd2d10a0cc9b1cf1e Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Fri, 9 Oct 2026 09:56:13 +0200 Subject: [PATCH 6/7] docs(reference): token status lists, standards endpoints and formats, API page Co-Authored-By: Claude Sonnet 5.5 --- docs/sirosid/reference/api.mdx | 34 +++---------- docs/sirosid/reference/standards.md | 45 ++++++++++------- docs/sirosid/reference/token-status-lists.md | 51 +++++++++++--------- 3 files changed, 63 insertions(+), 67 deletions(-) diff --git a/docs/sirosid/reference/api.mdx b/docs/sirosid/reference/api.mdx index 03d7009..d1f7e3d 100644 --- a/docs/sirosid/reference/api.mdx +++ b/docs/sirosid/reference/api.mdx @@ -28,7 +28,7 @@ The Go-Trust API is a multi-framework trust decision engine providing AuthZEN-ba Internal administration API for managing the wallet backend multi-tenant infrastructure. This API manages tenants, users, credential issuers, and verifiers. -- **Version:** 1.0.0 +- **Version:** 1.1.0 - **License:** Apache 2.0 - **Repository:** [sirosfoundation/go-wallet-backend](https://github.com/sirosfoundation/go-wallet-backend) - **Spec:** [Download OpenAPI spec](/api/wallet-backend-admin-openapi.yaml) @@ -46,7 +46,7 @@ The [VC platform](https://github.com/SUNET/vc) provides the core credential issu ### API Gateway (Datastore) {#vc-apigw} -The API Gateway handles core data operations for the VC platform. +The API Gateway (apigw) serves the OpenID4VCI/OAuth endpoints and the datastore REST API of the VC platform. The specification below covers the datastore operations only, not every apigw route. All paths in it already include the `/api/v1` prefix. - **Version:** 2.8 - **Spec:** [Download Swagger spec](/api/vc-apigw-swagger.yaml) @@ -58,32 +58,10 @@ The API Gateway handles core data operations for the VC platform. -### Registry API {#vc-registry} +### Registry (Token Status Lists) {#vc-registry} -The Registry service manages credential metadata and trust information. +The VC registry service is the Token Status List service: it serves `GET /statuslists` and `GET /statuslists/{id}`, plus an optional admin GUI under `/admin`. See [Token Status Lists](./token-status-lists). The Swagger specification published for it, [vc-registry-swagger.yaml](/api/vc-registry-swagger.yaml), currently defines no paths, so no interactive documentation is shown here. -- **Version:** 0.1.0 -- **Base path:** `/api/v1` -- **Spec:** [Download Swagger spec](/api/vc-registry-swagger.yaml) +### Issuer {#vc-issuer} -
-Expand Registry API - - - -
- -### Issuer API {#vc-issuer} - -The Issuer service handles credential issuance workflows. - -- **Version:** 0.1.0 -- **Base path:** `/issuer/api/v1` -- **Spec:** [Download Swagger spec](/api/vc-issuer-swagger.yaml) - -
-Expand Issuer API - - - -
+The VC issuer service is a gRPC service (`proto/v1-issuer.proto` in the [VC repository](https://github.com/SUNET/vc)); over HTTP it exposes only `/health` and `/swagger`. The Swagger specification published for it, [vc-issuer-swagger.yaml](/api/vc-issuer-swagger.yaml), currently defines no paths. See [API Integration](../issuers/api-integration#grpc-integration) for the gRPC interface. diff --git a/docs/sirosid/reference/standards.md b/docs/sirosid/reference/standards.md index c252078..59fc8e6 100644 --- a/docs/sirosid/reference/standards.md +++ b/docs/sirosid/reference/standards.md @@ -15,7 +15,7 @@ flowchart TB subgraph "Credential Formats" SDJWT[SD-JWT VC] MDOC[mDL/mDoc] - JWTVC[JWT VC] + VC20[W3C VC 2.0] end subgraph "Issuance Protocols" @@ -42,7 +42,7 @@ flowchart TB OID4VCI --> SDJWT OID4VCI --> MDOC - OID4VCI --> JWTVC + OID4VCI --> VC20 OID4VP --> SDJWT OID4VP --> MDOC @@ -64,24 +64,27 @@ Standards and specifications implemented by the SIROS ID **Issuer** for credenti | Attribute | Value | |-----------|-------| | **Specification** | [OpenID for Verifiable Credential Issuance 1.0](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) | -| **Status** | Draft (ID1) | -| **Component** | Issuer | +| **Status** | Final (1.0) | +| **Component** | Issuer (served by the API gateway, apigw) | The core protocol for issuing credentials to wallets. SIROS ID implements: - **Authorization Code Flow** – User authenticates via IdP, then receives credential - **Pre-Authorized Code Flow** – Server-to-server issuance without user redirect - **Credential Offer** – Deep links and QR codes for initiating issuance -- **Batch Issuance** – Multiple credentials in a single flow +- **Batch Issuance** – Multiple credentials in a single flow, by sending several proofs in the `proofs` array of one `/credential` request (limit advertised as `batch_credential_issuance.batch_size` in issuer metadata) - **Deferred Issuance** – Credentials delivered asynchronously **Endpoints:** - `/.well-known/openid-credential-issuer` – Issuer metadata -- `/credential-offer` – Initiate credential offer +- `/credential-offer/{credential_offer_uuid}` – Serves a credential offer by reference (offers are created through the `/offers/{scope}` page or `POST /api/v1/datastore/preauth_offer`) +- `/op/par` – Pushed Authorization Request (RFC 9126) +- `/authorize` – Authorization endpoint - `/token` – OAuth2 token endpoint +- `/nonce` – Nonce endpoint (`POST`) - `/credential` – Credential endpoint -- `/batch-credential` – Batch credential endpoint -- `/deferred-credential` – Deferred credential endpoint +- `/deferred_credential` – Deferred credential endpoint +- `/notification` – Credential notification endpoint ### SD-JWT VC (Selective Disclosure JWT Verifiable Credentials) @@ -111,7 +114,7 @@ Mobile driving license format, used for government-issued documents: - **CBOR Encoding** – Binary format for efficient transmission - **COSE Signatures** – CBOR Object Signing and Encryption - **Selective Disclosure** – Hardware-backed claim selection -- **Proximity Presentation** – NFC and Bluetooth LE support +- **Proximity Presentation** – NFC and Bluetooth LE are supported by the wallet SDKs, not by the vc issuer and verifier services ### VCTM (Verifiable Credential Type Metadata) @@ -139,7 +142,7 @@ Standards and specifications implemented by the SIROS ID **Verifier** for creden | Attribute | Value | |-----------|-------| | **Specification** | [OpenID for Verifiable Presentations 1.0](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) | -| **Status** | Draft (ID2) | +| **Status** | Final (1.0) | | **Component** | Verifier | Protocol for requesting and receiving credential presentations from wallets: @@ -151,26 +154,28 @@ Protocol for requesting and receiving credential presentations from wallets: **Endpoints:** - `/authorize` – Authorization endpoint (OIDC-style) -- `/direct_post` – Direct post response endpoint -- `/request_uri` – Request object endpoint +- `POST /verification/direct_post` – Direct post response endpoint (`POST /verification/oidc-direct_post` for the OIDC flow) +- `/verification/request-object` and `/verification/request-object/{session_id}` – Request object endpoints ### DCQL (Digital Credentials Query Language) | Attribute | Value | |-----------|-------| | **Specification** | [OID4VP DCQL](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l) | -| **Status** | Draft | +| **Status** | Final (part of OpenID4VP 1.0) | | **Component** | Verifier | Query language for specifying credential requirements: +An illustrative DCQL query (shown as YAML; on the wire DCQL is JSON): + ```yaml credentials: - id: pid_credential - format: vc+sd-jwt + format: dc+sd-jwt meta: vct_values: - - urn:eudi:pid:arf-1.8:1 + - urn:eudi:pid:1 claims: - path: ["given_name"] - path: ["family_name"] @@ -213,7 +218,7 @@ Browser-native API for credential presentation: |-----------|-------| | **Specification** | [draft-ietf-oauth-status-list](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/) | | **Status** | IETF Draft | -| **Component** | Issuer, Verifier | +| **Component** | Issuer, Registry, Verifier | Efficient credential revocation mechanism: @@ -445,6 +450,10 @@ Authorization interface for trust decisions: - **Standard API** – Interoperable authorization requests - **Policy-based Trust** – Configurable trust rules +### Additional implemented standards + +The platform also implements W3C Verifiable Credentials 2.0 with Data Integrity proofs (`ldp_vc`, `vc+ld+json`), pushed authorization requests (RFC 9126), OAuth 2.0 Dynamic Client Registration (RFC 7591) with an open, static or JWT-based registration policy, OpenID4VCI credential request and response encryption (JWE), JWT VC issuer metadata (`/.well-known/jwt-vc-issuer`), and SAML 2.0 and OpenID Connect authentication of users at the issuer (`/samlsp/*`, `/oidcrp/*`). + --- ## Protocol Profiles @@ -455,7 +464,9 @@ Authorization interface for trust decisions: |--------|----------|--------------|---------------------|-------------| | **SD-JWT VC** | ✅ | ✅ | ✅ | ✅ | | **mDL/mDoc** | ✅ | ✅ | ✅ | ✅ | -| **JWT VC** | ✅ | ✅ | ❌ | Optional | +| **W3C VC 2.0 (Data Integrity)** | ✅ | ✅ | ❌ | ✅ | +| **JWP (blind BBS)** | ✅ | – | ✅ | ✅ | +| **JWT VC** (`jwt_vc_json`) | ❌ | ❌ | – | – | ### Transport Profiles diff --git a/docs/sirosid/reference/token-status-lists.md b/docs/sirosid/reference/token-status-lists.md index d4c5568..7016eb7 100644 --- a/docs/sirosid/reference/token-status-lists.md +++ b/docs/sirosid/reference/token-status-lists.md @@ -14,11 +14,11 @@ Token Status Lists provide an efficient, privacy-preserving mechanism for tracki flowchart LR subgraph Issuer Issue[Issue Credential] - Revoke[Revoke Credential] end - + subgraph Registry TSL[Token Status List] + Revoke[Admin GUI / gRPC update] end subgraph Verifier @@ -45,7 +45,7 @@ Credentials include a status reference: "status": { "status_list": { "idx": 12345, - "uri": "https://registry.example.org/status/list-001" + "uri": "https://registry.example.org/statuslists/1" } } } @@ -53,21 +53,24 @@ Credentials include a status reference: ### Status List Format -Status lists are signed JWTs containing a compressed bitmap: +Status lists are signed Status List Tokens (`application/statuslist+jwt`, or `application/statuslist+cwt` when requested through the `Accept` header). The JWT payload carries a zlib-compressed, base64url-encoded status array with one byte (8 bits) per credential: ```json { "iss": "https://registry.example.org", - "sub": "https://registry.example.org/status/list-001", + "sub": "https://registry.example.org/statuslists/1", "iat": 1708963200, "exp": 1708966800, + "ttl": 43200, "status_list": { - "bits": 1, - "lst": "H4sIAAAAAAAAA..." + "bits": 8, + "lst": "eNrt..." } } ``` +The JWT header carries `typ: statuslist+jwt`, the signing `kid` and the signing key. The `uri` of a credential's `status_list` reference is `{registry.public_url}/statuslists/{section}`, where `section` is the integer number of the list section. + ## Configuration Token Status Lists are configured in the `registry.token_status_lists` section. @@ -89,7 +92,7 @@ registry: chain_path: "/pki/tsl_signing_chain.pem" # How often new status list tokens are generated (seconds) - # Default: 43200 (12 hours) + # Default: 43200 (12 hours); must be between 301 and 86400 token_refresh_interval: 43200 # Number of entries per status list section @@ -106,7 +109,7 @@ registry: | Option | Type | Default | Description | |--------|------|---------|-------------| | `key_config` | object | required | Signing key configuration | -| `token_refresh_interval` | int | 43200 | Token regeneration interval (seconds) | +| `token_refresh_interval` | int | 43200 | Token regeneration interval in seconds (valid range 301-86400; token validity is the interval minus 5 minutes) | | `section_size` | int | 1000000 | Entries per status list section | | `rate_limit_requests_per_minute` | int | 60 | Rate limit per IP | @@ -121,7 +124,7 @@ registry: pkcs11: module_path: /usr/lib/softhsm/libsofthsm2.so slot_id: 0 - pin: "${HSM_PIN}" + pin: "1234" # plaintext in the config, or supplied through the secrets file key_label: "registry-tsl-key" ``` @@ -132,7 +135,7 @@ registry: | `GET /statuslists/{list-id}` | Fetch a specific status list token (draft-ietf-oauth-status-list §8.1) | | `GET /statuslists` | Status List Aggregation — the list of all Status List Token URIs this registry hosts (draft-ietf-oauth-status-list §9.3) | -The registry has no `.well-known` metadata endpoint of its own; a verifier +The registry needs MongoDB (`common.mongo.uri`) and a `registry.public_url`. It has no `.well-known` metadata endpoint of its own; a verifier resolves its signing key the same way it resolves any other trust decision — via go-trust — rather than through SD-JWT VC issuer-metadata discovery. That discovery mechanism (`/.well-known/jwt-vc-issuer`) belongs to the credential @@ -141,16 +144,20 @@ unrelated to status list verification. ## Revocation Flow +Credential status is changed in the registry. apigw has no revoke endpoint and does not update statuses itself (deleting a document through the datastore API does not revoke credentials already issued). Two interfaces exist: + +- the **admin GUI**, enabled with `registry.admin_gui.enable` (plus `username` and `password`), which updates a status through `POST /admin/status`; +- the registry's **gRPC** `TokenStatusListUpdateStatus` method, protected by mTLS (`registry.grpc_server.tls`). + ```mermaid sequenceDiagram participant Admin - participant APIGW as API Gateway participant Registry - - Admin->>APIGW: Revoke credential - APIGW->>Registry: Update status (gRPC) + + Admin->>Registry: Update status (admin GUI or gRPC) Registry->>Registry: Mark index as revoked - Note over Registry: Next refresh generates
new status list token + Registry->>Registry: Refresh the affected section in the background + Note over Registry: New status list token
is generated immediately ``` ## Verification Flow @@ -166,7 +173,7 @@ sequenceDiagram Verifier->>Verifier: Verify signature Verifier->>Verifier: Decompress bitmap Verifier->>Verifier: Check index bit - Note over Verifier: 0 = valid, 1 = revoked + Note over Verifier: 0 = valid, 1 = revoked (one status byte per index) ``` ## Privacy Considerations @@ -176,15 +183,15 @@ Token Status Lists are designed with privacy in mind: - **No correlation**: Verifiers only see the status at an index, not credential details - **Batch fetching**: Multiple credentials can be checked with one request - **Caching**: Status lists can be cached to reduce registry load -- **Decoys**: Section size includes unallocated indices for privacy +- **Decoys**: Sections are padded with decoy entries, and a new section is started when few decoys remain, so list size does not reveal how many credentials were issued ## Caching -Verifiers should cache status lists based on the `exp` claim: +The registry does not set a `Cache-Control` header on status list responses. Verifiers should cache a status list according to the token's `ttl` claim (equal to `token_refresh_interval`) and its `exp`. -``` -Cache-Control: max-age=43200 -``` +## Separate Status Service + +SIROS also develops a separate, standalone status list service, [siros-status-service](https://github.com/sirosfoundation/siros-status-service) (a prototype implementing draft-ietf-oauth-status-list-21 with issuer authentication through a trust PDP, ingestion and verifier services, and sharding). The configuration above is for the registry that ships with the vc suite. ## Next Steps From 65d0bc30ff6d87a4232d5837e518a7b96a571fb8 Mon Sep 17 00:00:00 2001 From: Leif Johansson Date: Fri, 9 Oct 2026 09:56:46 +0200 Subject: [PATCH 7/7] docs(operations): link admin_host known issue Co-Authored-By: Claude Sonnet 5.5 --- docs/sirosid/operations/admin-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sirosid/operations/admin-api.md b/docs/sirosid/operations/admin-api.md index 5de3ae9..4fad98b 100644 --- a/docs/sirosid/operations/admin-api.md +++ b/docs/sirosid/operations/admin-api.md @@ -397,7 +397,7 @@ The admin API is protected by a single shared bearer token, not per-user credent | `server.admin_token_path` / `WALLET_SERVER_ADMIN_TOKEN_PATH` | – | File containing the bearer token | :::caution Admin bind address -The admin server currently binds to `server.host` (default `0.0.0.0`) on the admin port. `server.admin_host` / `WALLET_SERVER_ADMIN_HOST` is parsed but has no effect, so it cannot be used to restrict the admin API to `127.0.0.1`. Restrict access with a firewall or network policy instead (setting `server.host` would also move the public API). +The admin server currently binds to `server.host` (default `0.0.0.0`) on the admin port. `server.admin_host` / `WALLET_SERVER_ADMIN_HOST` is parsed but has no effect, so it cannot be used to restrict the admin API to `127.0.0.1` (known issue: [go-wallet-backend#444](https://github.com/sirosfoundation/go-wallet-backend/issues/444)). Restrict access with a firewall or network policy instead (setting `server.host` would also move the public API). ::: ---