did:webvh implementation
A complete implementation of the did:webvh method in Rust. Supports version 1.0 spec.
A helpful implementation site is the webvh DID Method Information site
Change log
Features
- Create a did:webvh LogEntry and DID Document
- Resolve a did:webvh method
- Validate webvh LogEntries to v1.0 specification
- Update webvh DID
- Revoke webvh DID
- Witness webvh DID
- Migration of DID (portability)
- Validate witness information
- DID Query Parameters versionId, versionTime, and versionNumber implemented
- WebVH DID specification version support (v1.0 and pre-v1.0)
- Export WebVH to a did:web document
- Generate did:scid:vh alsoKnownAs alias from did:webvh DIDs
- URL validation rejects IP addresses per spec (domain names required)
- WASM friendly for inclusion in other projects (resolution only —
clifeature excluded) - WebVH DID Create routines to make it easier to create DIDs programmatically
- Embeddable interactive CLI flows for 3rd-party applications (
clifeature) - Pluggable signing via the
Signertrait — use HSMs, KMS, or any external signing service without exposing secret key material to the library - Structured error types for programmatic error handling (e.g.
NetworkErrorexposesurl,status_code, andmessagefields) - Convenience API:
update_document(),rotate_keys(),deactivate()onDIDWebVHState -
WitnessesBuilderfor ergonomic witness configuration - Cache serialization:
save_state()/load_state()for offline caching -
async_traitre-exported soSignerimplementors don't need a separate dependency - Feature flags:
network(default),rustls,native-tlsfor TLS backend selection - In-memory log verification via
resolve_log()— verify DID documents without filesystem or network access
Usage
Add this to your Cargo.toml:
[]
= "0.4.2"
Then:
use *;
let mut webvh = default;
// Load LogEntries from a file
webvh.load_log_entries_from_file?;
The prelude module re-exports the most commonly needed types:
DIDWebVHError, DIDWebVHState, LogEntryMethods, Parameters,
CreateDIDConfig, create_did, Witnesses, WitnessProofCollection,
Signer, KeyType, and async_trait.
Feature Flags
| Feature | Default | Description |
|---|---|---|
network |
yes | Enables HTTP(S) resolution via reqwest. Disable with default-features = false for local-only validation. |
ssi |
no | Enables integration with the ssi crate (implies network). |
rustls |
no | Use rustls TLS backend (implies network). |
native-tls |
no | Use platform-native TLS backend (implies network). |
cli |
no | Interactive CLI flows for DID creation and updates. Adds dialoguer, console, and affinidi-tdk. Not included in WASM builds. |
To use the library without network support (e.g. for local file validation only):
[]
= { = "0.3.0", = false }
Convenience API
DIDWebVHState provides high-level methods for common DID lifecycle operations:
// Update the DID document
state.update_document.await?;
// Rotate update keys
state.rotate_keys.await?;
// Deactivate the DID
state.deactivate.await?;
See the examples/update_did.rs, examples/rotate_keys.rs, and
examples/deactivate_did.rs examples for full usage.
Updating a DID Programmatically
The update module provides [update_did()] for programmatic DID updates,
complementing create_did(). It handles document changes, key rotation,
parameter updates, domain migration, deactivation, and witness signing:
use *;
// Update the DID document
let result = update_did.await?;
// Rotate authorization keys
let result = update_did.await?;
// Migrate to a new domain (requires portable=true)
let result = update_did.await?;
// Deactivate permanently
let result = update_did.await?;
// Access results
result.log_entry.save_to_file?;
result.state.witness_proofs.save_to_file?;
Multiple changes can be combined in a single update (e.g. document + TTL + watchers). For deactivation with active pre-rotation, the function automatically creates an intermediate log entry to disable pre-rotation first.
Examples
The examples/ directory contains runnable demonstrations of the library's API:
| Example | Command | Description |
|---|---|---|
create |
cargo run --example create |
Create a new DID with create_did(), {DID} placeholders, and aliases |
update_did |
cargo run --example update_did |
Update a DID document (add a service endpoint) using update_did() |
rotate_keys |
cargo run --example rotate_keys |
Rotate authorization keys using update_did() |
deactivate_did |
cargo run --example deactivate_did |
Permanently deactivate a DID using update_did() |
custom_signer |
cargo run --example custom_signer |
Implement the Signer trait for HSM/KMS integration |
resolve |
cargo run --example resolve -- <DID> |
Resolve a did:webvh DID over HTTP(S) and display the document |
wizard |
cargo run --example wizard --features cli |
Interactive CLI wizard for DID creation, updates, and resolution |
generate_history |
cargo run --release --example generate_history -- -c 200 |
Generate large DID histories for performance testing |
generate_large_did |
cargo run --release --example generate_large_did |
Generate a 1 MB+ DID file for benchmarking |
WitnessesBuilder
Build witness configurations ergonomically:
use *;
let witnesses = builder
.threshold
.witness
.witness
.build?;
Cache Serialization
Save and load DIDWebVHState for offline caching:
// Save state to disk
state.save_state?;
// Load state from disk (re-validate before use)
let state = load_state?;
Important: Loaded state should be re-validated because computed fields
(active_update_keys, active_witness) use #[serde(skip)] and will be
at their defaults after deserialization.
Embedding Interactive CLI Flows in Your Application
The cli feature provides interactive terminal flows that 3rd-party applications
can embed directly in their own CLIs. These give your users the same guided
DID creation and management experience as the built-in wizard.
[]
= { = "0.4.1", = ["cli"] }
Interactive DID Creation
Run the full interactive DID creation flow, or pre-configure parts of it:
use *;
use json;
// Fully interactive — prompts for everything
let result = interactive_create_did.await?;
// Or pre-configure services and address, prompt for the rest
let config = builder
.address
.service
.portable
.also_known_as_web
.build;
let result = interactive_create_did.await?;
// Access results
println!;
result.log_entry.save_to_file?;
result.witness_proofs.save_to_file?;
Use {DID} as a placeholder in pre-configured services and verification method IDs —
it is automatically replaced with the actual DID identifier (including SCID) during creation.
Interactive DID Updates
Update an existing DID with the same guided flow. Supports three operations: modifying the document/parameters, migrating to a new domain, or revoking the DID.
use *;
// Fully interactive — loads state from files, prompts for operation
let result = interactive_update_did.await?;
// Or pre-load state and choose the operation
let config = builder
.state
.secrets
.operation
.build;
let result = interactive_update_did.await?;
// Save updated state
result.log_entry.save_to_file?;
result.state.witness_proofs.save_to_file?;
Note: The
clifeature is intentionally excluded from WASM builds. WASM targets should use the core library API (create_did,resolve, etc.) directly.
Everyone likes a wizard
Getting started with webvh at first can be daunting given the complexity of the specification and supporting infrastructure such as witness and watcher nodes.
To help with getting started, a wizard for webvh has been created to help you.
To run this wizard, you need to have Rust installed on your machine.
WARNING: This wizard will generate secrets locally on your machine, and display the secret on the screen.
The wizard is meant for demonstration purposes only. Use in a production environment is not recommended.
Default Wizard Files
did.jsonl is the default WebVH LogEntry file that the wizard will create.
did-witness.json where Witness Proofs are saved.
did.jsonl-secrets is the default file containing key secrets
Is WebVH performant?
There is a lot going on with the WebVH DID method. A lot of keys, signing and validations
Depending on how often you are creating LogEntries, number of witnesses etc can have a big impact on performance.
To help with testing different usage scenario's, there is an example tool that can help you with testing real-world performance of the WebVH method.
To get options for the generate_history performance tool, run:
For example, to generate 200 LogEntries with 10 witnesses each, you can run:
This tool will save the output to
- did.jsonl (LogEntries)
- did-witness.json (Witness Proofs)
Criterion Benchmarks (stable Rust)
Run the full benchmark suite using Criterion:
Run a specific benchmark group or individual benchmark:
HTML reports are generated in target/criterion/.
Nightly Benchmarks
If you have the Rust nightly toolchain installed, you can also run the built-in
#[bench] benchmarks:
Benchmark Groups
| Group | Benchmarks | Description |
|---|---|---|
did_creation |
basic, with_aliases |
DID creation with minimal config and with alsoKnownAs aliases |
did_resolution |
single_entry, large_with_witnesses_120_entries |
File-based DID resolution with 1 and 120+ log entries |
validation |
single_entry, large_with_witnesses_120_entries |
Log entry and witness proof validation |
Creating a DID Programmatically
The create module provides a library API for creating a DID without any
interactive prompts. Use CreateDIDConfig::builder() to construct the
configuration:
use *;
use Secret;
use json;
use Arc;
// Generate or load a signing key
let signing_key = generate_ed25519;
// Build parameters with the signing key as an update key
let parameters = Parameters ;
// Build the DID document
let did_document = json!;
// Create the DID
let config = builder
.address
.authorization_key
.did_document
.parameters
.also_known_as_web
.also_known_as_scid
.build
.unwrap;
let result = create_did.unwrap;
// result.did — the resolved DID identifier (with SCID)
// result.log_entry — the signed first log entry (serialize to JSON for did.jsonl)
// result.witness_proofs — witness proofs (empty if no witnesses configured)
Bring Your Own Signer (HSM / KMS)
The library does not require you to hold secret key material in memory. All
signing operations go through the Signer trait, so you can delegate to an
HSM, cloud KMS, or any other external signing service. The built-in Secret
type implements Signer for local Ed25519 keys, but you can replace it with
your own implementation:
use *; // async_trait is re-exported here
Then use your custom signer with CreateDIDConfig::builder_generic():
let kms_signer = MyKmsSigner ;
let config = builder_generic
.address
.authorization_key
.did_document
.parameters
.build
.unwrap;
let result = create_did.await.unwrap;
The same applies to witness signing — sign_witness_proofs() accepts any
HashMap<String, W> where W: Signer.
Witness Support
If your DID uses witnesses, provide the witness signers via the builder:
// For each witness, add its DID and signer
let config = builder
.address
.authorization_key
.did_document
.parameters
.witness_secret
.build
.unwrap;
The sign_witness_proofs() function is also available separately if you need
to sign witness proofs outside of the full DID creation flow.
License
Licensed under:
- Apache License, Version 2.0, (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)