didwebvh-rs 0.3.1

Implementation of the did:webvh method in Rust
Documentation

did:webvh implementation

Crates.io Documentation Rust

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
  • WebVH DID Create routines to make it easier to create DIDs programmatically
  • Pluggable signing via the Signer trait — 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. NetworkError exposes url, status_code, and message fields)
  • Convenience API: update_document(), rotate_keys(), deactivate() on DIDWebVHState
  • WitnessesBuilder for ergonomic witness configuration
  • Cache serialization: save_state() / load_state() for offline caching
  • async_trait re-exported so Signer implementors don't need a separate dependency
  • Feature flags: network (default), rustls, native-tls for 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:

[dependencies]
didwebvh-rs = "0.3.1"

Then:

use didwebvh_rs::prelude::*;

let mut webvh = DIDWebVHState::default();

// Load LogEntries from a file
webvh.load_log_entries_from_file("did.jsonl")?;

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).

To use the library without network support (e.g. for local file validation only):

[dependencies]
didwebvh-rs = { version = "0.3.0", default-features = false }

Convenience API

DIDWebVHState provides high-level methods for common DID lifecycle operations:

// Update the DID document
state.update_document(new_doc, &signing_key).await?;

// Rotate update keys
state.rotate_keys(vec![new_key], &signing_key).await?;

// Deactivate the DID
state.deactivate(&signing_key).await?;

See the examples/update_did.rs, examples/rotate_keys.rs, and examples/deactivate_did.rs examples for full usage.

WitnessesBuilder

Build witness configurations ergonomically:

use didwebvh_rs::prelude::*;

let witnesses = Witnesses::builder()
    .threshold(2)
    .witness(Multibase::new("z6Mk..."))
    .witness(Multibase::new("z6Mk..."))
    .build()?;

Cache Serialization

Save and load DIDWebVHState for offline caching:

// Save state to disk
state.save_state("cache.json")?;

// Load state from disk (re-validate before use)
let state = DIDWebVHState::load_state("cache.json")?;

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.

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.

cargo run --example wizard -- --help

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:

cargo run --release --example generate_history -- --help

For example, to generate 200 LogEntries with 10 witnesses each, you can run:

cargo run --release --example generate_history -- -c 200 -w 10

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:

cargo bench --bench did_benchmarks

Run a specific benchmark group or individual benchmark:

cargo bench --bench did_benchmarks -- "did_creation"
cargo bench --bench did_benchmarks -- "did_creation/basic"

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:

cargo +nightly bench --bench did_benchmarks_nightly

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 didwebvh_rs::prelude::*;
use didwebvh_rs::affinidi_secrets_resolver::secrets::Secret;
use serde_json::json;
use std::sync::Arc;

// Generate or load a signing key
let signing_key = Secret::generate_ed25519(None, None);

// Build parameters with the signing key as an update key
let parameters = Parameters {
    update_keys: Some(Arc::new(vec![
        signing_key.get_public_keymultibase().unwrap(),
    ])),
    portable: Some(true),
    ..Default::default()
};

// Build the DID document
let did_document = json!({
    "id": "did:webvh:{SCID}:example.com",
    "@context": ["https://www.w3.org/ns/did/v1"],
    "verificationMethod": [{
        "id": "did:webvh:{SCID}:example.com#key-0",
        "type": "Multikey",
        "publicKeyMultibase": signing_key.get_public_keymultibase().unwrap(),
        "controller": "did:webvh:{SCID}:example.com"
    }],
    "authentication": ["did:webvh:{SCID}:example.com#key-0"],
    "assertionMethod": ["did:webvh:{SCID}:example.com#key-0"],
});

// Create the DID
let config = CreateDIDConfig::builder()
    .address("https://example.com/")
    .authorization_key(signing_key)
    .did_document(did_document)
    .parameters(parameters)
    .also_known_as_web(true)
    .also_known_as_scid(true)
    .build()
    .unwrap();

let result = create_did(config).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 didwebvh_rs::prelude::*; // async_trait is re-exported here

struct MyKmsSigner { /* your KMS client, key ID, etc. */ }

#[async_trait]
impl Signer for MyKmsSigner {
    fn key_type(&self) -> KeyType {
        KeyType::Ed25519
    }

    fn verification_method(&self) -> &str {
        // Must be "did:key:{multibase}#{multibase}" format
        "did:key:z6Mk...#z6Mk..."
    }

    async fn sign(&self, data: &[u8]) -> Result<Vec<u8>, affinidi_data_integrity::DataIntegrityError> {
        // Call your KMS / HSM here — no private key bytes needed locally
        todo!()
    }
}

Then use your custom signer with CreateDIDConfig::builder_generic():

let kms_signer = MyKmsSigner { /* ... */ };

let config = CreateDIDConfig::builder_generic()
    .address("https://example.com/")
    .authorization_key(kms_signer)
    .did_document(did_document)
    .parameters(parameters)
    .build()
    .unwrap();

let result = create_did(config).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 = CreateDIDConfig::builder()
    .address("https://example.com/")
    .authorization_key(signing_key)
    .did_document(did_document)
    .parameters(parameters)
    .witness_secret("z6Mkw...", witness_key)
    .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: