use std::sync::Arc;
use didwebvh_rs::create::{CreateDIDConfig, create_did};
use didwebvh_rs::log_entry::LogEntryMethods;
use didwebvh_rs::parameters::Parameters as WebVHParameters;
use didwebvh_rs::prelude::Secret;
use serde_json::json;
use tracing::{info, warn};
use vta_config::AppConfig;
use vta_keys as keys;
use vta_keys::seed_store::SeedStore;
use vta_keys::seeds::{get_active_seed_id, load_seed_bytes};
use vta_support::contexts;
use vta_support::version_time::next_version_time;
use vti_common::error::AppError;
use vti_common::store::{KeyspaceHandle, Store};
pub const VTA_DID_STORE_KEY: &str = "tee:vta_did";
pub const DID_LOG_STORE_KEY: &str = "tee:did_log";
pub async fn maybe_generate_vta_did(
config: &mut AppConfig,
seed_store: &dyn SeedStore,
store: &Store,
storage_encryption_key: Option<[u8; 32]>,
) -> Result<(), AppError> {
let apply_enc = |ks: KeyspaceHandle| -> KeyspaceHandle {
if let Some(key) = storage_encryption_key {
ks.with_encryption(key)
} else {
ks
}
};
let keys_ks = apply_enc(store.keyspace(vta_keyspaces::KEYS)?);
if let Some(did_bytes) = keys_ks.get_raw(VTA_DID_STORE_KEY).await? {
let stored = String::from_utf8(did_bytes)
.map_err(|e| AppError::Internal(format!("corrupt stored VTA DID: {e}")))?;
match &config.vta_did {
Some(supplied) if supplied != &stored => {
warn!(
stored = %stored,
supplied = %supplied,
"baked config supplied a VTA DID that differs from the \
established stored identity — ignoring the supplied value and \
keeping the stored DID (a later boot must not redirect an \
already-established identity)"
);
}
_ => {
info!(did = %stored, "restored VTA identity from encrypted store");
}
}
config.vta_did = Some(stored);
return Ok(());
}
if let Some(direct) = config.vta_did.clone() {
let inserted = keys_ks
.insert_raw_if_absent(VTA_DID_STORE_KEY, direct.as_bytes().to_vec())
.await?;
if inserted {
info!(did = %direct, "persisted directly-supplied VTA DID as the first-boot identity");
}
return Ok(());
}
let kms_config = match &config.tee.kms {
Some(kms) if kms.vta_did_template.is_some() => kms.clone(),
_ => return Ok(()),
};
let template = kms_config.vta_did_template.as_ref().unwrap();
info!(template = %template, "auto-generating VTA did:webvh identity from template");
let contexts_ks = apply_enc(store.keyspace(vta_keyspaces::CONTEXTS)?);
let active_seed_id = get_active_seed_id(&keys_ks)
.await
.map_err(|e| AppError::Internal(format!("{e}")))?;
let seed = load_seed_bytes(&keys_ks, seed_store, Some(active_seed_id))
.await
.map_err(|e| AppError::Internal(format!("{e}")))?;
let ctx = match contexts::get_context(&contexts_ks, "vta").await? {
Some(ctx) => ctx,
None => contexts::create_context(&contexts_ks, "vta", "VTA Identity")
.await
.map_err(|e| AppError::Internal(format!("failed to create VTA context: {e}")))?,
};
let mut derived = keys::derive_entity_keys(
&seed,
&ctx.base_path,
"VTA signing key",
"VTA key-agreement key",
&keys_ks,
)
.await
.map_err(|e| AppError::Internal(format!("{e}")))?;
let sealed_transfer = keys::derive_sealed_transfer_key(
&seed,
&ctx.base_path,
"VTA sealed-transfer producer-assertion key",
&keys_ks,
)
.await
.map_err(|e| AppError::Internal(format!("{e}")))?;
let signing_pub_mb = derived
.signing_secret
.get_public_keymultibase()
.map_err(|e| AppError::Internal(format!("{e}")))?;
derived.signing_secret.id = format!("did:key:{signing_pub_mb}#{signing_pub_mb}");
let url_str = template_to_url(template)?;
let did_document = build_vta_did_document(&derived, &sealed_transfer, config);
let (next_key_hashes, pre_rotation_keys) =
vta_keys::derivation::derive_pre_rotation_keys(&seed, &ctx.base_path, "VTA", &keys_ks, 1)
.await?;
let parameters = WebVHParameters {
update_keys: Some(Arc::new(vec![derived.signing_pub.clone().into()])),
portable: Some(true),
next_key_hashes: if next_key_hashes.is_empty() {
None
} else {
Some(Arc::new(
next_key_hashes.into_iter().map(Into::into).collect(),
))
},
..Default::default()
};
let create_config = build_genesis_create_config(
&url_str,
derived.signing_secret.clone(),
did_document,
parameters,
)
.await?;
let result = create_did(create_config)
.await
.map_err(|e| AppError::Internal(format!("failed to create DID: {e}")))?;
let final_did = result.did().to_string();
let scid = result
.log_entry()
.get_scid()
.unwrap_or_default()
.to_string();
let log_content = serde_json::to_string(result.log_entry())
.map_err(|e| AppError::Internal(format!("failed to serialize DID log: {e}")))?;
keys::save_entity_key_records(
&final_did,
&derived,
&keys_ks,
Some("vta"),
Some(active_seed_id),
)
.await
.map_err(|e| AppError::Internal(format!("{e}")))?;
keys::save_sealed_transfer_key_record(
&final_did,
&sealed_transfer,
&keys_ks,
Some("vta"),
Some(active_seed_id),
)
.await
.map_err(|e| AppError::Internal(format!("{e}")))?;
for (i, pk) in pre_rotation_keys.iter().enumerate() {
keys::save_key_record(
&keys_ks,
&format!("{final_did}#pre-rotation-{i}"),
&pk.path,
keys::KeyType::Ed25519,
&pk.public_key,
&pk.label,
Some("vta"),
Some(active_seed_id),
)
.await
.map_err(|e| AppError::Internal(format!("{e}")))?;
}
let mut ctx = ctx;
ctx.did = Some(final_did.clone());
ctx.updated_at = chrono::Utc::now();
contexts::store_context(&contexts_ks, &ctx)
.await
.map_err(|e| AppError::Internal(format!("{e}")))?;
keys_ks
.insert_raw(VTA_DID_STORE_KEY, final_did.as_bytes().to_vec())
.await?;
keys_ks
.insert_raw(DID_LOG_STORE_KEY, log_content.as_bytes().to_vec())
.await?;
let bootstrap_ks = store.keyspace(vta_keyspaces::BOOTSTRAP)?;
bootstrap_ks
.insert_raw(DID_LOG_STORE_KEY, log_content.as_bytes().to_vec())
.await?;
store.persist().await?;
info!(
did = %final_did,
scid = %scid,
store_key = DID_LOG_STORE_KEY,
"VTA did:webvh identity auto-generated — retrieve did.jsonl via: \
GET /attestation/did-log or from the bootstrap keyspace key in `store_key`"
);
config.vta_did = Some(final_did);
Ok(())
}
fn build_vta_did_document(
derived: &keys::DerivedEntityKeys,
sealed_transfer: &keys::DerivedSealedTransferKey,
config: &AppConfig,
) -> serde_json::Value {
let mut did_document = json!({
"@context": [
"https://www.w3.org/ns/did/v1",
"https://www.w3.org/ns/cid/v1"
],
"id": "{DID}",
"verificationMethod": [
{
"id": "{DID}#key-0",
"type": "Multikey",
"controller": "{DID}",
"publicKeyMultibase": &derived.signing_pub
},
{
"id": "{DID}#key-1",
"type": "Multikey",
"controller": "{DID}",
"publicKeyMultibase": &derived.ka_pub
},
{
"id": "{DID}#sealed-transfer-0",
"type": "Multikey",
"controller": "{DID}",
"publicKeyMultibase": &sealed_transfer.public_key
}
],
"authentication": ["{DID}#key-0"],
"assertionMethod": ["{DID}#key-0", "{DID}#sealed-transfer-0"],
"keyAgreement": ["{DID}#key-1"]
});
if let Some(ref msg) = config.messaging {
let services = did_document
.as_object_mut()
.unwrap()
.entry("service")
.or_insert_with(|| json!([]));
services.as_array_mut().unwrap().push(json!({
"id": "{DID}#vta-didcomm",
"type": "DIDCommMessaging",
"serviceEndpoint": [{
"accept": ["didcomm/v2"],
"uri": msg.mediator_did
}]
}));
}
let public_url = config
.public_url
.as_deref()
.map(str::trim)
.filter(|u| !u.is_empty());
if config.services.rest
&& let Some(url) = public_url
{
let services = did_document
.as_object_mut()
.unwrap()
.entry("service")
.or_insert_with(|| json!([]));
services.as_array_mut().unwrap().push(json!({
"id": "{DID}#vta-rest",
"type": "VTARest",
"serviceEndpoint": url,
}));
}
if config.tee.embed_in_did
&& let Some(url) = public_url
{
let services = did_document
.as_object_mut()
.unwrap()
.entry("service")
.or_insert_with(|| json!([]));
services.as_array_mut().unwrap().push(json!({
"id": "{DID}#tee-attestation",
"type": "TeeAttestation",
"serviceEndpoint": format!("{}/attestation/report", url.trim_end_matches('/'))
}));
}
did_document
}
fn template_to_url(template: &str) -> Result<String, AppError> {
let rest = template.strip_prefix("did:webvh:{SCID}:").ok_or_else(|| {
AppError::Config(format!(
"vta_did_template must start with 'did:webvh:{{SCID}}:' — got: {template}"
))
})?;
if rest.is_empty() {
return Err(AppError::Config(
"vta_did_template must include a domain after 'did:webvh:{SCID}:'".into(),
));
}
let url_path = rest
.replace("%3A", "\x00")
.replace(':', "/")
.replace('\x00', ":");
Ok(format!("https://{url_path}"))
}
async fn build_genesis_create_config(
url: &str,
authorization_key: Secret,
did_document: serde_json::Value,
parameters: WebVHParameters,
) -> Result<CreateDIDConfig, AppError> {
CreateDIDConfig::builder()
.address(url)
.authorization_key(authorization_key)
.did_document(did_document)
.parameters(parameters)
.version_time(next_version_time(None).await)
.build()
.map_err(|e| AppError::Internal(format!("failed to build DID config: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use didwebvh_rs::DIDWebVHState;
use didwebvh_rs::log_entry::LogEntry;
use didwebvh_rs::log_entry_state::{LogEntryState, LogEntryValidationStatus};
use didwebvh_rs::update::{UpdateDIDConfig, update_did};
#[test]
fn test_template_to_url_simple() {
assert_eq!(
template_to_url("did:webvh:{SCID}:example.com:vta").unwrap(),
"https://example.com/vta"
);
}
#[test]
fn test_template_to_url_nested_path() {
assert_eq!(
template_to_url("did:webvh:{SCID}:example.com:org:agents:vta-1").unwrap(),
"https://example.com/org/agents/vta-1"
);
}
#[test]
fn test_template_to_url_with_port() {
assert_eq!(
template_to_url("did:webvh:{SCID}:example.com%3A8080:vta").unwrap(),
"https://example.com:8080/vta"
);
}
#[test]
fn test_template_to_url_domain_only() {
assert_eq!(
template_to_url("did:webvh:{SCID}:example.com").unwrap(),
"https://example.com"
);
}
#[test]
fn test_template_to_url_invalid_prefix() {
assert!(template_to_url("did:key:z6Mk...").is_err());
}
#[test]
fn test_template_to_url_empty_domain() {
assert!(template_to_url("did:webvh:{SCID}:").is_err());
}
fn genesis_inputs() -> (Secret, serde_json::Value, WebVHParameters) {
let mut signing = Secret::generate_ed25519(None, Some(&[7u8; 32]));
let signing_pub = signing.get_public_keymultibase().unwrap();
signing.id = format!("did:key:{signing_pub}#{signing_pub}");
let document = json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:webvh:{SCID}:example.com:vta",
});
let parameters = WebVHParameters {
update_keys: Some(Arc::new(vec![signing_pub.clone().into()])),
portable: Some(true),
..Default::default()
};
(signing, document, parameters)
}
#[tokio::test]
async fn genesis_version_time_is_stamped_and_not_future() {
let (signing, document, parameters) = genesis_inputs();
let config =
build_genesis_create_config("https://example.com/vta", signing, document, parameters)
.await
.expect("genesis config builds");
let stamped = config
.version_time
.expect("genesis must set an explicit versionTime, not inherit Utc::now()");
let result = create_did(config).await.expect("genesis entry is created");
let entry_time = result.log_entry().get_version_time();
assert_eq!(
entry_time, stamped,
"the log entry must carry the versionTime the config set"
);
let now = chrono::Utc::now().fixed_offset();
assert!(
entry_time <= now,
"genesis must not be future-dated, got {entry_time} against now={now}"
);
assert!(
entry_time > now - chrono::Duration::seconds(5),
"genesis should be essentially now, not backdated, got {entry_time} against now={now}"
);
}
#[tokio::test]
async fn first_update_after_genesis_is_strictly_later() {
let (signing, document, parameters) = genesis_inputs();
let config =
build_genesis_create_config("https://example.com/vta", signing, document, parameters)
.await
.expect("genesis config builds");
let genesis_time = create_did(config)
.await
.expect("genesis entry is created")
.log_entry()
.get_version_time();
let update_time = next_version_time(Some(genesis_time)).await;
assert!(
update_time > genesis_time,
"first update must be strictly after genesis: \
update={update_time}, genesis={genesis_time}"
);
assert_ne!(
update_time.timestamp(),
genesis_time.timestamp(),
"must differ after did:webvh's second-granularity truncation"
);
assert!(
update_time <= chrono::Utc::now().fixed_offset(),
"first update must not be future-dated"
);
}
#[tokio::test]
async fn tee_genesis_then_immediate_update_resolves() {
let (signing, document, parameters) = genesis_inputs();
let config = build_genesis_create_config(
"https://example.com/vta",
signing.clone(),
document,
parameters,
)
.await
.expect("genesis config builds");
let genesis = create_did(config).await.expect("genesis entry is created");
let genesis_entry = genesis.log_entry().clone();
let genesis_time = genesis_entry.get_version_time();
let mut genesis_state = DIDWebVHState::default();
genesis_state.log_entries_mut().push(LogEntryState {
log_entry: genesis_entry.clone(),
version_number: genesis_entry.get_version_id_fields().expect("version id").0,
validation_status: LogEntryValidationStatus::NotValidated,
validated_parameters: WebVHParameters::default(),
});
genesis_state
.validate()
.expect("genesis chain validates")
.assert_complete()
.expect("genesis chain is complete");
let update_config = UpdateDIDConfig::<Secret, Secret>::builder_generic()
.state(genesis_state)
.signing_key(signing)
.version_time(next_version_time(Some(genesis_time)).await)
.build()
.expect("update config builds");
let update = update_did(update_config)
.await
.expect("immediate update is created");
let jsonl = format!(
"{}\n{}\n",
serde_json::to_string(&genesis_entry).expect("serialize genesis"),
serde_json::to_string(update.log_entry()).expect("serialize update"),
);
let mut resolved = DIDWebVHState::default();
for line in jsonl.lines() {
let entry = LogEntry::deserialize_string(line, None).expect("parse serialized entry");
let version_number = entry.get_version_id_fields().expect("version id").0;
resolved.log_entries_mut().push(LogEntryState {
log_entry: entry,
version_number,
validation_status: LogEntryValidationStatus::NotValidated,
validated_parameters: WebVHParameters::default(),
});
}
resolved
.validate()
.expect("TEE genesis and update chain validates")
.assert_complete()
.expect("TEE genesis and update chain is complete");
assert_eq!(resolved.log_entries().len(), 2);
}
}