Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ chrono = { version = "0.4", features = ["serde"] }
rand = "0.10"

# DID Web with Verifiable History
didwebvh-rs = "0.5"
didwebvh-rs = "0.5.6"

# URL parsing
url = "2"
Expand Down
17 changes: 17 additions & 0 deletions vta-service/src/operations/did_webvh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,20 @@ async fn load_key_as_secret(
Ok((secret, record.public_key.clone(), record))
}

/// A synthetic, strictly-increasing, backdated `versionTime` for the VTA's next
/// did:webvh log entry. did:webvh serialises `versionTime` at second granularity
/// and requires each entry to be strictly later than the previous and not in the
/// future; the real wall-clock value is irrelevant for resolution. We backdate a
/// day and space entries a minute apart by their index, so the VTA can create
/// then update its DID back-to-back (e.g. `setup` then `services didcomm enable`)
/// without producing same-second timestamps that serialise identically and make
/// the DID unresolvable. `existing_entry_count` is the number of log entries
/// already in the chain (0 for the genesis entry).
fn backdated_version_time(existing_entry_count: usize) -> chrono::DateTime<chrono::FixedOffset> {
use chrono::Duration;
Utc::now().fixed_offset() - Duration::days(1) + Duration::minutes(existing_entry_count as i64)
}

/// Check whether a DID document (JSON) contains any DIDCommMessaging service.
fn document_has_didcomm_service(doc: &serde_json::Value) -> bool {
doc.get("service")
Expand Down Expand Up @@ -919,6 +933,9 @@ pub async fn create_did_webvh(
.authorization_key(derived.signing_secret.clone())
.did_document(did_document.clone())
.parameters(parameters)
// Backdated genesis timestamp (entry index 0) so a follow-on update in
// the same second doesn't collide — see `backdated_version_time`.
.version_time(backdated_version_time(0))
.build()
.map_err(|e| AppError::Internal(format!("failed to build DID config: {e}")))?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ pub async fn update_did_webvh(
let last_state = state.log_entries().last().ok_or_else(|| {
UpdateDidWebvhError::Library(format!("DID {} has no log entries", record.did))
})?;
// Index for the new entry's backdated versionTime (count already in the chain).
let new_entry_index = state.log_entries().len();

// 4a. Optimistic-concurrency precondition. Check BEFORE key
// derivation / signing so a stale `get → edit → save` cycle
Expand Down Expand Up @@ -203,7 +205,10 @@ pub async fn update_did_webvh(
// 9. Build the library config.
let mut builder = UpdateDIDConfig::<Secret, Secret>::builder_generic()
.state(state)
.signing_key(signing_secret);
.signing_key(signing_secret)
// Backdated, index-spaced timestamp so a back-to-back update doesn't
// collide with the previous entry's second — see `backdated_version_time`.
.version_time(super::super::backdated_version_time(new_entry_index));
if let Some(doc) = new_doc {
builder = builder.document(doc);
let new_keys: Vec<Multibase> = if pre_rotation_active {
Expand Down
Loading