Fixes the negative-pct-encoded-ip-host vector in the
didwebvh-test-suite
(closes #47). Not a resolver vulnerability — a percent-encoded IP host was
already blocked before any HTTP request — but the spec requires the DID to be
rejected as invalidDid by the parser itself, with no fetch attempted.
WebVHURL::parse_did_url()now rejects a host segment that resolves to an IPv4 or IPv6 literal. The check previously ranIpAddr::from_stragainst the raw, still-percent-encoded segment, so127%2E0%2E0%2E1was classified as a domain name and parsing succeeded;Url::parsewould then decode it back to127.0.0.1. The IP-rejection check now goes throughurl::Host::parse, which percent-decodes (case-insensitively, per RFC 3986 §2.1) and applies IDNA before classifying the host.get_http_url()/get_http_whois_url()/get_http_files_url()keep their post-normalisation host check as defense in depth.- The same change closes a related parse-time gap: the alternate IPv4 spellings
2130706433,0x7f.0.0.1,127.1and0177.0.0.1were also accepted as "domain names" (and, like the above, caught only later at URL-build time). - A host segment that is empty or that decodes to something which is not a legal
URL host (e.g.
a%2Fb,1.2.3.4.5) is now rejected at parse time rather than being carried intoUrl::parse.
- Refreshed the dependency lockfile to the latest compatible versions (31
crates). This clears RUSTSEC-2026-0204 (
crossbeam-epoch0.9.18 — invalid pointer dereference in thefmt::Pointerimpl forAtomic/Shared), published 6th July 2026 and fixed in 0.9.20.
No public-API breakage — the new field is optional and defaults to the
previous now() behavior.
CreateDIDConfig/UpdateDIDConfignow accept an optionalversion_time(builder method.version_time(...)).None(default) stamps the entry withnow()as before; set it to control the log-entry timestamp. This lets a caller creating several entries in quick succession (e.g. a back-to-back create-then-update) backdate and space them —versionTimeserialises at second granularity and must be strictly increasing and not in the future, so same-second entries would otherwise serialise identically and make the DID unresolvable. Honoured on the single-entry operations (genesis create, standard update, migrate); the multi-entry deactivation path keeps per-entrynow().
- Refreshed the dependency lockfile to the latest compatible versions.
Closes #44. No public-API breakage — everything new is additive or behind an off-by-default feature flag.
- Off-by-default
arbitraryfeature. Implementsarbitrary::Arbitraryon the public log-entry and parameters types so downstream consumers can do structure-aware, coverage-guided fuzzing of the chain verifier. The leaf types (Version,Multibase,Witness,Witnesses,Parameters,Parameters1_0,Parameters1_0Pre) derive it; theLogEntrytypes get hand-written impls because theirchrono::DateTime/serde_json::Value/DataIntegrityProoffields are foreign types the orphan rule won't let us derive through.stateis generated as a bounded JSON value and proofs are built from arbitrary parts viaDataIntegrityProof::new, so the structural proof path (shape enforcement, did:key resolution, cryptosuite gating) is reachable without valid signatures. No effect on default builds and no new always-on dependency. DIDWebVHState::from_log_entries(Vec<LogEntry>). Assembles an unvalidated state from an in-memory list of log entries (each wrapped asNotValidated), mirroringload_log_entries_from_filewithout touching the filesystem. The documented way to drivevalidate()from aVec<LogEntry>(e.g. fuzzing);version_numberis parsed best-effort so deliberately broken chains still reach the verifier.fuzz/crate with cargo-fuzz targets (parameters_validate,logentry_deserialize,chain_validate,proof_verify). It is a workspace-detached crate (its own empty[workspace]table), so normalcargo build/test/clippynever compile it and the nightly-only libfuzzer toolchain stays out of the default CI. A newFuzzGitHub workflow runs the targets only onworkflow_dispatchand a weekly cron. See README "Fuzzing".
- Dependency refresh (
cargo update). Notablyaffinidi-data-integrity0.7.1 → 0.7.6, which madeDataIntegrityProof#[non_exhaustive]; the test/test-utility helpers that built it by struct literal now useDataIntegrityProof::new().
Closes #42. No public-API breakage. Pre-existing logs produced by spec-compliant implementations continue to resolve unchanged.
- Witness
idis now serialized as adid:keyidentifier. The didwebvh 1.0 spec § "Witnesses" requires each entry in thewitnessesarray to carry adid:keyid(did:key:z6Mk…), but aWitnessbuilt from a bare multibase key (z6Mk…) serialized the raw key, producing non-spec, non-interoperable DID logs (thewitness-threshold/witness-updatetest-suite vectors showed"id":"z6Mk…"instead of"id":"did:key:z6Mk…").Witnessnow canonicalizes itsidtodid:keyform on both serialization and deserialization, so output is spec-compliant regardless of how the value was constructed. Canonicalization is a no-op for an already-did:keyid, so logs from spec-compliant implementations round-trip byte-for-byte and theirentryHashcontinues to verify. Duplicate detection inWitnesses::validate()now compares on the canonical form, and a newWitness::new()constructor applies the same normalization.
affinidi-data-integrity0.6 → 0.7. Picked up via the manifest bump; all other dependencies refreshed to their latest semver-compatible versions (cargo update), pruning stale duplicate trees (oldreqwest,hyper0.14,rustls0.21,bitflags1.x).
Closes the issues raised in
#39,
a cross-implementation review of the four open-source did:webvh
resolvers. Patch series tightens authorization, DID-URL parsing, and
witness-proof validation. No public-API breakage; consumers on 0.5.x
should upgrade. Pre-existing logs continue to resolve; the changes
reject malformed or malicious inputs that previously slipped through.
MSRV: 1.94.0 → 1.95.0 (required by transitive lockfile bumps stable since 2026-04-14).
- Reject mismatched
did:keybody/fragment in log-entry proof authorization.check_signing_key_authorized()compared only the fragment of the proof'sverificationMethodagainstupdateKeys, but signature verification decoded the public key from the DID body. An attacker could setverificationMethod = "did:key:<attacker-mb>#<authorized-mb>"— the fragment matched an authorized key so authorization passed, while the signature was verified against the attacker's own key. This allowed anyone to forge arbitrary DID log entries for anydid:webvhDID. TheverificationMethodis now required to be exactlydid:key:{mb}#{mb}where{mb}is an authorized multibase. - Disable HTTP redirects in DID resolution.
reqwestfollowed up to 10 redirects by default. A malicious host serving adid:webvhDID could 302-redirect thedid.jsonl/did-witness.jsonfetch to an internal address (cloud metadata, localhost, RFC 1918), turning the resolver into an SSRF proxy and bypassing the existing IP-address rejection inWebVHURL::parse_did_url(). The native client now setsredirect(Policy::none()); the WASM path is unchanged (governed by the browser's fetch/CORS model). - Reject duplicate witness IDs.
Witnesses::validate()checked the count met the threshold but not for duplicates.WitnessProofCollection::validate_log_entry()incrementsvalid_proofsonce per listed witness, so a controller declaringthreshold: 3, witnesses: [W1, W1, W1]could satisfy the threshold with a single proof fromW1. Duplicate witness IDs are now rejected. - Reject path-traversal segments when converting DID → HTTP URL.
WebVHURL::parse_did_url()joined the colon-separated path components of adid:webvhidentifier into the HTTP path with no validation. A DID such asdid:webvh:<scid>:example.com:..:..:otherresolved tohttps://example.com/../../other/did.jsonl..,.., empty segments, and segments containing/are now rejected. - Percent-decode path segments before the traversal check. The
raw check could be defeated by encoding:
…:%2E%2E:xpassed the literal..test, became…/%2E%2E/x/…, andUrl::parsecollapsed it. Segments are now percent-decoded before the check.\is also rejected. - Match lowercase
%3awhen splitting host:port in DID URL parsing. Percent-encoding is case-insensitive (RFC 3986 §2.1) but the parser only split on literal%3A. A DID usingdid:webvh:<scid>:127.0.0.1%3a8080leftdomain = "127.0.0.1%3a8080", which did not parse as anIpAddrand slipped pastreject_ip_address(). Both encodings are now recognised. - Re-check host after
Url::parseto block percent-encoded IP bypass.reject_ip_address()ran on the raw DID domain segment before percent-decoding, sodid:webvh:SCID:127%2E0%2E0%2E1did not parse asIpAddrand passed —Url::parsethen decoded the host to127.0.0.1and the resolver fetched from localhost. Same for169.254.169.254(cloud metadata). All threeget_http_*_url()functions now re-checkurl.host()after parsing and rejectHost::Ipv4/Host::Ipv6. The earlyreject_ip_address()remains as a cheap pre-check. - Don't re-include stripped fragment when DID URL has no query.
The query-split fallback used the wrong variable, gluing an
already-stripped fragment back onto the prefix before scid/domain
splitting. For
did:webvh:<scid>:127.0.0.1#xthis yieldeddomain = "127.0.0.1#x", again bypassingreject_ip_address(). - Verify "later version" witness proofs before counting toward
threshold. A witness proof for published version
N > currentwas counted toward the current entry's threshold without signature verification, on the assumption it would be verified when entryNitself was processed. But if the witness was rotated out before entryN, the proof was never verified anywhere — letting a compromised controller forge "later" proofs for rotated-out witnesses and satisfy the threshold for every earlier entry. The signature is now verified against its own versionId in this branch before incrementingvalid_proofs. - Bind witness proofs to this log's versionIds. A witness signs
{"versionId": X}. Nothing in that payload names the DID, so a genuine signatureWmade for another DID's entry was cryptographically valid here too. Since the witness-proofs file is fetched from the (potentially compromised) DID host, an attacker could replayW's proof from another DID into this one. Aftergenerate_proof_state, proofs whose versionId is not present in this log are now dropped. - Enforce
eddsa-jcs-2022cryptosuite on controller proofs.verify_log_entrycheckedproofPurposebut notcryptosuite, while witness proofs already enforcededdsa-jcs-2022viaenforce_witness_proof_shape(). The didwebvh 1.0 spec mandateseddsa-jcs-2022for log-entry proofs too — without this check, the proof's suite chose the canonicalization pipeline while public-key bytes were decoded fromdid:keyindependently, an algorithm-substitution surface that grows as the upstream library adds suites. The widened check still admitsMlDsa44Jcs2024andSlhDsa128Jcs2024when theexperimental-pqcfeature is enabled. - Bind the DID document's SCID to the verified
parameters.scid.verify_scid()proved thatparameters.scidis the genesis self-hash, but nothing checked that the SCID embedded instate["id"]— the DID the resolver matches the request against — is that same value. An attacker could publish a genesis withstate.id = "did:webvh:<anything>:host"whileparameters.scid = <real genesis hash>and a user resolvingdid:webvh:<anything>:hostgot a "validated" log with no cryptographic binding between the resolved DID and the genesis. The third colon-segment ofstate["id"]is now required to equalparameters.scid. - Enforce SCID immutability across every entry, not just genesis.
verify_portability()let a portable DID changestate["id"]to anything as long as the previous DID appeared inalsoKnownAs— including a new id with a different SCID segment, reopening the self-certifying bypass on every non-genesis entry. The SCID segment ofstate["id"]is now required to equalparameters.scidon every entry. The spec is explicit that portability moves the host/path; the SCID is the cryptographic anchor and never changes. - Require
updateKeyswhen previous entry committednextKeyHashes. When entryNsetnextKeyHashes(pre-rotation active), entryN+1could omitupdateKeysentirely — theNone/ empty arms inheritedprevious.active_update_keysunchanged and the proof was then authorized against those inherited old keys. An attacker who compromised an old update key after the controller pre-committed its replacement could forge entryN+1by leavingupdateKeysout, a complete bypass of the pre-rotation guarantee. Absent and emptyupdateKeysare now rejected when the previous entry had pre-rotation active.
- Don't panic on malformed
idinconvert_webvh_id_to_web_id. Theidpassed in is read from the DID document'sstate["id"]field, which is attacker-controlled. A value with fewer than three:segments (e.g."foo") panicked on theparts[3..]slice, crashing any resolver that calledto_web_did()on a hostile log. Malformed input now degrades to a baredid:webinstead of a DoS. - Distinguish older-version proofs from current-version proofs in
the witness verifier. When the stored proof for a witness had
oldest_id < version_number, the code fell through to the "current version" branch and tried to verify against the current entry's versionId — which the proof was not signed over. The verifier failed with "signature invalid", masking the actual semantic: this proof simply does not cover this entry. The threshold check at the end now correctly surfaces"threshold (N) was not met. Only (M) proofs were validated"instead of a misleading signature error. Surfaces a real cross-implementation interop issue: Python, Java, and Java-EECC witness files keep stale per-version proofs (they do not cull) so the threshold cannot be met by older-version proofs alone. Rust's culling behaviour (keep only the latest per witness) was already correct.
- MSRV bumped 1.94.0 → 1.95.0. Required by transitive
dependencies pulled in by
cargo update(stable since 2026-04-14). CI MSRV job's toolchain matrix updated accordingly. No source changes required. serde_withconstraint widened3.18→3.20to reflect the actual minimum.Cargo.lockrefreshed.affinidi-crypto 0.1.5 → 0.1.6(fixes upstreamml-dsaAPI drift onKeyGen),tokio 1.52.1 → 1.52.3,reqwest 0.13.2 → 0.13.3,serde_json 1.0.149 → 1.0.150,serde_with 3.18.0 → 3.20.0, plus transitive bumps (tower-http,wasm-bindgenfamily,web-sys,winnow,zerofrom). No major-version churn; noCargo.tomlconstraints widened.
cargo audit: ignore RUSTSEC-2026-0104 (rustls-webpki CRL panic). Same crate version (0.101.7) and same transitive chain (reqwest 0.11via the optionalssifeature) as already-ignored RUSTSEC-2026-0098 and RUSTSEC-2026-0099. Thessifeature is optional and not enabled indefault, so the default build has zero advisories. Inline comments now separate vulnerabilities from unmaintained-package warnings to make triage easier.
Fixes a resolution-time interop break with typed DID-Document parsers
(notably affinidi-did-common::DIDDocument, whose service[].id field
is url::Url). Resolution now produces documents that satisfy DID Core
1.0 §5.4.
- Implicit
#filesand#whoisservices now use absolute-URI IDs. Previously the resolver emitted relative-fragment IDs ("#files","#whois") to match the didwebvh-test-suite reference output and the didwebvh-ts implementation. DID Core 1.0 §5.4 requires serviceidto be a URI per RFC 3986 — which mandates a scheme — so a fragment-only relative reference is a URI-reference, not a URI, and is rejected by spec-compliant typed parsers with"relative URL without a base: '#files'".get_did_document()andto_web_did()now emit"<did>#files"/"<did>#whois". The duplicate-detection logic still recognises both forms, so existing user-supplied services keep working unchanged.
generate_historyexample. Adds a "Run Summary" block reporting total keys generated (split into update vs witness), witness/watcher swap counts, witness proofs created, and witness proofs after optimisation. Fixes a panic on--witnesses 0and a staledid-witness.jsonload on the no-witness path. Wires upWitnessVerifyOptions::with_extra_allowed_suiteso--key-type ml-dsa-44validates end-to-end without manual config.
Closes gaps flagged in
didwebvh-test-suite PR #4.
Additive public surface change only: MetaData gains a version_number
field. Consumers that construct MetaData with ..Default::default() or
that only read fields are unaffected. Positional struct-literal
construction would need the new field added — but MetaData is primarily
produced by the resolver and consumed by callers, so this is expected to
be transparent in practice.
- Deactivation accepts non-empty
updateKeys. Per didwebvh 1.0 §Deactivate,updateKeysSHOULD be set to[]on the deactivation entry — it is not a MUST. The resolver previously rejected deactivation entries whoseupdateKeyswas non-empty with"DID Parameters say deactivated, yet updateKeys are not null!"; it now accepts any shape. Logs produced by the TypeScript reference resolver that carry forwardupdateKeyson the deactivation entry now resolve.
MetaData::version_number: u32. The resolved DID document metadata now exposes the integer version number (e.g.2for versionId"2-Qm...") alongside the existingversion_idstring, matching the shape emitted bydidwebvh-ts. Consumers no longer need to parse theversionIdprefix themselves.- Committed interop fixtures from didwebvh-test-suite PR #4 under
tests/test_vectors/test_suite/with a matching walker intests/test_suite_interop.rs. Twelve happy-path scenarios resolve; one (witness-update) is#[ignore]'d pending a follow-up investigation into TS-vs-Rust witness proof canonicalization after mid-chain witness configuration changes.
affinidi-data-integritybumped to0.6. No API impact on didwebvh-rs consumers — the upstream change is transparent through our wrappers.tokioloosened from= "1.50"to"1"to match the workspace-wide policy of tracking the minor line rather than pinning a patch.- Transitive dependencies refreshed via
cargo update:affinidi-crypto 0.1.4 → 0.1.5,openssl 0.10.77 → 0.10.78,openssl-sys 0.9.113 → 0.9.114,rustls-webpki 0.103.12 → 0.103.13,sha3 0.10.8 → 0.10.9,typenum 1.19.0 → 1.20.0,winnow 1.0.1 → 1.0.2.
- Fixed issue #35 — resolution rejected spec-compliant logs that used
plain key rotation (no pre-rotation). The read path now authorises
non-pre-committed log entries against the previous entry's
updateKeys, matchingdidwebvh 1.0§"Authorized Keys" and the writer's pre-existing logic. Logs produced by other spec-compliant resolvers (e.g. the TypeScript reference resolver) that use plain rotation now resolve correctly.
DIDWebVHState::validatereturnsValidationReport. Previously the function silently truncated the log and returnedOk(())on partial validation — the failure mode that surfaced #35. The new#[must_use]ValidationReportcarriesok_until: Stringandtruncated: Option<TruncationReason>;ValidationReport::assert_complete()is the one-call path for strict resolution.- Third-party types removed from top-level re-exports.
didwebvh_rs::{Signer, KeyType, async_trait}anddidwebvh_rs::affinidi_secrets_resolver::*are gone; usedidwebvh_rs::prelude::*or depend on the source crates directly. Avoids silent version skew via the crate-root convenience re-export. - Dropped
affinidi-tdkas a runtime dep. Theclifeature no longer pulls in tdk (and its messaging-SDK / meeting-place transitive graph). Replaced the single callsite withdidwebvh_rs::did_key::generate_did_key, a ~30-line wrapper overaffinidi-did-common+affinidi-secrets-resolver. - Witness proofs now spec-strict by default.
LogEntry::validate_witness_proofandWitnessProofCollection::validate_log_entrytake&WitnessVerifyOptions. The default options reject witness proofs that don't useeddsa-jcs-2022withproofPurpose: assertionMethod, matchingdidwebvh 1.0§"The Witness Proofs File". UseWitnessVerifyOptions::with_extra_allowed_suitefor additive runtime opt-in;DIDWebVHState::validate_with(options)is the companion tovalidate()for callers that need this. - Deprecated
affinidi-data-integrityAPIs migrated off.sign_jcs_data,sign_rdfc_data*, andverify_data_with_public_keyreplaced with the unifiedDataIntegrityProof::sign(&doc, signer, SignOptions)andproof.verify_with_public_key(&doc, pk, VerifyOptions)in 0.5.4. - MSRV bumped to 1.94.0 (required by upstream
affinidi-*0.5.x / 0.6.x).
didwebvh_rs::did_key::generate_did_key— creates a freshdid:keywith aSecret(id set todid:key:{mb}#{mb}).didwebvh_rs::witness::WitnessVerifyOptions(#[non_exhaustive]) withextra_allowed_suites: Vec<CryptoSuite>for runtime cryptosuite opt-in.ValidationReport+TruncationReasonre-exported at the crate root.- Regression test for issue #35 (
tests/plain_rotation_issue_35.rs), with a Node-based fixture generator committed undertests/fixtures/plain-rotation/. - [lints.rust] + [lints.clippy] section in Cargo.toml with pedantic group enabled and a curated allowlist.
experimental-pqcCargo feature — forwards toaffinidi-data-integrity/post-quantum+affinidi-secrets-resolver/post-quantumto unlock ML-DSA-{44,65,87} and SLH-DSA-SHA2-128s cryptosuites. Off-spec for didwebvh 1.0; use for interop testing with other PQC-aware implementations. The runtimeWitnessVerifyOptions::extra_allowed_suitesescape hatch stays available independently of the compile-time feature.
- Bumped
ssirequirement from 0.15 to 0.16 (optional feature). The old pin carried a transitivelibipld 0.14 → core2 ^0.4chain whose solecore2 0.4.0version is yanked from crates.io, breakingcargo update. 0.16 dropped that chain.
PublicKey::get_public_key_bytesnow delegates toaffinidi_data_integrity::did_vm::resolve_did_key, which supports every multicodec registered upstream (Ed25519, secp256k1, P-256/384/521 today; ML-DSA / SLH-DSA when the upstream feature is enabled).
didwebvh-tsinterop test ignored — the reference TypeScript resolver uses the literal"{SCID}"placeholder (not the previous entry's versionId) when computing entryHashes for non-genesis entries, contrary to thedidwebvh 1.0spec §"Entry Hash Generation and Verification". The committed cross-impl fixture is behind#[ignore]pending upstream resolution.ssioptional feature pulls known-advisory transitive crates. The default build (nossifeature) is advisory-clean. Enablingssipulls in a long transitive tree including thedid-*adapters, which indirectly depend onrsa 0.6.1(RUSTSEC-2023-0071, Marvin Attack),rustls-webpki 0.101.7(RUSTSEC-2026-0098 + RUSTSEC-2026-0099, name constraint bugs), and four unmaintained crates (derivative,proc-macro-error,rustls-pemfile,serde_cbor). CI'scargo auditstep explicitly ignores these with a reason comment; resolve by upgradingssiwhen upstream drops the affected dep chains.
// Before (0.4.x)
use didwebvh_rs::{Signer, KeyType};
use didwebvh_rs::affinidi_secrets_resolver::secrets::Secret;
state.validate()?;
// After (0.5.0)
use didwebvh_rs::prelude::*; // gets Signer, KeyType, Secret, ValidationReport, ...
state.validate()?.assert_complete()?; // strict mode (recommended for resolvers)
// or
let report = state.validate()?;
if let Some(reason) = report.truncated { /* handle partial validation */ }- Removed
multihashdependency — Themultihashcrate (and its transitivecore2dependency) has been replaced with an inline SHA-256 multihash encoder. Thecore2crate has been yanked from crates.io, makingmultihash 0.19uninstallable for new users. The library only used multihash for a simple 2-byte prefix encoding, so this is now handled directly without any external dependency. No public API changes.
- Updated
aws-lc-systo 0.39.1 — Fixes RUSTSEC-2026-0044 (X.509 Name Constraints Bypass via Wildcard/Unicode CN). - Updated
rustls-webpkito 0.103.12 — Fixes RUSTSEC-2026-0049 (CRLs not considered authoritative by Distribution Point due to faulty matching logic).
- Committed
Cargo.lock— Pinning resolved dependency versions to prevent CI and fresh builds from failing due to the yankedcore2crate (transitive dependency of thessicrate viassi-ucan → libipld → multihash 0.16).
clifeature flag — Embeddable interactive CLI flows for 3rd-party applications. Addsdialoguer,console, andaffinidi-tdkas optional dependencies. Not included in WASM builds.interactive_create_did()(cli_createmodule) — Interactive DID creation flow with the same guided experience as the built-in wizard. Third-party apps can embed this in their own CLIs. Supports:- Full interactivity (all values prompted) via
InteractiveCreateConfig::default() - Partial pre-configuration via the builder (skip specific prompts)
- Full pre-configuration (no prompts) for automated use
{DID}placeholder rewriting in pre-configured services and VM IDs- Returns the created DID, signed log entry, witness proofs, and all secrets
- Full interactivity (all values prompted) via
interactive_update_did()(cli_updatemodule) — Interactive DID update flow supporting three operations:- Modify: Edit DID document and/or parameters (auth keys, witnesses, watchers, TTL, portability, pre-rotation)
- Migrate: Move DID to a new domain (rewrites identifiers, adds previous
DID to
alsoKnownAs) - Deactivate: Permanently deactivate the DID (handles pre-rotation teardown)
- Returns the updated state, new log entry, and updated secrets
UpdateSecrets— Secret management type with hash-based and public-key-based lookups, used for DID update operations. Compatible with the wizard'sConfigInfoJSON format for loading secrets from existing files.update_did()(updatemodule) — Programmatic DID update API, complementingcreate_did(). Supports document changes, key rotation, parameter updates (witnesses, watchers, TTL, pre-rotation, portability), domain migration (with identifier rewriting), and deactivation (with automatic pre-rotation teardown). Handles witness proof signing. Uses the same builder pattern asCreateDIDConfig.- Shared CLI utilities (
cli_commonmodule, internal) — Common prompt helpers, key generation, witness setup, and next-key-hash generation shared between create and update flows.
- Inline concept explanations — All interactive prompts now explain key DID concepts in context: what witnesses and watchers do, how pre-rotation works, what verification relationships mean (authentication, assertionMethod, keyAgreement, etc.), what controllers are for, and what portability implies.
- Key type guidance — The verification method key selection now describes each algorithm (Ed25519 recommended, X25519 for encryption, P-256/P-384 for enterprise, secp256k1 for blockchain).
- TTL and threshold recommendations — Default TTL of 3600 seconds suggested, witness threshold explained with concrete example (e.g. "threshold=2 with 3 witnesses means any 2 of 3 must sign").
- Consistent terminology — Standardized on "deactivate" (not "revoke"), "authorization keys" (not "updateKeys"), and consistent prompt phrasing throughout all CLI flows.
- Format hints — Input prompts now include format examples (e.g. multibase
encoding
z6Mk..., DID formatdid:key:z6Mk..., watcher URLs). - Wizard example refactored — The
wizardexample now uses the library'sinteractive_create_did()andinteractive_update_did()flows instead of its own standalone implementation. This reduced the wizard from ~1800 lines across 9 files to ~280 lines across 3 files (main.rs,did_web.rs,resolve.rs). The wizard now requires theclifeature (cargo run --example wizard --features cli).
- Version bump: 0.4.0 → 0.4.1
resolve()/resolve_owned()signature change — Thetimeoutandeager_witness_downloadparameters have been replaced with a singleResolveOptionsstruct. Callers should migrate fromresolve(did, None, false)toresolve(did, ResolveOptions::default()). Custom timeout or eager witness download can be set via struct fields:ResolveOptions { timeout: Some(Duration::from_secs(5)), eager_witness_download: true, ..ResolveOptions::default() }
- HTTP response size limits —
download_file()now enforces a maximum response body size to prevent memory exhaustion from malicious or misconfigured servers.Content-Lengthheader is checked first for early rejection before any body data is read.- Body is read in chunks via
response.chunk()with a running byte counter, catching oversized responses even whenContent-Lengthis absent or inaccurate (e.g. chunked transfer encoding). - Default limit: 200 KB (
DEFAULT_MAX_RESPONSE_BYTES), configurable per-request viaResolveOptions::max_response_bytes.
ResolveOptionsstruct — Bundles network resolution options (timeout,eager_witness_download,max_response_bytes) into a single configuration type with sensible defaults viaDefaulttrait. Re-exported frompreludebehind thenetworkfeature gate.ResponseTooLargeerror variant — NewDIDWebVHError::ResponseTooLargecarries the offending URL and the configured byte limit, making it easy for consumers to distinguish size-limit rejections from other network errors.generate_large_didexample — Generates a valid 1 MB+did.jsonlfile with backdated timestamps for benchmarking and testing. Accepts a URL via--url(properly parsed into WebVH DID format viaWebVHURL::parse_url()), configurable target size (--target-kb), and includes generation, write, load, and validation timing.resolveexample CLI improvements — Now usesclapfor argument parsing with a--max-size-kb(-l) flag to set the response size limit from the command line.
resolve_log()/resolve_log_owned()— Accept raw JSONL log data and optional witness proofs as strings, enabling client-side cryptographic verification without filesystem or network access. Supports architectures where a cache server resolves DIDs and forwards the raw log alongside the document, allowing clients to independently verify the DID document has not been tampered with.- Public parsing helpers —
parse_log_entries(),parse_witness_proofs(), andneeds_witness_proofs()are now public and available without thenetworkfeature, since they operate on in-memory data only.
- Convenience API —
DIDWebVHStatenow providesupdate_document(),rotate_keys(), anddeactivate()methods for common DID lifecycle operations without manually constructing parameter diffs. - Feature flags —
reqwestis now optional behind thenetworkfeature (default on). Consumers who only need local file validation can opt out withdefault-features = false. TLS backend selection viarustlsandnative-tlsfeatures. WitnessesBuilder— Ergonomic builder for constructing witness configurations with threshold validation:Witnesses::builder().threshold(2).witness(key).build()?{SCID}placeholder validation —CreateDIDConfigBuilder::build()now validates that the DID documentidfield contains a{SCID}or{DID}placeholder, with a clear error message if missing.- Error context helpers —
DIDWebVHError::validation(),DIDWebVHError::parameter(), andDIDWebVHError::log_entry()stamp version/field context into error messages for easier debugging. async_traitre-export —async_traitmoved from dev-dependencies to dependencies and re-exported from the crate root andprelude, soSignerimplementors don't need a separate dependency.- Cache serialization —
DIDWebVHStatenow implementsSerializeandDeserialize, withsave_state(path)andload_state(path)convenience methods for offline caching.LogEntryState,LogEntry,Parameters, andVersionnow also deriveDeserialize. resolve_owned()/resolve_file_owned()— Return owned (cloned)(LogEntry, MetaData)so callers don't need to borrowDIDWebVHState.- Property-based tests —
proptestadded for Multibase serde round-trips and WitnessesBuilder threshold validation. - Lifecycle examples —
examples/update_did.rs,examples/rotate_keys.rs, andexamples/deactivate_did.rsdemonstrate the convenience API. - Pluggable signing via
Signertrait — all signing operations now go through theSignertrait fromaffinidi-data-integrity. This means secret key material no longer needs to be held in-process; you can delegate signing to an HSM, cloud KMS (e.g. AWS KMS, Azure Key Vault, HashiCorp Vault), or any external signing service by implementing theSignertrait.CreateDIDConfig<A, W>is now generic over authorization and witness signer types, with defaults ofSecretfor full backward compatibilitycreate_did(),sign_witness_proofs(), andDIDWebVHState::create_log_entry()accept anySignerimplementationSignertrait andKeyTypere-exported from the crate root andpreludeCreateDIDConfig::builder_generic()added for custom signer types;CreateDIDConfig::builder()continues to work withSecretas before
- Structured
NetworkError—DIDWebVHError::NetworkErrornow carries typed fields (url,status_code,message) instead of a plainString. Consumers can programmatically distinguish HTTP errors (404, 500) from transport failures (timeouts, connection refused) by inspectingstatus_code. - Removed
regexdependency — DID string operations indid_web.rsnow usestr::split_once(),str::strip_prefix(), and a customreplace_webvh_prefix()function, eliminating theregexcrate from the dependency tree.
- Dependencies updated:
affinidi-data-integrity0.4→0.5,affinidi-secrets-resolver0.5.0→0.5.2 - Internal
ensure_did_key_id()(which mutatedSecretIDs) replaced withvalidate_did_key_vm()(validation only, no mutation) — signers are now required to provide a correctly formatteddid:key:verification method - Added
wiremockdev-dependency for network failure testing - Consolidated duplicate test helpers into shared
test_utilsmodule - Added comprehensive documentation for
resolve(),validate(), implicit services, and witness proof semantics - Added network failure tests (HTTP 404/500, timeout, connection refused, malformed/empty responses)
- Added file I/O error tests for log entry and witness proof loading/saving
- Added unit tests for
LogEntryStateaccessors - Test count: 383 tests (370 unit + 12 integration + 1 doc-test)
- IP addresses rejected in DID URLs per spec (
parse_did_url()andparse_url()) - Resolved DID validated against DID Document
id(Read/Resolve step 6) - DID portability enforced:
idchanges requireportable: trueand previous DID inalsoKnownAs versionTimeordering uses strict greater-than (equal timestamps rejected)- Query parameter mutual exclusivity enforced at parse time (
versionId,versionTime,versionNumber) - Witness
{}and watchers[]correctly treated as "not configured" instead of erroring - Empty arrays for
watchers,nextKeyHashes,updateKeysno longer error in diff calculation Parameters1_0.deactivatedchanged frombooltoOption<bool>for lossless serialization round-trips
resolve()conditionally downloadsdid-witness.jsononly when witnesses are configured (eager_witness_downloadparameter)preludemodule added for convenient imports (use didwebvh_rs::prelude::*)NotFoundandUnsupportedMethoderror variants now carry context strings- All
unwrap()calls in production code replaced with proper error handling - Deduplicated log entry spec implementations via
impl_log_entry_common!macro (~150 lines removed) - Deduplicated resolver helpers (
validate_log_entries(),resolve_witness_proofs()) - Simplified
Parameters::validate()(removed dead code, consolidated TTL validation)
- Benchmark harness:
cargo bench --bench did_benchmarks(Criterion) and nightly benchmarks WebVHURL::to_did_base()helper for DID comparison without query/fragment
- Dependencies updated:
affinidi-data-integrity0.3→0.4,criterion0.5→0.8,ssi0.14→0.15,rand0.9→0.10 - Fixed minimum Rust version badge in README (1.88→1.90)
generate_historyexample uses deterministic timestamps; fixesrand0.10 import- Comprehensive test coverage: 353 tests (340 unit + 12 integration + 1 doc-test) with shared test utilities
- FEATURE: New
createmodule with a library API for programmatic DID creationcreate_did()encapsulates the full DID creation flow (log entry creation, validation, witness signing) without any interactive promptsCreateDIDConfig::builder()provides a fluent builder for constructing the configurationadd_web_also_known_as()andadd_scid_also_known_as()are non-interactive helpers for adding aliases to the DID documentsign_witness_proofs()is a standalone function for signing witness proofs outside of the full creation flow
- IMPROVEMENT: Wizard example updated to delegate core logic to the new library API, reducing code duplication between the library and the example
- MAINTENANCE: Updated downstream dependencies
SSIcrate updated to 0.14
- MAINTENANCE: Tests added for code coverage (@82.67% code coverage)
- MAINTENANCE: Updated downstream dependencies
- FIX: Updated back to
affinid-data-integrity0.3.x after fixing cyclic dependency issue
- FIX: Downgrading
affinidi-data-integrityto 0.2.x due to cyclic dependency issue
- FEATURE: Added the wizard the ability to easily add a did:scid:vh alsoKnownAs
alias
- This matches with making WebVH as reusable as possible, supporting did:web and did:scid in parallel
- MAINTENANCE: Updated
affinidi-data-integrityto 0.3.x release - MAINTENANCE: Updated downstream dependencies
- MAINTENANCE: Updated
affinidi-secrets-resolverto 0.4.x release
- MAINTENANCE: Updated
affinidi-secrets-resolverto 0.3.x release
- MAINTENANCE: Crate dependencies updated
- Removes a lot of the SSI crate dependencies from downstream crates simplifying the build
- IMPROVEMENT: Example
generate_historynow has interactive modecargo run --release --example generate_history -- -i
- IMPROVEMENT:
get_did_document()added toLogEntryStateandLogEntry- Use
get_did_document()to get a full DID Document including implied WebVH Services - Use
get_state()if you want to access the raw un-modified DID Document
- Use
- IMPROVEMENT:
didwebvh-rsis now WASM compile friendly.
- IMPROVEMENT: Wizard will now assist with exporting to did:web format
- The wizard will change DID Document values on your behalf
- It can also add to the did:webvh Document
alsoKnownAsrecords
- IMPROVEMENT: Resolver will now auto add implicit service records to the resolved DID Document where missing (#files and #whois)
- IMPROVEMENT: DID Secrets now stored in the
did-secrets.jsonwhen using the Wizard - IMPROVEMENT: Added X25519 key support for Encryption Keys (DID Doc)
- IMPROVEMENT: SSI Crate moved to a feature flag (
ssi) so it becomes optional - FIX: URL parsing would incorrectly handle trailing slashes on URL Path
- FIX: Wizard would exit with an error when aborting migrating the DID
- MAINTENANCE: Updated crate dependencies
- MAINTENANCE: Tests added for code coverage (@74.27% code coverage)
- IMPROVEMENT: Removed Option on return from
create_log_entry- Will now return an
Errif something wrong internally occurs - Simplifies error handling on the client side
- Will now return an
- IMPROVEMENT: Exporting Secrets Resolver via this crate so it is easier for developers to manage secrets required to manage DIDs
- MAINTENANCE: Bumped crate dependencies to latest versions
- IMPROVEMENT: Added additional notes to the wizard when creating updateKeys and NextKeyHashes so it is clear that it is a loop until you decide you have enough keys
- IMPROVEMENT: Ability to resolve a WebVH DID from the wizard added
- You can still use the example
resolveas well
- You can still use the example
- IMPROVEMENT: Resolver Query parameter
versionNumberimplemented- Allows you to resolve a specific LogEntry version number instead of the full
versionId
- Allows you to resolve a specific LogEntry version number instead of the full
- FIX: DID Deactivation metadata is now stored at the DID level and not LogEntry
- Once a DID has been deactivated, all LogEntries will show the DID as deactivated
- MAINTENANCE: Bumped crate dependencies to latest versions
dialoguerupgraded from 0.11 to 0.12
- MAINTENANCE: More tests added for code coverage (@73.15% code coverage)
- FIX: Issue #5
UpdateKey not propagating to new LogEntry Parameter set when pre-rotation is disabled
- Secondary issue of changing update-key also results in an error
- FIX: Wizard would not reset an existing did.jsonl file for a new DID
- FIX: Parameter method was always being placed in each LogEntry, will now correctly skip if version is the same
- MAINTENANCE: More tests added for code coverage (@66.78% code coverage)
- FIX: Issue #2 Handle when there is no witness proof file on resolution
- FIX: Issue #3
URL Conversions incorrectly including .well-known path for default location
- Added unit test to test for inclusion of default paths in URL conversions
- MAINTENANCE: Updating crate dependencies to latest versions
- MAINTENANCE: Addressing rust-analyzer warnings
- Chaining
if letstatements where multipleifstatements were being used
- Chaining
- RELEASE: Initial release of the
didwebvh-rslibrary.