This software is a web server implementing the technical standards as specified in the swiyu Trust Infrastructure Interoperability Profile. Together with the other generic components provided, this software forms a collection of APIs allowing issuance and verification of verifiable credentials without the need of reimplementing the standards.
The generic verifier service provides in its management interface the possibility to initiate a verification process and in the oid4vp interface the necessary tools to verify a verifiable presentation.
The management interface should be only accessible from inside the organization. Whereas the oid4vp interface needs to be accessible by the wallet.
Please make sure that you did the following before starting the deployment:
- Generated the signing keys file with the didtoolbox.jar
- Generated a DID which is registered on the identifier registry
- Registered yourself on the swiyuprobeta portal
- Registered yourself on the api self-service portal
Starting with v3.0.0 we publish two image variants to GHCR so existing operators have a transition period to adopt the hardened runtime:
| Tag pattern | Base image | Entrypoint | User | Status |
|---|---|---|---|---|
ghcr.io/swiyu-admin-ch/swiyu-verifier:<tag> |
dhi.io/eclipse-temurin:21-debian13 (hardened, no shell) |
java … directly |
nonroot |
Default — recommended |
ghcr.io/swiyu-admin-ch/swiyu-verifier:<tag>-unhardened |
eclipse-temurin:21-jre-ubi9-minimal |
scripts/entrypoint.sh |
UID 1001 |
Transitional — will be removed in a later release |
- New deployments and operators who have completed the migration should use the default (unsuffixed) tag.
- Operators with pipelines that still depend on the shell-based entrypoint
(
HTTP_PROXY/HTTPS_PROXY/NO_PROXY,MY_SPRING_PROFILES,JAVA_BOOTCLASSPATH//libJCE-provider mounts) must pin to the-unhardenedsuffix while they apply the changes inmigration-guides/v2.x-to-v3.0.0.md. - The two
Dockerfiles in this repository (Dockerfile.dhifor the default,Dockerfilefor the-unhardenedvariant) are both built and Snyk-scanned on every PR.
All published images are signed with Cosign using keyless (OIDC) signing directly in the GitHub Actions build workflow. The signature is bound to the image digest and recorded in the public Sigstore transparency log. You can verify the authenticity of an image before deploying it:
cosign verify \
--certificate-identity-regexp "https://github.com/swiyu-admin-ch/swiyu-verfier/.github/workflows/docker-builder.yml@.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/swiyu-admin-ch/swiyu-issuer:<tag>A sample compose file for an entire setup of both components and a database can be found in sample.compose.yml file. Replace all placeholder <VARIABLE_NAME>. In addition to that you need to adapt the verifier metadata to your use case. Those information will be provided to the holder on a dedicated endpoint serving as metadata information of your verifier.
Please be aware that the oid4vp endpoints need to be publicly accessible and set in the environment variable
EXTERNAL_URL.
For a detailed understanding of the verification process and the data structure of verification please consult the DIF presentation exchange specification. For more information on the general verification flow consult the OpenID4VP specification
Once the components are deployed you can create your first verification. For this you first need to define a presentation definition. Based on that definition you can then create a verification request for a holder as shown in the example below. In this case we're asking for a credential called "my-custom-vc" which should at least have the attributes firstName and lastName. The following request can be performed by using the swagger endpoint on https://<EXTERNAL_URL of verifier-agent-management>/swagger-ui/index.html
To see more details and examples of the verification process please consult the documentation.
The verifier service now supports the Digital Credentials Query Language (DCQL) as specified in the OpenID for Verifiable Presentations (OID4VP) Standard 1.0. This replaces the previous DIF Presentation Exchange (PE) specification that was integrated into the "claims" request parameter.
- Creation: Business Verifier creates verification request with DCQL query
- Storage: Verifier Service stores request in database
- Holder Retrieval: Holder uses Verification URI to get request object containing DCQL query
- Presentation: Holder's wallet processes DCQL query to identify suitable credentials
- Verification: Service validates credentials against DCQL query criteria
- Status Update: Business Verifier receives status and requested data via polling or webhooks
- Required after transition period
- Uses client_metadata for encryption information
Please note that by default configuration the verifier service is set up in a way to easily gain experience with the verification process, not as a productive deployment. With the configuration options found below, it can be configured and set up for productive use.
We recommend to not expose the service directly to the web. The focus of the application lies in the functionality of the verification. Using API Gateway or Web Application Firewall can decrease the attack surface significantly.
To prevent misuse, the management endpoints should be protected either by network infrastructure (for example mTLS) or using OAuth.
flowchart LR
verint[\Verifier Business System\]
ver(Verifier Service)
vdb[(Postgres)]
wallet[Wallet]
apigw[\API Gateway\]
auth[\Authentication Server\]
verint --Internal network calls--> ver
ver --Cache verification results--> vdb
wallet --Web calls--> apigw
apigw --Filtered calls--> ver
verint --Get OAuth2.0 Token--> auth
ver --Validate OAuth2.0 Token--> auth
Please be aware that this section focus on the development of the verifier service. For the deployment of the component please consult deployment section.
For the purpose of integration testing, @Testcontainers annotation is used broadly in this repo.
Needless to say, to run Testcontainers-based tests, you would need a Docker-API compatible container runtime.
As Docker has made a few changes to its licensing in the past,
alternative container runtimes started gaining on popularity.
In general, switching the container runtime from Docker to any other (such as Podman/Podman Desktop) for Testcontainers in Java usually requires awareness of socket configuration, cleanup mechanisms, permissions, and underlying differences. So, customizing Docker host detection would be more or less all it takes to make it work.
Luckily, one of the quite popular Docker alternatives featuring pretty seamless integration is Podman Desktop.
Although the official manual suggests otherwise,
from our experience on macOS, it would be sufficient to enable the Docker Compatibility
feature and the tests would all run through. Furthermore, running mvn clean install for the first time would even implicitly create
a minimalistic $HOME/.testcontainers.properties, if not found in your home directory.
Run the following commands to start the service. This will also spin up a local postgres database from the docker compose.yml:
./mvnw -f verifier-application spring-boot:run -Dspring-boot.run.profiles=local # start spring boot java applicationAfter the start api definitions can be found here
The openapi.yaml can be updated by using the generate-doc profile.
mvn verify -P generate-doc
Currently only EC 256 keys are used.
Generate private key with:
openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
Remember to keep private keys private and safe. It should never be transmitted, etc.
On the base registry the public key is published. To generate the public key form the private key we can use
openssl ec -in private.pem -pubout -out ec_public.pem
| Variable | Description | Type | Default |
|---|---|---|---|
| POSTGRES_USER | Username to connect to the Verifier Database | string | none |
| POSTGRES_PASSWORD | Username to connect to the Verifier Database | string | none |
| POSTGRES_JDBC | JDBC Connection string to the shared DB | string | none |
| POSTGRES_DB_SCHEMA | Database Schema to be used | string | public |
| VERIFICATION_TTL_SEC | Validity period in seconds of an verification offer | int | 900 |
| DATA_CLEAR_PROCESS_INTERVAL_MS | Interval in which expired offers should be removed from cache in milliseconds. | int | 420000 |
| MONITORING_BASIC_AUTH_ENABLED | Enables basic auth protection of the /actuator/prometheus endpoint. (Default: false) | ||
| MONITORING_BASIC_AUTH_USERNAME | Sets the username for the basic auth protection of the /actuator/prometheus endpoint. | ||
| MONITORING_BASIC_AUTH_PASSWORD | Sets the password for the basic auth protection of the /actuator/prometheus endpoint. | ||
| EXTERNAL_URL | URL of this deployed instance in order to add it to the request | URL | None |
| VERIFIER_DID | DID of this service-instance to identify the requester | string (did:webvh) | none |
| DID_VERIFICATION_METHOD | The full DID with fragment as used to find the public key for sd-jwt VCs in the DID Document. eg: did:webvh:<base-registry-url>:<issuer_uuid>#<sd-jwt-public-key-fragment> |
string (did:webvh) | none |
| SIGNING_KEY | Private Key in PEM format used to sign request objects sent to the holder | string | none |
| URL_REWRITE_MAPPING | Json object for url replacements during rest client call. Key represents the original url and value the one which should be used instead (e.g. {"https://mysample1.ch":"https://somethingdiffeerent1.ch"}) | string | "{}" |
| OPENID_CLIENT_METADATA_FILE | Path to the verifier metdata file as shown in the verifier-agent-management sample | string | None |
| STATUS_LIST_CACHE_TTL_MILLI | TTL in milliseconds how long a status list result should be cached. If 0 or less will not cache status lists. Note that choosing a too long TTL will cause acceptance of VCs that have been already revoked or suspended. Choosing to not cache or having a very short TTL will cause additional latency in verification, as status list jwts must repeately be fetched. | int | 0 |
| JWK_CACHE_TTL_MILLI | TTL in milliseconds how long a public key result should be cached. Note that choosing a too long TTL may result in accepting VCs from a compromised key the issuer removed from their did doc. | int | 3600000 (1h) |
| MAX_COMPRESSED_CIPHER_TEXT_LENGTH | Maximum allowed size of a compressed ciphertext the service will process | int | 100000 |
| SIGNING_KEY_VERIFICATION_ENABLED | Enables or disables the signing-key verification health check. Set to false when using dynamic key management without a statically configured DID_VERIFICATION_METHOD. When disabled (or when DID_VERIFICATION_METHOD is empty), the health check reports UP instead of DOWN. |
bool | true |
| CALLBACK_HEALTH_ENABLED | Enables or disables the stale-callback health check. | bool | true |
| STATUS_REGISTRY_HEALTH_ENABLED | Enables or disables the status-registry accessibility health check. | bool | true |
| IDENTIFIER_REGISTRY_HEALTH_ENABLED | Enables or disables the identifier-registry DID-resolution health check. | bool | true |
| SWIYU_TRUST_REGISTRY_API_URL | Trust registry API URL (read-only, IF-007). If set, the verifier can fetch its own trust statements. Currently intended for testing purposes only. | string | none |
| SWIYU_TMS_AUTHORING_URL | Trust registry API URL (authoring, IF-014). Used for on-the-fly vqPS registration. If not set, the vqPS registration feature is disabled. Currently intended for testing purposes only. | URL | none |
| SWIYU_TMS_OAUTH_TOKEN_URL | OAuth2 token endpoint used to obtain an access token for the TMS B2B Authoring API. Required when SWIYU_TMS_AUTHORING_URL is set. |
URL | none |
| SWIYU_TMS_OAUTH_CLIENT_ID | OAuth2 client ID for authenticating against the TMS B2B Authoring API. Required when SWIYU_TMS_AUTHORING_URL is set. |
string | none |
| SWIYU_TMS_OAUTH_CLIENT_SECRET | OAuth2 client secret for authenticating against the TMS B2B Authoring API. Required when SWIYU_TMS_AUTHORING_URL is set. |
string | none |
| SWIYU_TMS_BOOTSTRAP_REFRESH_TOKEN | Initial OAuth 2.0 refresh token for the TMS API. Required when SWIYU_TMS_AUTHORING_URL is set. Rotated refresh tokens are persisted in the token_set table and shared across all pods. | string | none |
| SWIYU_TMS_TOKEN_REFRESH_INTERVAL | Interval for proactive TMS access-token refreshes (ISO 8601 duration). Must be shorter than the refresh-token lifetime. | ISO-8601 duration | PT12H |
| Variable | Description |
|---|---|
| secret.db.username | Username to connect to the Verifier Database |
| secret.db.password | Username to connect to the Verifier Database |
| secret.signing_key | Private Key used to sign the request object sent to the holder - alternative to the env variable |
| secret.swiyu.trust-registry.tms-oauth-client-secret | TMS OAuth2 client secret – alternative to SWIYU_TMS_OAUTH_CLIENT_SECRET. |
| secret.swiyu.trust-registry.tms-bootstrap-refresh-token | Initial TMS OAuth2 refresh token – alternative to SWIYU_TMS_BOOTSTRAP_REFRESH_TOKEN. |
| secret.swiyu.trust-registry.tms-bootstrap-refresh-token | Initial TMS OAuth2 refresh token – alternative to SWIYU_TMS_BOOTSTRAP_REFRESH_TOKEN. |
| secret.swiyu.trust-registry.tms-oauth-client-secret | OAuth2 client secret for the TMS B2B Authoring API – alternative to SWIYU_TMS_OAUTH_CLIENT_SECRET. |
| secret.swiyu.trust-registry.tms-bootstrap-refresh-token | Static OAuth2 refresh token used to bootstrap the first TMS access token – alternative to SWIYU_TMS_BOOTSTRAP_REFRESH_TOKEN. |
For operations with an HSM, the keys need not be mounted directly into the environment running this application. Instead, a connection is created to the HSM via JCA. This can be with the Sun PKCS11 provider or a vendor specific option. Note that for creating the keys it is expected that the public key is provided as self-signed certificate.
For vendor specific options it is necessary to provide the library in the Java classpath. How you do this depends on the image variant you deploy (see Container image variants):
-
Default hardened image (
dhi.io-based, distroless-style, no shell) — the entrypoint invokesjavadirectly, so a classpath directory cannot be expanded at startup. Vendor JARs must be baked into a derived image and referenced explicitly via-Xbootclasspath/a:, and vendor PKCS#11 native libraries (.so) need their transitive C-runtime dependencies staged in because the hardened base image strips them. Ready-made example Dockerfiles for both common setups are inexamples/hsm/:examples/hsm/Dockerfile.sunpkcs11— JDK-bundled SunPKCS11 bridge against a vendor module (SoftHSM2, Thales Luna, nCipher, …).examples/hsm/Dockerfile.securosys— Securosys Primus JCE provider via the vendor JAR.
The pattern (multi-stage build to stage native libs into
/app/lib-native/, vendor JAR baked into/app/lib-ext/, explicit-Xbootclasspath/a:inENTRYPOINT) and the common pitfalls are documented inmigration-guides/v2.x-to-v3.0.0.md§4. -
Unhardened
-unhardenedimage (transitional) — the shell entrypoint still expands${JAVA_BOOTCLASSPATH}(default./lib) into-Xbootclasspath/a:…, so mounting a volume that contains the vendor JARs at/app/libkeeps working as before.
| Variable | Description |
|---|---|
| SIGNING_KEY_MANAGEMENT_METHOD | This variable serves as selector. key is used for a mounted key. pkcs11 for the sun pkcs11 selector. For vendor specific libraries the project must be compiled with these configured. |
| HSM_HOST | URI of the HSM Host or Proxy to be connected to |
| HSM_PORT | |
| HSM_USER | User for logging in on the host |
| HSM_PASSWORD | Password for logging in to the HSM |
| HSM_PROXY_USER | |
| HSM_PROXY_PASSWORD | |
| HSM_USER_PIN | For some proprietary providers required pin |
| HSM_KEY_ID | Key identifier or alias, or label when using pkcs11-tool |
| HSM_KEY_PIN | Optional pin to unlock the key |
| HSM_CONFIG_PATH | File Path to the HSM config file when using Sun PKCS11 provider |
| HSM_USER_PIN | PIN for getting keys from the HSM |
For verifiers, it can be useful to receive a webhook callback from this service instead of performing active polling to check if a verification has been done. It is possible to configure a Webhook Callback endpoint, optionally secured by API Key. Please note that delivery of callback events will be retried until successful, to guarantee an at-least-once delivery. Failed deliveries will create error logs and be retried in the next interval.
| Variable | Description |
|---|---|
| WEBHOOK_CALLBACK_URI | Full URI of the REST endpoint where webhooks shall be sent to. No Callback events will be created if not set. |
| WEBHOOK_API_KEY_HEADER | (Optional) API key header, if the callback uri has a api key for protection. Will be used as HTTP header key. |
| WEBHOOK_API_KEY_VALUE | (Optional, Required if WEBHOOK_API_KEY_HEADER is set) The API key used. |
| WEBHOOK_INTERVAL | How often the collected events are sent. Value interpreted as milliseconds if given a plain integer or an ISO 8601 duration format. |
Callbacks will be sent on change of verification state. This means the verification can be fetched by the business verifier.
Callback Object Structure
| Field | Description |
|---|---|
| verification_id | ID of the element the callback is about. For now the management id of the verification |
| timestamp | timestamp the event occurred. Can differ from the time it is sent. |
The management endpoints for both the issuer/verifier (generic component) might seem like they're unprotected and that there is a lack of controls securing them. This is because they are meant to be used exclusively by the business issuer/verifier (business component) that are built on top of them by each participant in the ecosystem. The generic component should be considered closer to a library than to stand-alone services. As such these endpoints are meant to be deployed in a way where they can only be accessed by the business component of the software. The threat model therefore excludes attackers being able to send crafted payloads to these management endpoints. If attackers can send anything to these endpoints, they must have completely taken over the business component and can already do everything.
Management Endpoints can be secured as OAuth2 Resource Server using Spring Security, if required. The generic component leaves user management to the business component.
For more details see the official spring security documentation.
For easy playground setup or when using the component in an isolated zone security starts deactivated. It is activated when the appropriate environment variables are set.
| Variable | Description | Type |
|---|---|---|
| SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_PUBLICKEYLOCATION | URI path to a single public key in pem format. See Details | URI eg: file:/app/public-key.pem |
| Variable | Description | Type |
|---|---|---|
| SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI | URI to the issuer including path component. Will be resolved to /.well-known/openid-configuration to fetch the public key See Details | URI / String |
| SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWKSETURI | URI directly to fetch directly the jwk-set instead of fetching the openid connect first. | URI / String |
| SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWSALGORITHMS | List of algorithms supported for the key of the jkw-set. Defaults to only RS256. | String |
Other properties as defined by spring can be used.
Multitenancy is not supported.
To add additional CA or TLS certificates to the application image, place PEM encoded files into the project certs
directory (path: ./certs) and rebuild the image. Certificates must end with crt. Do not store private keys
in certs.
Steps:
- Copy one or more
.crtfiles to./certs(e.g.my-ca.crt). - Rebuild the Docker image so the files are included.
- Certificates are imported into the truststore during image build.
To perform a verification, it is required to first create the request. This is done with
the POST /management/verifications
endpoint.
What data is requested can be selected by adding in additional fields only containing "path".
Filters are currently only supported for $.vct - the Verifiable Credential Type.
In the following example we request to have the dateOfBirth revealed to us from a Credential with the type "test-sdjwt".
If SWIYU_TMS_AUTHORING_URL is configured, you can optionally provide a verification_purpose object in the request body. The verifier will then automatically register (or reuse) a Verification Query Public Statement (vqPS) with the TMS and inject it into the signed Authorization Request sent to the wallet. This allows wallets to display a verified, human-readable purpose for the verification.
{
"dcql_query": { ... },
"verification_purpose": {
"scope": "com.example.age_verification",
"purpose_name": {
"default": "Age Verification",
"en": "Age Verification",
"de-CH": "Altersverifikation"
},
"purpose_description": {
"default": "We verify that you are of legal age.",
"en": "We verify that you are of legal age.",
"de-CH": "Wir prüfen, ob Sie volljährig sind."
}
}
}Note: The verifier accepts both
dc+sd-jwt(current spec, SD-JWT VC Draft 06+) andvc+sd-jwt(legacy SD-JWT VC drafts ≤ 05) on the credential'stypheader.
The response of this post call contains the URI which has to be provided to the holder.
| Value | Description |
|---|---|
| credential_invalid | The credential presented during validation was deemed invalid. This is a general purpose code if none of the other codes apply. |
| jwt_expired | During the verification process an expired jwt was used. |
| jwt_premature | During the verification process a not yet valid jwt was used. |
| missing_nonce | During the verification process a nonce was missing. |
| invalid_format | The data send in the verification process used an invalid format. |
| credential_expired | The credential presented during validation was expired. |
| unsupported_format | The credential presented during validation was in an unsupported format. |
| credential_revoked | The credential presented during validation was revoked. |
| credential_suspended | The credential presented during validation was suspended. |
| credential_missing_data | The credential presented during validation does not contain the required fields. |
| unresolvable_status_list | The credential presented during validation contains a status list which cannot be reached during validation. |
| public_key_of_issuer_unresolvable | The credential presented during validation was issued by an entity that does not provide the public key at the time of verification. |
| issuer_not_accepted | The credential presented during validation was issued by an entity that is not in the list of allowed issuers. |
| malformed_credential | The credential presented during validation isn't valid according to the format specification in question |
| holder_binding_mismatch | The holder has provided invalid proof that the credential is under their control. |
| client_rejected | The holder rejected the verification request. |
| issuer_not_accepted | The issuer of the vc was not in the allow-list given in the verification request. |
| authorization_request_missing_error_param | During the verification process a required parameter (eg.: vp_token, presentation) was not provided in the request. |
| authorization_request_object_not_found | The requested verification process cannot be found. |
| verification_process_closed | The requested verification process is already closed. |
| invalid_presentation_definition | The provided credential presentation was invalid. |
| presentation_submission_constraint_violated | The presentation submission provided violated at least one constraint defined in the presentation definition |
| invalid_presentation_submission | The presentation submission couldn't be deserialized and is therefore invalid |
| invalid_scope | Requested scope value is invalid, unknown or malformed |
| invalid_request | Various issues with the request |
| invalid_client | client_metadata parameter exists, but the Wallet recognizes Client Identifier and knows metadata associated with it, Verifier's pre-registered metadata has been found based on the Client Identifier, but client_metadata parameter is also present |
| vp_formats_not_supported | The Wallet doesn't support any of the formats requested by the Verifier |
| invalid_presentation_definition_uri | Presentation Definition URI can't be reached |
| invalid_presentation_definition_reference | Presentation Definition URI can be reached, but the presentation_definition cannot be found there |
Docker images for this project follow a formalized environment-based tagging approach:
| Tag | Meaning | Description |
|---|---|---|
| dev | Development Build | Latest commit from the development branch. Automatically generated on every push to main. |
| staging | Integration Test Build | Set at the end of a sprint or after completion of a feature for integration testing. |
| rc | Release Candidate | Frozen state prior to release and penetration testing. |
| stable | Verified Production | Released after successful QA and penetration testing. |
These tags are assigned automatically or manually as part of the CI/CD workflow. This ensures that environments can reliably reference images by their lifecycle stage (e.g., swiyu-issuer-service:staging) without requiring manual version management.
The image promotion process follows these steps:
[Commit → dev]
↓ build & push :dev
[Feature completed / Sprint end]
↓ promote → :staging
[Release candidate created]
↓ promote → :rc
[QA & penetration test passed]
↓ promote → :stable
This project follows Semantic Versioning (SemVer) to make it easy to understand the impact of a software release just by looking at the version number. Our version numbers follow the format:
MAJOR.MINOR.PATCH[-rc][+BUILD]
- Incremented when we Contract the system by removing, changing, or breaking existing features
- Example: Removing a deprecated endpoint, changing response formats in a non-compatible way
- Incremented when we Extend the system with new, backward-compatible functionality
- Example: Adding a new endpoint, introducing an optional field, or extending valid inputs
- Incremented when we Maintain the system with backward-compatible security fixes on Release Branch
- Example: Security bug fixes or important performance optimizations needed on the last Release
Release Candidates (RCs) are tagged as prereleases to indicate a build that is a candidate for the next official release:
- Format: x.y.z-rc.N (e.g., 1.4.0-rc.1, 1.4.0-rc.2)
- Used for testing, validation, and final quality assurance
- Once validated, the RC suffix is dropped for the official release
- Example: 1.4.0-rc.1, 1.4.0-rc.2 → 1.4.0 (final release)
Our release process follows these principles:
Version Contract: If you upgrade within the same MAJOR version, your existing integrations will continue to work (following the Expand and Migrate Pattern)
GitHub Pre-release Tagging:
- All versions with -rc.N suffix (e.g., 2.1.0-rc.1) are published as GitHub Prereleases
- Prereleases are meant for testing, staging, and final validation
- After validation, we remove the -rc suffix and publish the official release (e.g., 2.1.0)
- Official releases are not marked as pre-releases on GitHub
The swiyu Public Beta Trust Infrastructure was deliberately released at an early stage to enable future ecosystem participants. The feature roadmap shows the current discrepancies between Public Beta and the targeted productive Trust Infrastructure. There may still be minor bugs or security vulnerabilities in the test system. These are marked as ‘KnownIssues’ in each repository.
We welcome any feedback on the code regarding both the implementation and security aspects. Please follow the guidelines for contributing found in CONTRIBUTING.md.
This project is licensed under the terms of the MIT license. See the LICENSE file for details.
