Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Affinidi DID Hosting Control Plane
The DID Hosting Control Plane is the authoritative source of truth for
all DID management. Every management operation (DID lifecycle, agent
names, domains, ACL, the service registry, stats) is a signed Trust Task,
served the same over TSP, DIDComm and HTTPS (POST /api/trust-tasks); there
is no REST management API. It pushes updates to server edge nodes as signed
Trust Tasks, hosts an optional web-based management UI, maintains a service
registry, and supports passkey (WebAuthn) sign-in.
IMPORTANT: did-hosting-service crates are provided "as is" without any warranties or guarantees, and by using this framework, users agree to assume all risks associated with its deployment and use including implementing security, and privacy measures in their applications. Affinidi assumes no liability for any issues arising from the use or modification of the project.
Getting Started
Prerequisites
- Rust 1.94.0+ (2024 Edition)
- Node.js 20+ (only if building with the management UI)
1. Build
# Without UI
# With embedded management UI
&& && &&
The binary is produced at target/release/did-hosting-control.
2. Run the setup wizard
For non-interactive runs see
docs/bootstrap_startup.md
and the example recipe at examples/did-hosting-control-build.toml. The
recipe handles online VTA flow (with --setup-key-file), the
air-gapped offline-prepare / offline-complete pair, env-var
overlays, and reprovision safety.
The interactive wizard configures:
- Configuration file path — where to write
config.toml - Server DID identity — for DIDComm authentication
- Public URL — for passkey (WebAuthn) RP origin
- Host / port — listen address (default:
0.0.0.0:8532) - Log level / format — logging configuration
- Data directory — persistent storage path
- Secrets backend — where to store private key material
- Admin bootstrap — create an initial admin ACL entry
3. Start the control plane
If built with --features ui, browse to
http://localhost:8532/ to access the management UI.
Configuration
The control plane is configured via a TOML file. By default it
looks for config.toml in the current directory. You can specify
a different path with the --config flag or the
CONTROL_CONFIG_PATH environment variable.
Example config.toml
= "did:webvh:control.example.com"
= "did:webvh:mediator.example.com"
= "https://control.example.com"
[]
= true
[]
= "0.0.0.0"
= 8532
[]
= "info"
[]
= "data/did-hosting-control"
[]
= 900
= 86400
= 300
= 86400
[]
= "did-hosting-control"
# Service registry — register backend instances
# [[registry.instances]]
# label = "Primary Server"
# service_type = "server"
# url = "http://localhost:8530"
#
# [[registry.instances]]
# label = "Witness"
# service_type = "witness"
# url = "http://localhost:8531"
[]
= 60 # seconds
# Hosts the control plane may register — see "Registry allowlist" below.
= ["server-eu.internal", "witness-eu.internal"]
# Watchers: a DID whose log names this URL in its `watchers` parameter is
# pushed there (signed webvh/sync/* Trust Tasks, through the outbox). The DID
# may acknowledge those syncs and nothing else.
# [[registry.watchers]]
# url = "https://watcher1.example.com"
# did = "did:webvh:...:watcher1.example.com"
Service Registry
The [registry] section configures backend service instances
that the control plane manages.
Static instances can be defined in config.toml:
[[]]
= "Primary Server"
= "server" # server, witness, or watcher
= "http://localhost:8530"
[[]]
= "Witness Node"
= "witness"
= "http://localhost:8531"
[[]]
= "EU Watcher"
= "watcher"
= "http://watcher-eu:8533"
Instances can also be managed dynamically with the registry/* Trust Tasks.
Registry allowlist
registry.url_allowlist bounds which hosts may be registered (by an
administrator's registry/admin-register, or a server's server/register).
Matching is on the URL host only (scheme and port are ignored),
case-insensitive, and exact — example.com does not match
sub.example.com.
- Empty (the default): registration refuses non-routable / internal
literal hosts (loopback, RFC1918,
169.254.169.254, CGNAT, …); a public host is accepted. - Non-empty: only listed hosts may be registered. If a backend is on an
internal IP literal (e.g.
10.0.0.5), list that literal exactly.
Environment Variable Overrides
Environment variables use the CONTROL_ prefix:
| Variable | Description |
|---|---|
CONTROL_CONFIG_PATH |
Path to config file |
CONTROL_SERVER_DID |
Control plane DID |
CONTROL_MEDIATOR_DID |
Mediator DID |
CONTROL_PUBLIC_URL |
Public URL (passkey origin) |
CONTROL_SERVER_HOST |
Bind host |
CONTROL_SERVER_PORT |
Bind port |
CONTROL_LOG_LEVEL |
Log level |
CONTROL_REGISTRY_HEALTH_CHECK_INTERVAL |
Health check interval (sec) |
CLI Commands
did-hosting-control # Run control plane (default)
did-hosting-control setup # Interactive config wizard
did-hosting-control add-acl --did <DID> [--role admin|owner] [--label <name>] # Add ACL entry
did-hosting-control list-acl # List ACL entries
Features
Management UI
When built with the ui feature, the control plane embeds a
web-based management interface using rust-embed. The UI is
served as a fallback for any non-API GET requests — no separate
web server needed.
The UI provides:
- Server health and DID counts
- DID creation, upload, and deletion
- Witness proof management
- Access control management (admin only)
- Service instance overview
Passkey Authentication
Passkey/WebAuthn support is always compiled into the control plane.
It is activated at runtime when a public_url is configured: the
control plane then supports WebAuthn passkey enrollment and login,
providing browser-based passwordless authentication alongside DIDComm
challenge-response auth. Without a public_url, WebAuthn is not
initialised and authentication falls back to DID challenge-response.
Health Checking
Registered service instances are periodically health-checked.
The health check interval is configurable via
registry.health_check_interval (default: 60 seconds).
HTTP surface
The control plane has no REST management API. Its HTTP surface is:
| Method | Path | Description |
|---|---|---|
POST |
/api/trust-tasks |
The HTTPS binding of the Trust Task listener: the same dispatch TSP and DIDComm reach. Every management operation arrives here, authorised on the document's own proof. |
GET |
/api/health |
Unauthenticated liveness probe |
POST |
/api/auth/challenge |
Browser sign-in, step 1: a challenge for a SIOPv2 id_token |
POST |
/api/auth/ |
Browser sign-in, step 2: redeem a SIOPv2 id_token minted by the holder's VTA |
POST |
/api/auth/refresh |
Renew a console session: its refresh token, plus a proof from the browser's bound session key |
GET |
/* |
The management UI (ui feature), for any path outside /api |
The three /api/auth/* routes are the one sign-in with no Trust Task form:
auth/authenticate/0.2 carries no id_token, and a browser session does not
hold the subject key a Trust Task auth/refresh must be signed with. A peer
that signs its own documents signs in with auth/challenge/0.1 and
auth/authenticate/0.2 on POST /api/trust-tasks.
Operational metrics are the admin-only did-management/server/metrics/0.1
Trust Task; there is no Prometheus scrape endpoint.
Passkeys
Enrolment and login are Trust Tasks on POST /api/trust-tasks (and TSP and
DIDComm), with WebAuthn's ceremony data inside their payloads:
| Trust Task | Proof | Purpose |
|---|---|---|
auth/passkey/enroll/invite/0.2 |
administrator | Issue an invite: a URL token and a separate claim code, stored only as hashes |
auth/passkey/enroll/redeem/start/0.1 |
optional | Present token + claim code; get creation options (and uvOptions over existing passkeys of the purpose) |
auth/passkey/enroll/redeem/finish/0.1 |
optional | Bind the passkey to the invite's subject and consume the invite |
auth/passkey/enroll/start/0.2, finish/0.2 |
the subject | Add a login passkey to your own VID (re-verified with an existing one) |
auth/passkey/enroll/invite/{list,update,revoke}/0.1 |
administrator | Manage invites by inviteId |
auth/passkey/login/start/0.2, finish/0.2 |
optional / session key | Sign in, or step a session up |
A stepUp invite enrols a step-up-only passkey, kept in its own keyspace
(passkey_step_up) that the login ceremony never reads.
Library Usage
The did-hosting-control crate can be used as a library (e.g., by the did-hosting-daemon). It exposes:
did_hosting_control::config::AppConfig— configurationdid_hosting_control::server::AppState— application statedid_hosting_control::routes::router()— Axum routerdid_hosting_control::server::run()— standalone entry point
Support & feedback
If you face any issues or have suggestions, please don't hesitate to contact us using this link.
Reporting technical issues
If you have a technical issue with the Affinidi DID Hosting Service codebase, you can also create an issue directly in GitHub.
-
Ensure the bug was not already reported by searching on GitHub under Issues.
-
If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behaviour that is not occurring.
Contributing
Want to contribute? Head over to our CONTRIBUTING guidelines.