use tracing::info;
use vta_sdk::webvh::WebvhDidRecord;
use vti_common::error::AppError;
use crate::store::{KeyspaceHandle, Store};
use crate::webvh_store;
use crate::tee::did_autogen::DID_LOG_STORE_KEY as TEE_DID_LOG_STORE_KEY;
pub async fn backfill_serverless_webvh_identity(
store: &Store,
storage_encryption_key: Option<[u8; 32]>,
vta_did: &str,
) -> Result<(), AppError> {
if !vta_did.starts_with("did:webvh:") {
return Ok(());
}
let with_enc = |ks: KeyspaceHandle| match storage_encryption_key {
Some(key) => ks.with_encryption(key),
None => ks,
};
let webvh_ks = with_enc(store.keyspace(crate::keyspaces::WEBVH)?);
let mut persisted = false;
if webvh_store::get_did_log(&webvh_ks, vta_did)
.await?
.is_none()
{
let keys_ks = with_enc(store.keyspace(crate::keyspaces::KEYS)?);
if let Some(bytes) = keys_ks.get_raw(TEE_DID_LOG_STORE_KEY).await? {
let log_content = String::from_utf8(bytes).map_err(|e| {
AppError::Internal(format!("corrupt stored VTA did.jsonl log: {e}"))
})?;
webvh_store::store_did_log(&webvh_ks, vta_did, &log_content).await?;
persisted = true;
}
}
if webvh_store::get_did(&webvh_ks, vta_did).await?.is_none() {
let record = build_serverless_webvh_record(vta_did);
webvh_store::store_did(&webvh_ks, &record).await?;
persisted = true;
}
if persisted {
store.persist().await?;
info!(
did = %vta_did,
"backfilled serverless webvh DID record + log into the webvh keyspace"
);
}
Ok(())
}
fn build_serverless_webvh_record(did: &str) -> WebvhDidRecord {
let scid = did.split(':').nth(2).unwrap_or_default().to_string();
let now = chrono::Utc::now();
WebvhDidRecord {
did: did.to_string(),
server_id: "serverless".to_string(),
mnemonic: String::new(),
scid,
context_id: "vta".to_string(),
portable: true,
log_entry_count: 1,
pre_rotation_count: 1,
next_fragment_id: 2,
created_at: now,
updated_at: now,
}
}
#[cfg(test)]
mod tests {
use super::*;
use didwebvh_rs::log_entry::LogEntryMethods;
use vta_sdk::protocol::matching::ServiceCapabilities;
use vta_tee::did_autogen::maybe_generate_vta_did;
use crate::operations::did_webvh::state_from_jsonl_pub;
use crate::test_support::{TestSeedStore, test_app_config};
const STORAGE_KEY: [u8; 32] = [42; 32];
const PUBLIC_URL: &str = "https://api.example.com:8443/tenant/vta/";
struct AutogenFixture {
_dir: tempfile::TempDir,
store: Store,
config: vta_config::AppConfig,
seed: TestSeedStore,
}
impl AutogenFixture {
async fn new(rest: bool, public_url: Option<&str>, embed: bool) -> Self {
let dir = tempfile::tempdir().unwrap();
let mut config = test_app_config(dir.path().into());
config.services.rest = rest;
config.public_url = public_url.map(str::to_owned);
config.tee.embed_in_did = embed;
config.tee.kms = Some(
serde_json::from_value(serde_json::json!({
"region": "ap-southeast-1",
"key_arn": "unused-in-did-generation",
"vta_did_template": "did:webvh:{SCID}:identity.example.com:logs:vta"
}))
.unwrap(),
);
let store = Store::open(&config.store).unwrap();
let seed = TestSeedStore(vec![7; 64]);
maybe_generate_vta_did(&mut config, &seed, &store, Some(STORAGE_KEY))
.await
.unwrap();
backfill_serverless_webvh_identity(
&store,
Some(STORAGE_KEY),
config.vta_did.as_deref().unwrap(),
)
.await
.unwrap();
Self {
_dir: dir,
store,
config,
seed,
}
}
fn keyspace(&self, name: &str) -> KeyspaceHandle {
self.store
.keyspace(name)
.unwrap()
.with_encryption(STORAGE_KEY)
}
async fn log(&self) -> String {
webvh_store::get_did_log(
&self.keyspace(crate::keyspaces::WEBVH),
self.config.vta_did.as_deref().unwrap(),
)
.await
.unwrap()
.unwrap()
}
}
fn verified_document(log: &str) -> serde_json::Value {
let state = state_from_jsonl_pub(log).expect("SCID and signed chain must validate");
state
.log_entries()
.last()
.unwrap()
.log_entry
.get_state()
.clone()
}
#[tokio::test]
async fn tee_generated_log_supports_bootstrap_rest_discovery() {
for (rest, url, embed, expected) in [
(true, Some(PUBLIC_URL), true, Some(PUBLIC_URL)),
(true, Some(PUBLIC_URL), false, Some(PUBLIC_URL)),
(
true,
Some(" https://api.example.com:8443/vta/ "),
false,
Some("https://api.example.com:8443/vta/"),
),
(
true,
Some(" https://api.example.com:8443/vta/ "),
true,
Some("https://api.example.com:8443/vta/"),
),
(false, Some(PUBLIC_URL), true, None),
(true, None, false, None),
(true, Some(" "), false, None),
] {
let fx = AutogenFixture::new(rest, url, embed).await;
let log = fx.log().await;
let doc = verified_document(&log);
assert_eq!(doc["id"].as_str(), fx.config.vta_did.as_deref());
let endpoint = ServiceCapabilities::from_did_document(&doc).rest;
assert_eq!(endpoint.as_deref(), expected);
if let Some(endpoint) = endpoint {
vta_sdk::http::guard_vta_endpoint(
&endpoint,
vta_sdk::http::EndpointPolicy::public_only(),
)
.unwrap();
}
for svc in doc["service"].as_array().into_iter().flatten() {
let advertised = svc["serviceEndpoint"].as_str().unwrap_or_default();
assert_eq!(advertised.trim(), advertised, "{svc}");
}
for ks in [
fx.keyspace(crate::keyspaces::KEYS),
fx.store.keyspace(crate::keyspaces::BOOTSTRAP).unwrap(),
] {
assert_eq!(
ks.get_raw(TEE_DID_LOG_STORE_KEY).await.unwrap().unwrap(),
log.as_bytes()
);
}
}
}
#[tokio::test]
async fn tee_existing_identity_rest_repair_appends_signed_update() {
use crate::operations::did_webvh::{
UpdateDidWebvhOptions, WebvhAuthLocks, WebvhDeps, update_did_webvh,
};
use affinidi_did_resolver_cache_sdk::{DIDCacheClient, config::DIDCacheConfigBuilder};
use std::sync::Arc;
let mut fx = AutogenFixture::new(true, None, false).await;
let genesis = fx.log().await;
let original = verified_document(&genesis);
assert!(
ServiceCapabilities::from_did_document(&original)
.rest
.is_none()
);
let did = fx.config.vta_did.clone().unwrap();
fx.config.public_url = Some(PUBLIC_URL.into());
maybe_generate_vta_did(&mut fx.config, &fx.seed, &fx.store, Some(STORAGE_KEY))
.await
.unwrap();
assert_eq!(fx.log().await, genesis, "reboot must not rewrite genesis");
assert_eq!(
fx.keyspace(crate::keyspaces::KEYS)
.get_raw(TEE_DID_LOG_STORE_KEY)
.await
.unwrap()
.unwrap(),
genesis.as_bytes(),
);
let resolver = DIDCacheClient::new(DIDCacheConfigBuilder::default().build())
.await
.unwrap();
let bridge = Arc::new(crate::didcomm_bridge::DIDCommBridge::placeholder());
let locks = WebvhAuthLocks::new();
let audit = vta_audit::shared_keyspace_sink(fx.keyspace(crate::keyspaces::AUDIT));
let deps = WebvhDeps {
keys_ks: &fx.keyspace(crate::keyspaces::KEYS),
imported_ks: &fx.keyspace(crate::keyspaces::IMPORTED_SECRETS),
contexts_ks: &fx.keyspace(crate::keyspaces::CONTEXTS),
webvh_ks: &fx.keyspace(crate::keyspaces::WEBVH),
delete_cascade: None,
audit: &audit,
seed_store: &fx.seed,
did_resolver: &resolver,
didcomm_bridge: &bridge,
auth_locks: &locks,
#[cfg(feature = "tsp")]
tsp: None,
};
let patched =
crate::operations::protocol::document::with_rest_service(original.clone(), PUBLIC_URL)
.unwrap();
let result = update_did_webvh(
&deps,
&crate::test_support::super_admin_claims(),
&did,
UpdateDidWebvhOptions {
document: Some(patched),
..Default::default()
},
Some(&did),
"test",
)
.await
.unwrap();
assert!(result.new_version_id.starts_with("2-"));
let updated_log = fx.log().await;
assert_eq!(updated_log.lines().count(), 2);
assert_eq!(updated_log.lines().next(), genesis.lines().next());
let updated = verified_document(&updated_log);
assert_eq!(updated["id"], original["id"]);
assert_eq!(
updated["verificationMethod"],
original["verificationMethod"]
);
assert_eq!(
ServiceCapabilities::from_did_document(&updated)
.rest
.as_deref(),
Some(PUBLIC_URL)
);
backfill_serverless_webvh_identity(&fx.store, Some(STORAGE_KEY), &did)
.await
.unwrap();
assert_eq!(
fx.log().await,
updated_log,
"backfill must not restore the older genesis"
);
}
#[test]
fn record_scid_is_third_colon_segment() {
let record = build_serverless_webvh_record("did:webvh:QmScidValue:example.com:vta");
assert_eq!(record.scid, "QmScidValue");
assert_eq!(record.did, "did:webvh:QmScidValue:example.com:vta");
assert_eq!(record.server_id, "serverless");
assert_eq!(record.next_fragment_id, 2);
assert_eq!(record.pre_rotation_count, 1);
assert_eq!(record.log_entry_count, 1);
assert!(record.portable);
}
}