The Status List Server manages and publishes status lists for credential issuers.
It allows issuers to register, publish, and update status lists, and verifiers to retrieve and validate them securely.
This service implements the Token Status List specification.
It supports both JWT and CWT formats, with cryptographic signing using multiple algorithms (ECDSA, EdDSA, RSA with SHA-256, SHA-384, SHA-512 digest algorithms).
For a detailed explanation of the architecture, see the hexagonal architecture documentation.
| Feature | Status | Notes |
|---|---|---|
| Issuer Registration | ✅ Implemented | Public key registration via POST /api/v1/credentials |
| Status List Publishing | ✅ Implemented | JWT-signed publishing via PUT /api/v1/status-lists/{id}/statuses |
| Status List Updates | ✅ Implemented | Partial updates via PATCH /api/v1/status-lists/{id}/statuses |
| JWT Status List Format | ✅ Implemented | JWS Compact Serialization with exp/ttl |
| CWT Status List Format | ✅ Implemented | COSE_Sign1_Tagged (tag 18) with exp/ttl |
| Gzip Compression | ✅ Implemented | Applied to JWT responses only (draft-21 section 8.2) |
| HTTP Content Negotiation | Exact match only; RFC 9110 patterns (*/*, q=) not yet supported |
|
Historical Resolution (time=) |
✅ Implemented | Optional feature for time-based status queries |
| Status List Aggregation | ✅ Implemented | GET /api/v1/aggregation + optional aggregation_uri token member |
| X.509 Certificate EKU | Placeholder OID (...3.30); rename pending spec finalization |
Before running the server, ensure you have the following tools installed:
- Rust & Cargo (Latest stable).
- A supported database backend: PostgreSQL, MySQL, or SQLite for persistent deployments. The default local mode uses in-memory repositories.
- Docker (optional, for local testing).
Clone the Repository:
git clone https://github.com/adorsys/status-list-server.git
cd status-list-serverEnvironment Variables:
Create a .env file in the root directory. Take a look at the .env.template file for an example of the required variables.
The simplest way to run the project is with docker compose:
- Execute the command below at the root of the project:
docker compose up --buildThis command will pull all required images and start the server compiled with default compose features (postgres,aws).
To pass custom Cargo feature flags during build, specify the FEATURES environment variable:
FEATURES="mysql,aws" docker compose --profile mysql up --buildTo start the server in zero-infrastructure default in-memory mode, execute:
cargo runBy default, the server will listen on http://localhost:8000 using in-memory repositories, Moka cache, and store-based certificate management. No external databases or cloud services are required for development.
The crate uses modular Cargo feature flags to gate optional production backend drivers:
| Feature | Description | Default |
|---|---|---|
memory |
In-memory repositories, TTL cache, and static certificate loading. | ✅ Default |
postgres |
SeaORM PostgreSQL database driver. | ❌ Opt-in |
sqlite |
SeaORM SQLite database driver. | ❌ Opt-in |
mysql |
SeaORM MySQL database driver. | ❌ Opt-in |
aws |
AWS Secrets Manager and Route53 DNS-01 driver | ❌ Opt-in |
gcp |
GCP Secret Manager and Cloud DNS driver | ❌ Opt-in |
azure |
Azure Key Vault and Azure DNS driver | ❌ Opt-in |
vault |
HashiCorp Vault / OpenBao driver | ❌ Opt-in |
To build with specific backend drivers, pass the matching feature flag(s):
# Run with PostgreSQL support available
cargo run --features postgres
# Run with AWS native integration (automatically enables ACME)
cargo run --features postgres,awsFor deployment guidance and backend tradeoffs, see docs/database-backends.md.
The public API is documented with an OpenAPI 3.1 specification. See docs/openapi.yaml for the complete API contract.
-
Endpoint:
GET /api/v1/aggregation -
Description: Returns all Status List Token URIs hosted by this server in a single response (Token Status List draft-21 §9), enabling consumers to pre-fetch or keep an offline mirror of every list. The endpoint is publicly accessible with no authentication required. The aggregation is issuer-agnostic — every hosted status list URI is included regardless of which issuer owns it.
-
Responses:
200 OK
{ "status_lists": [ "https://statuslist.example.com/api/v1/status-lists/30202cc6-1e3f-4479-a567-74e86ad73693", "https://statuslist.example.com/api/v1/status-lists/755a0cf7-8289-4f65-9d24-0e01be92f4a6" ] }500 INTERNAL SERVER ERROR: System incurred an error
When the optional APP_SERVER__AGGREGATION_URI configuration is set, every emitted Status List Token (JWT and CWT) includes it as the optional aggregation_uri member (draft-21 §4.2 / §4.3), allowing a consumer to discover the aggregation link directly from any single list token. When unset, the member is omitted entirely.
All runtime behavior is controlled via environment variables prefixed with APP_ and using __ as a nested separator (e.g. APP_SERVER__PORT=8000). Sensible defaults are built in, so only non-default values need to be set. See .env.template for a complete example.
For deployment guidance and backend tradeoffs, see docs/database-backends.md.
The following constraints are validated at startup and will cause the server to fail fast if violated:
server.portmust be between 1 and 65535 (theu16type enforces the upper bound)server.cert.renewal_cron_schedulemust be a valid 6-field cron expression (seconds required)
The server uses JWT-based authentication with the following requirements:
-
Issuers must provide valid public key during registration using the
/api/v1/credentialsendpoint -
All authenticated requests must include a JWT token in the Authorization header:
Authorization: Bearer <jwt_token> -
The JWT token must:
- Be signed with the private key corresponding to the registered public key
- Have
iss(issuer) claim matching the registered issuer - Have valid
exp(expiration) andiat(issued at) claims
Example JWT token header:
{
"alg": "ES256"
}Example JWT token claims:
{
"iss": "test-issuer",
"exp": 1752515200,
"iat": 1752515200
}The Status List Server is provisioned with a cryptographic certificate that is embedded into all issued status list tokens. This certificate ensures the authenticity and integrity of the tokens distributed by the server.
- Certificate issuance and renewal are managed according to the configured renewal strategy.
- Every day, a cron job checks whether the certificate should be renewed based on this strategy.
- If the certificate is still considered valid according to the configured strategy, no renewal occurs; renewal is only triggered when necessary.
- The server signing key and certificate chain are stored in one cryptographic-material backend selected by enabled Cargo features (
vault,aws,gcp,azure, or in-memory fallback). - Certificate material stays cached until provisioning or renewal invalidates it. Signing-key reads can be cached with
APP_SERVER__CERT__SIGNING_KEY_CACHE_TTL; set it to0to force private-key reads to bypass the material cache. - Parsed certificate chains stay cached in memory until certificate provisioning or renewal replaces them.
Provisioning Modes:
- ACME Provisioning: Requests, validates (via DNS-01 challenge), and automatically renews TLS certificates using Let's Encrypt / ACME directory. Built into cloud-native binary variants.
- Static Loading: Loads pre-existing certificate chain and private key directly into memory, without ACME overhead or DNS challenges.
- If neither or only partial static material is provided, server startup immediately fails with a clear validation error.
Certificate Manager Builder Defaults:
CertManager::builder()defaults to ACME provisioning.- The default renewal strategy is
PercentageOfLifetime(None), which renews at 2/3 of the certificate lifetime. - ACME uses
DefaultHttpClientunless.acme_http_client(...)is supplied. - Store provisioning does not create ACME HTTP client state unless explicitly configured.
emaildefaults to an empty string,organizationdefaults to none, andekudefaults to none.domainsandcrypto_storagemust always be provided.- ACME additionally requires
challenge_handlerandacme_directory_url.
let manager = CertManager::builder()
.domains(["statuslist.example.com"])
.email("support@example.com")
.organization(Some("example.com"))
.acme_directory_url("https://acme-v02.api.letsencrypt.org/directory")
.crypto_storage(material_storage)
.challenge_handler(challenge_handler)
.eku(&[1, 3, 6, 1, 5, 5, 7, 3, 30])
.acme_strategy()
.build()?;let manager = CertManager::builder()
.domains(["statuslist.example.com"])
.crypto_storage(material_storage)
.store_strategy(StoreProvisioningStrategy::filesystem(
"/etc/status-list/tls.crt",
"/etc/status-list/tls.key",
))
.build()?;DNS Providers:
ACME DNS-01 challenges are solved through a configurable DNS provider. AWS Route53, Cloudflare, Google Cloud DNS, Azure DNS and self-hosted ACME-DNS are supported, selected via APP_SERVER__CERT__DNS__PROVIDER. See the DNS Provider Documentation for setup instructions.
The server implements proper error handling and returns appropriate HTTP status codes:
400 BAD REQUEST: Invalid input data401 UNAUTHORIZED: Missing or invalid authentication token403 FORBIDDEN: Insufficient permissions404 NOT FOUND: Resource not found406 NOT ACCEPTABLE: Requested format not supported409 CONFLICT: Resource already exists500 INTERNAL SERVER ERROR: Server-side error
The server can be deployed using a containerization platform such as Docker.
A Helm chart is provided for easy deployment on Kubernetes. For detailed instructions, see the Helm Deployment Guide.
Release images are scanned by digest and carry an SBOM and SLSA provenance, and release tags are applied only after the scan. See the Container Supply Chain guide for thresholds, verification commands, and how to triage findings.
You can run the tests using the following command:
cargo testTo verify the infrastructure-free application composition (domain models, domain ports, service container, and in-memory outbound adapters only), run:
cargo check --no-default-features --features memoryContributions are welcome and encouraged. Before contributing, please review the architecture documentation, which provides an overview of our architectural design. Also refer to the contributing guide for more details.
This project uses release-plz with Conventional Commits to automate versioning and changelog generation. Every push to main opens or updates a Release PR. Merging that PR creates a git tag and a GitHub Release automatically. The first Release PR creates the initial changelog entry.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.