use crate::{
DIDWebVHError, DIDWebVHState, Signer, ensure_object_mut,
log_entry::{LogEntry, LogEntryMethods},
log_entry_state::LogEntryState,
parameters::Parameters,
url::WebVHURL,
witness::{Witnesses, proofs::WitnessProofCollection},
};
use affinidi_data_integrity::DataIntegrityProof;
use affinidi_secrets_resolver::secrets::Secret;
use ahash::HashMap;
use serde_json::{Value, json};
use std::sync::Arc;
use url::Url;
pub struct CreateDIDConfig<A: Signer = Secret, W: Signer = Secret> {
pub address: String,
pub authorization_keys: Vec<A>,
pub did_document: Value,
pub parameters: Parameters,
pub witness_secrets: HashMap<String, W>,
pub also_known_as_web: bool,
pub also_known_as_scid: bool,
}
pub struct CreateDIDConfigBuilder<A: Signer = Secret, W: Signer = Secret> {
address: Option<String>,
authorization_keys: Vec<A>,
did_document: Option<Value>,
parameters: Option<Parameters>,
witness_secrets: HashMap<String, W>,
also_known_as_web: bool,
also_known_as_scid: bool,
}
impl<A: Signer, W: Signer> CreateDIDConfigBuilder<A, W> {
fn new() -> Self {
Self {
address: None,
authorization_keys: Vec::new(),
did_document: None,
parameters: None,
witness_secrets: HashMap::default(),
also_known_as_web: false,
also_known_as_scid: false,
}
}
pub fn address(mut self, address: impl Into<String>) -> Self {
self.address = Some(address.into());
self
}
pub fn authorization_key(mut self, key: A) -> Self {
self.authorization_keys.push(key);
self
}
pub fn authorization_keys(mut self, keys: Vec<A>) -> Self {
self.authorization_keys = keys;
self
}
pub fn did_document(mut self, doc: Value) -> Self {
self.did_document = Some(doc);
self
}
pub fn parameters(mut self, params: Parameters) -> Self {
self.parameters = Some(params);
self
}
pub fn witness_secret(mut self, did: impl Into<String>, secret: W) -> Self {
self.witness_secrets.insert(did.into(), secret);
self
}
pub fn witness_secrets(mut self, secrets: HashMap<String, W>) -> Self {
self.witness_secrets = secrets;
self
}
pub fn also_known_as_web(mut self, enabled: bool) -> Self {
self.also_known_as_web = enabled;
self
}
pub fn also_known_as_scid(mut self, enabled: bool) -> Self {
self.also_known_as_scid = enabled;
self
}
pub fn build(self) -> Result<CreateDIDConfig<A, W>, DIDWebVHError> {
let address = self
.address
.ok_or_else(|| DIDWebVHError::DIDError("address is required".to_string()))?;
if self.authorization_keys.is_empty() {
return Err(DIDWebVHError::LogEntryError(
"At least one authorization key is required".to_string(),
));
}
let did_document = self
.did_document
.ok_or_else(|| DIDWebVHError::DIDError("did_document is required".to_string()))?;
let parameters = self
.parameters
.ok_or_else(|| DIDWebVHError::ParametersError("parameters is required".to_string()))?;
match did_document.get("id") {
Some(Value::String(id)) => {
if !id.contains("{SCID}") && !id.contains("{DID}") {
return Err(DIDWebVHError::DIDError(
"DID document 'id' must contain a '{SCID}' or '{DID}' placeholder \
(e.g. \"did:webvh:{SCID}:example.com\" or \"{DID}\"). \
The placeholder is replaced with the actual identifier during creation."
.to_string(),
));
}
}
Some(_) => {
return Err(DIDWebVHError::DIDError(
"DID document 'id' field must be a string".to_string(),
));
}
None => {
return Err(DIDWebVHError::DIDError(
"DID document must have a top-level 'id' field".to_string(),
));
}
}
Ok(CreateDIDConfig {
address,
authorization_keys: self.authorization_keys,
did_document,
parameters,
witness_secrets: self.witness_secrets,
also_known_as_web: self.also_known_as_web,
also_known_as_scid: self.also_known_as_scid,
})
}
}
impl CreateDIDConfig {
pub fn builder() -> CreateDIDConfigBuilder {
CreateDIDConfigBuilder::new()
}
}
impl<A: Signer, W: Signer> CreateDIDConfig<A, W> {
pub fn builder_generic() -> CreateDIDConfigBuilder<A, W> {
CreateDIDConfigBuilder::new()
}
}
#[derive(Clone, Debug)]
pub struct CreateDIDResult {
pub(crate) did: String,
pub(crate) log_entry: LogEntry,
pub(crate) witness_proofs: WitnessProofCollection,
}
impl CreateDIDResult {
pub fn did(&self) -> &str {
&self.did
}
pub fn log_entry(&self) -> &LogEntry {
&self.log_entry
}
pub fn witness_proofs(&self) -> &WitnessProofCollection {
&self.witness_proofs
}
}
fn validate_did_key_vm(vm: &str) -> Result<(), DIDWebVHError> {
if !vm.starts_with("did:key:") || !vm.contains('#') {
return Err(DIDWebVHError::LogEntryError(format!(
"Signer verification_method '{vm}' must be in 'did:key:{{mb}}#{{mb}}' format"
)));
}
Ok(())
}
pub async fn create_did<A: Signer, W: Signer>(
mut config: CreateDIDConfig<A, W>,
) -> Result<CreateDIDResult, DIDWebVHError> {
let did_url = if config.address.starts_with("did:") {
WebVHURL::parse_did_url(&config.address)?
} else {
let url = Url::parse(&config.address).map_err(|e| {
DIDWebVHError::DIDError(format!("Invalid URL ({}): {e}", config.address))
})?;
WebVHURL::parse_url(&url)?
};
let webvh_did = did_url.to_string();
if config.also_known_as_web {
add_web_also_known_as(&mut config.did_document, &webvh_did)?;
}
if config.also_known_as_scid {
add_scid_also_known_as(&mut config.did_document, &webvh_did)?;
}
replace_did_placeholder(&mut config.did_document, &webvh_did);
for key in &config.authorization_keys {
validate_did_key_vm(key.verification_method())?;
}
let mut didwebvh = DIDWebVHState::default();
let signing_key = config.authorization_keys.first().ok_or_else(|| {
DIDWebVHError::LogEntryError("At least one authorization key is required".to_string())
})?;
let log_entry_state = didwebvh
.create_log_entry(
None, &config.did_document,
&config.parameters,
signing_key,
)
.await?;
log_entry_state.log_entry.verify_log_entry(None, None)?;
let resolved_did =
if let Some(Value::String(id)) = log_entry_state.log_entry.get_state().get("id") {
id.clone()
} else {
webvh_did
};
let log_entry = log_entry_state.log_entry.clone();
let active_witnesses = log_entry_state.get_active_witnesses();
let mut witness_proofs = WitnessProofCollection::default();
sign_witness_proofs(
&mut witness_proofs,
log_entry_state,
&active_witnesses,
&config.witness_secrets,
)
.await?;
Ok(CreateDIDResult {
did: resolved_did,
log_entry,
witness_proofs,
})
}
fn replace_did_placeholder(did_document: &mut Value, did: &str) {
match did_document {
Value::Object(map) => {
for value in map.values_mut() {
replace_did_placeholder(value, did);
}
}
Value::Array(arr) => {
for value in arr.iter_mut() {
replace_did_placeholder(value, did);
}
}
Value::String(s) => {
if s.contains("{DID}") {
*s = s.replace("{DID}", did);
}
}
_ => {}
}
}
pub fn add_web_also_known_as(did_document: &mut Value, did: &str) -> Result<(), DIDWebVHError> {
let did_web_id = DIDWebVHState::convert_webvh_id_to_web_id(did);
let also_known_as = did_document.get_mut("alsoKnownAs");
let Some(also_known_as) = also_known_as else {
ensure_object_mut(did_document)?.insert(
"alsoKnownAs".to_string(),
Value::Array(vec![Value::String(did_web_id.to_string())]),
);
return Ok(());
};
let new_aliases = build_alias_list(also_known_as, &did_web_id)?;
ensure_object_mut(did_document)?.insert("alsoKnownAs".to_string(), Value::Array(new_aliases));
Ok(())
}
pub fn add_scid_also_known_as(did_document: &mut Value, did: &str) -> Result<(), DIDWebVHError> {
let did_scid_id = DIDWebVHState::convert_webvh_id_to_scid_id(did);
let also_known_as = did_document.get_mut("alsoKnownAs");
let Some(also_known_as) = also_known_as else {
ensure_object_mut(did_document)?.insert(
"alsoKnownAs".to_string(),
Value::Array(vec![Value::String(did_scid_id.to_string())]),
);
return Ok(());
};
let new_aliases = build_alias_list(also_known_as, &did_scid_id)?;
ensure_object_mut(did_document)?.insert("alsoKnownAs".to_string(), Value::Array(new_aliases));
Ok(())
}
fn build_alias_list(also_known_as: &Value, new_alias: &str) -> Result<Vec<Value>, DIDWebVHError> {
let mut new_aliases = vec![];
let mut already_exists = false;
if let Some(aliases) = also_known_as.as_array() {
for alias in aliases {
if let Some(alias_str) = alias.as_str() {
if alias_str == new_alias {
already_exists = true;
}
new_aliases.push(alias.clone());
}
}
} else {
return Err(DIDWebVHError::DIDError(
"alsoKnownAs is not an array".to_string(),
));
}
if !already_exists {
new_aliases.push(Value::String(new_alias.to_string()));
}
Ok(new_aliases)
}
pub async fn sign_witness_proofs<W: Signer>(
witness_proofs: &mut WitnessProofCollection,
log_entry: &LogEntryState,
witnesses: &Option<Arc<Witnesses>>,
witness_secrets: &HashMap<String, W>,
) -> Result<bool, DIDWebVHError> {
let Some(witnesses) = witnesses else {
return Ok(false);
};
let (_, witness_nodes) = match &**witnesses {
Witnesses::Value {
threshold,
witnesses,
} => (threshold, witnesses),
_ => {
return Err(DIDWebVHError::WitnessProofError(
"No valid witness parameter config found".to_string(),
));
}
};
for witness in witness_nodes {
let Some(secret) = witness_secrets.get(witness.id.as_str()) else {
return Err(DIDWebVHError::WitnessProofError(format!(
"Couldn't find secret for witness ({})",
witness.id
)));
};
validate_did_key_vm(secret.verification_method())?;
let proof = DataIntegrityProof::sign_jcs_data(
&json!({"versionId": log_entry.get_version_id()}),
None,
secret,
None,
)
.await
.map_err(|e| {
DIDWebVHError::SCIDError(format!(
"Couldn't generate Data Integrity Proof for LogEntry. Reason: {e}",
))
})?;
witness_proofs
.add_proof(log_entry.get_version_id(), &proof, false)
.map_err(|e| DIDWebVHError::WitnessProofError(format!("Error adding proof: {e}")))?;
}
witness_proofs.write_optimise_records()?;
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DIDWebVHState, Multibase, witness::Witness};
use affinidi_secrets_resolver::secrets::Secret;
use serde_json::json;
use std::sync::Arc;
use crate::test_utils::{did_doc_with_key, key_and_params};
async fn create_log_entry_state(key: &Secret, params: &Parameters) -> (DIDWebVHState, String) {
let mut state = DIDWebVHState::default();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", key);
state
.create_log_entry(None, &doc, params, key)
.await
.expect("Failed to create log entry");
let version_id = state
.log_entries
.last()
.unwrap()
.get_version_id()
.to_string();
(state, version_id)
}
#[test]
fn builder_missing_address() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let result = CreateDIDConfig::builder()
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build();
assert!(result.is_err());
}
#[test]
fn builder_missing_authorization_keys() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let result = CreateDIDConfig::builder()
.address("https://example.com/")
.did_document(doc)
.parameters(params)
.build();
assert!(result.is_err());
}
#[test]
fn builder_missing_did_document() {
let (key, params) = key_and_params();
let result = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.parameters(params)
.build();
assert!(result.is_err());
}
#[test]
fn builder_missing_parameters() {
let (key, _) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let result = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.build();
assert!(result.is_err());
}
#[test]
fn builder_all_required_fields() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let result = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build();
assert!(result.is_ok());
}
#[test]
fn builder_authorization_keys_replaces() {
let key1 = crate::test_utils::generate_signing_key();
let key2 = crate::test_utils::generate_signing_key();
let (_, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key2);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key1)
.authorization_keys(vec![key2])
.did_document(doc)
.parameters(params)
.build()
.unwrap();
assert_eq!(config.authorization_keys.len(), 1);
}
#[test]
fn builder_witness_secrets() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let witness_key = crate::test_utils::generate_signing_key();
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.witness_secret("did:key:z6Mk1", witness_key)
.build()
.unwrap();
assert_eq!(config.witness_secrets.len(), 1);
assert!(config.witness_secrets.contains_key("did:key:z6Mk1"));
}
#[test]
fn builder_defaults() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
assert!(!config.also_known_as_web);
assert!(!config.also_known_as_scid);
assert!(config.witness_secrets.is_empty());
}
#[tokio::test]
async fn create_did_with_url_address() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await;
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.did.starts_with("did:webvh:"));
assert!(result.did.contains("example.com"));
assert!(!result.did.contains("{SCID}"));
}
#[tokio::test]
async fn create_did_with_did_address() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("did:webvh:{SCID}:example.com")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await;
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.did.starts_with("did:webvh:"));
assert!(!result.did.contains("{SCID}"));
}
#[tokio::test]
async fn create_did_invalid_address() {
let (key, _params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config: CreateDIDConfig = CreateDIDConfig {
address: "not a valid url or did".to_string(),
authorization_keys: vec![key],
did_document: doc,
parameters: _params,
witness_secrets: HashMap::default(),
also_known_as_web: false,
also_known_as_scid: false,
};
assert!(create_did(config).await.is_err());
}
#[tokio::test]
async fn create_did_no_update_keys() {
let key = crate::test_utils::generate_signing_key();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let params = Parameters::default();
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
assert!(create_did(config).await.is_err());
}
#[tokio::test]
async fn create_did_with_also_known_as_web() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.also_known_as_web(true)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
let state = result.log_entry.get_state();
let also_known_as = state.get("alsoKnownAs").unwrap().as_array().unwrap();
assert!(
also_known_as
.iter()
.any(|v| { v.as_str().is_some_and(|s| s.starts_with("did:web:")) })
);
}
#[tokio::test]
async fn create_did_with_also_known_as_scid() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.also_known_as_scid(true)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
let state = result.log_entry.get_state();
let also_known_as = state.get("alsoKnownAs").unwrap().as_array().unwrap();
assert!(
also_known_as
.iter()
.any(|v| { v.as_str().is_some_and(|s| s.starts_with("did:scid:vh:")) })
);
}
#[tokio::test]
async fn create_did_with_both_aliases() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.also_known_as_web(true)
.also_known_as_scid(true)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
let state = result.log_entry.get_state();
let also_known_as = state.get("alsoKnownAs").unwrap().as_array().unwrap();
let has_web = also_known_as
.iter()
.any(|v| v.as_str().is_some_and(|s| s.starts_with("did:web:")));
let has_scid = also_known_as
.iter()
.any(|v| v.as_str().is_some_and(|s| s.starts_with("did:scid:vh:")));
assert!(has_web);
assert!(has_scid);
}
#[tokio::test]
async fn create_did_no_witnesses_returns_empty_proofs() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
assert_eq!(result.witness_proofs.get_total_count(), 0);
}
#[tokio::test]
async fn create_did_with_witnesses() {
let (key, _) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let witness1 = crate::test_utils::generate_signing_key();
let witness2 = crate::test_utils::generate_signing_key();
let w1_id = witness1.get_public_keymultibase().unwrap();
let w2_id = witness2.get_public_keymultibase().unwrap();
let params = Parameters {
update_keys: Some(Arc::new(vec![Multibase::new(
key.get_public_keymultibase().unwrap(),
)])),
witness: Some(Arc::new(Witnesses::Value {
threshold: 1,
witnesses: vec![
Witness {
id: Multibase::new(w1_id.clone()),
},
Witness {
id: Multibase::new(w2_id.clone()),
},
],
})),
..Default::default()
};
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.witness_secret(w1_id, witness1)
.witness_secret(w2_id, witness2)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
assert_eq!(result.witness_proofs.get_total_count(), 2);
}
#[tokio::test]
async fn create_did_witnesses_missing_secret() {
let (key, _) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let witness1 = crate::test_utils::generate_signing_key();
let w1_id = witness1.get_public_keymultibase().unwrap();
let params = Parameters {
update_keys: Some(Arc::new(vec![Multibase::new(
key.get_public_keymultibase().unwrap(),
)])),
witness: Some(Arc::new(Witnesses::Value {
threshold: 1,
witnesses: vec![Witness {
id: Multibase::new(w1_id),
}],
})),
..Default::default()
};
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
assert!(create_did(config).await.is_err());
}
#[tokio::test]
async fn create_did_portable() {
let (key, _) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let params = Parameters {
update_keys: Some(Arc::new(vec![Multibase::new(
key.get_public_keymultibase().unwrap(),
)])),
portable: Some(true),
..Default::default()
};
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
assert!(result.did.starts_with("did:webvh:"));
}
#[tokio::test]
async fn create_did_log_entry_serializable() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
let json = serde_json::to_string(&result.log_entry);
assert!(json.is_ok());
assert!(!json.unwrap().is_empty());
}
#[test]
fn add_web_also_known_as_no_existing() {
let mut doc = json!({"id": "did:webvh:abc123:example.com"});
add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 1);
assert_eq!(aliases[0].as_str().unwrap(), "did:web:example.com");
}
#[test]
fn add_web_also_known_as_with_existing() {
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": ["did:example:other"]
});
add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 2);
assert!(
aliases
.iter()
.any(|v| v.as_str() == Some("did:example:other"))
);
assert!(
aliases
.iter()
.any(|v| v.as_str() == Some("did:web:example.com"))
);
}
#[test]
fn add_web_also_known_as_already_present() {
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": ["did:web:example.com"]
});
add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 1);
assert_eq!(aliases[0].as_str().unwrap(), "did:web:example.com");
}
#[test]
fn add_web_also_known_as_not_array() {
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": "not an array"
});
assert!(add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").is_err());
}
#[test]
fn add_scid_also_known_as_no_existing() {
let mut doc = json!({"id": "did:webvh:abc123:example.com"});
add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 1);
assert!(aliases[0].as_str().unwrap().starts_with("did:scid:vh:1:"));
}
#[test]
fn add_scid_also_known_as_with_existing() {
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": ["did:example:other"]
});
add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 2);
assert!(
aliases
.iter()
.any(|v| v.as_str() == Some("did:example:other"))
);
assert!(
aliases
.iter()
.any(|v| { v.as_str().is_some_and(|s| s.starts_with("did:scid:vh:1:")) })
);
}
#[test]
fn add_scid_also_known_as_already_present() {
let scid_id = DIDWebVHState::convert_webvh_id_to_scid_id("did:webvh:abc123:example.com");
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": [scid_id]
});
add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 1);
}
#[test]
fn add_scid_also_known_as_not_array() {
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": 42
});
assert!(add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").is_err());
}
#[tokio::test]
async fn sign_witness_proofs_no_witnesses() {
let (key, params) = key_and_params();
let (state, _) = create_log_entry_state(&key, ¶ms).await;
let log_entry = state.log_entries.last().unwrap();
let mut proofs = WitnessProofCollection::default();
let result = sign_witness_proofs(
&mut proofs,
log_entry,
&None,
&HashMap::<String, Secret>::default(),
)
.await;
assert!(result.is_ok());
assert!(!result.unwrap()); assert_eq!(proofs.get_total_count(), 0);
}
#[tokio::test]
async fn sign_witness_proofs_with_witnesses() {
let (key, _) = key_and_params();
let witness1 = crate::test_utils::generate_signing_key();
let witness2 = crate::test_utils::generate_signing_key();
let w1_id = witness1.get_public_keymultibase().unwrap();
let w2_id = witness2.get_public_keymultibase().unwrap();
let params = Parameters {
update_keys: Some(Arc::new(vec![Multibase::new(
key.get_public_keymultibase().unwrap(),
)])),
witness: Some(Arc::new(Witnesses::Value {
threshold: 1,
witnesses: vec![
Witness {
id: Multibase::new(w1_id.clone()),
},
Witness {
id: Multibase::new(w2_id.clone()),
},
],
})),
..Default::default()
};
let (state, version_id) = create_log_entry_state(&key, ¶ms).await;
let log_entry = state.log_entries.last().unwrap();
let mut secrets = HashMap::default();
secrets.insert(w1_id, witness1);
secrets.insert(w2_id, witness2);
let witnesses = log_entry.get_active_witnesses();
let mut proofs = WitnessProofCollection::default();
let result = sign_witness_proofs(&mut proofs, log_entry, &witnesses, &secrets).await;
assert!(result.is_ok());
assert!(result.unwrap()); assert_eq!(proofs.get_proof_count(&version_id), 2);
}
#[tokio::test]
async fn sign_witness_proofs_missing_secret() {
let (key, _) = key_and_params();
let witness1 = crate::test_utils::generate_signing_key();
let w1_id = witness1.get_public_keymultibase().unwrap();
let params = Parameters {
update_keys: Some(Arc::new(vec![Multibase::new(
key.get_public_keymultibase().unwrap(),
)])),
witness: Some(Arc::new(Witnesses::Value {
threshold: 1,
witnesses: vec![Witness {
id: Multibase::new(w1_id),
}],
})),
..Default::default()
};
let (state, _) = create_log_entry_state(&key, ¶ms).await;
let log_entry = state.log_entries.last().unwrap();
let witnesses = log_entry.get_active_witnesses();
let mut proofs = WitnessProofCollection::default();
let result = sign_witness_proofs(
&mut proofs,
log_entry,
&witnesses,
&HashMap::<String, Secret>::default(),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn sign_witness_proofs_empty_witnesses_config() {
let (key, params) = key_and_params();
let (state, _) = create_log_entry_state(&key, ¶ms).await;
let log_entry = state.log_entries.last().unwrap();
let witnesses = Some(Arc::new(Witnesses::Empty {}));
let mut proofs = WitnessProofCollection::default();
let result = sign_witness_proofs(
&mut proofs,
log_entry,
&witnesses,
&HashMap::<String, Secret>::default(),
)
.await;
assert!(result.is_err());
}
#[test]
fn validate_did_key_vm_accepts_valid() {
let vm = "did:key:z6MkTest#z6MkTest";
assert!(validate_did_key_vm(vm).is_ok());
}
#[test]
fn validate_did_key_vm_rejects_missing_hash() {
let vm = "did:key:z6MkTest";
assert!(validate_did_key_vm(vm).is_err());
}
#[test]
fn validate_did_key_vm_rejects_wrong_prefix() {
let vm = "did:web:example.com#key-0";
assert!(validate_did_key_vm(vm).is_err());
}
#[test]
fn validate_did_key_vm_rejects_empty() {
assert!(validate_did_key_vm("").is_err());
}
#[test]
fn builder_also_known_as_flags() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.also_known_as_web(true)
.also_known_as_scid(true)
.build()
.unwrap();
assert!(config.also_known_as_web);
assert!(config.also_known_as_scid);
}
#[test]
fn builder_multiple_authorization_keys_accumulate() {
let key1 = crate::test_utils::generate_signing_key();
let key2 = crate::test_utils::generate_signing_key();
let (_, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key1);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key1)
.authorization_key(key2)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
assert_eq!(config.authorization_keys.len(), 2);
}
#[test]
fn builder_witness_secrets_bulk_replaces() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let w1 = crate::test_utils::generate_signing_key();
let w2 = crate::test_utils::generate_signing_key();
let mut bulk = HashMap::default();
bulk.insert("did:key:z6MkBulk".to_string(), w2);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.witness_secret("did:key:z6MkSingle", w1)
.witness_secrets(bulk)
.build()
.unwrap();
assert_eq!(config.witness_secrets.len(), 1);
assert!(config.witness_secrets.contains_key("did:key:z6MkBulk"));
assert!(!config.witness_secrets.contains_key("did:key:z6MkSingle"));
}
#[tokio::test]
async fn create_did_key_with_existing_did_key_id() {
let mut key = crate::test_utils::generate_signing_key();
let pub_mb = key.get_public_keymultibase().unwrap();
key.id = format!("did:key:{pub_mb}#{pub_mb}");
let params = Parameters {
update_keys: Some(Arc::new(vec![Multibase::new(pub_mb)])),
..Default::default()
};
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn create_did_state_has_no_scid_placeholder() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
let state_str = serde_json::to_string(result.log_entry.get_state()).unwrap();
assert!(!state_str.contains("{SCID}"));
assert!(!result.did.contains("{SCID}"));
}
#[tokio::test]
async fn create_did_log_entry_has_proof() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
assert!(!result.log_entry.get_proofs().is_empty());
}
#[tokio::test]
async fn create_did_version_id_starts_with_one() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
let version_id = result.log_entry.get_version_id();
assert!(version_id.starts_with("1-"));
}
#[tokio::test]
async fn create_did_with_url_path() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com:dids:alice", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/dids/alice/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
assert!(result.did.starts_with("did:webvh:"));
assert!(result.did.contains("example.com"));
}
#[tokio::test]
async fn create_did_result_did_matches_state_id() {
let (key, params) = key_and_params();
let doc = did_doc_with_key("did:webvh:{SCID}:example.com", &key);
let config = CreateDIDConfig::builder()
.address("https://example.com/")
.authorization_key(key)
.did_document(doc)
.parameters(params)
.build()
.unwrap();
let result = create_did(config).await.unwrap();
let state_id = result
.log_entry
.get_state()
.get("id")
.unwrap()
.as_str()
.unwrap();
assert_eq!(result.did, state_id);
}
#[test]
fn add_web_also_known_as_empty_array() {
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": []
});
add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 1);
assert_eq!(aliases[0].as_str().unwrap(), "did:web:example.com");
}
#[test]
fn add_web_also_known_as_idempotent() {
let mut doc = json!({"id": "did:webvh:abc123:example.com"});
add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 1);
assert_eq!(aliases[0].as_str().unwrap(), "did:web:example.com");
}
#[test]
fn add_web_also_known_as_preserves_all_existing() {
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": ["did:example:a", "did:example:b", "did:web:example.com"]
});
add_web_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 3);
assert!(aliases.iter().any(|v| v.as_str() == Some("did:example:a")));
assert!(aliases.iter().any(|v| v.as_str() == Some("did:example:b")));
assert!(
aliases
.iter()
.any(|v| v.as_str() == Some("did:web:example.com"))
);
}
#[test]
fn add_scid_also_known_as_empty_array() {
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": []
});
add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 1);
assert!(aliases[0].as_str().unwrap().starts_with("did:scid:vh:1:"));
}
#[test]
fn add_scid_also_known_as_idempotent() {
let mut doc = json!({"id": "did:webvh:abc123:example.com"});
add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 1);
}
#[test]
fn add_scid_also_known_as_preserves_all_existing() {
let scid_id = DIDWebVHState::convert_webvh_id_to_scid_id("did:webvh:abc123:example.com");
let mut doc = json!({
"id": "did:webvh:abc123:example.com",
"alsoKnownAs": ["did:example:a", "did:example:b", scid_id]
});
add_scid_also_known_as(&mut doc, "did:webvh:abc123:example.com").unwrap();
let aliases = doc.get("alsoKnownAs").unwrap().as_array().unwrap();
assert_eq!(aliases.len(), 3);
assert!(aliases.iter().any(|v| v.as_str() == Some("did:example:a")));
assert!(aliases.iter().any(|v| v.as_str() == Some("did:example:b")));
}
#[tokio::test]
async fn sign_witness_proofs_are_verifiable() {
let (key, _) = key_and_params();
let witness1 = crate::test_utils::generate_signing_key();
let w1_id = witness1.get_public_keymultibase().unwrap();
let params = Parameters {
update_keys: Some(Arc::new(vec![Multibase::new(
key.get_public_keymultibase().unwrap(),
)])),
witness: Some(Arc::new(Witnesses::Value {
threshold: 1,
witnesses: vec![Witness {
id: Multibase::new(w1_id.clone()),
}],
})),
..Default::default()
};
let (state, version_id) = create_log_entry_state(&key, ¶ms).await;
let log_entry_state = state.log_entries.last().unwrap();
let mut secrets = HashMap::default();
secrets.insert(w1_id, witness1);
let witnesses = log_entry_state.get_active_witnesses();
let mut proofs = WitnessProofCollection::default();
sign_witness_proofs(&mut proofs, log_entry_state, &witnesses, &secrets)
.await
.unwrap();
let witness_proof = proofs.get_proofs(&version_id).unwrap();
let validation = log_entry_state
.log_entry
.validate_witness_proof(witness_proof.proof.first().unwrap());
assert!(validation.is_ok());
}
#[tokio::test]
async fn sign_witness_proofs_returns_true_with_witnesses() {
let (key, _) = key_and_params();
let witness1 = crate::test_utils::generate_signing_key();
let w1_id = witness1.get_public_keymultibase().unwrap();
let params = Parameters {
update_keys: Some(Arc::new(vec![Multibase::new(
key.get_public_keymultibase().unwrap(),
)])),
witness: Some(Arc::new(Witnesses::Value {
threshold: 1,
witnesses: vec![Witness {
id: Multibase::new(w1_id.clone()),
}],
})),
..Default::default()
};
let (state, _) = create_log_entry_state(&key, ¶ms).await;
let log_entry_state = state.log_entries.last().unwrap();
let mut secrets = HashMap::default();
secrets.insert(w1_id, witness1);
let witnesses = log_entry_state.get_active_witnesses();
let mut proofs = WitnessProofCollection::default();
let signed = sign_witness_proofs(&mut proofs, log_entry_state, &witnesses, &secrets)
.await
.unwrap();
assert!(signed);
}
#[tokio::test]
async fn sign_witness_proofs_returns_false_no_witnesses() {
let (key, params) = key_and_params();
let (state, _) = create_log_entry_state(&key, ¶ms).await;
let log_entry = state.log_entries.last().unwrap();
let mut proofs = WitnessProofCollection::default();
let signed = sign_witness_proofs(
&mut proofs,
log_entry,
&None,
&HashMap::<String, Secret>::default(),
)
.await
.unwrap();
assert!(!signed);
}
#[test]
fn replace_did_placeholder_replaces_all_occurrences() {
let did = "did:webvh:abc:example.com".to_string();
let mut did_document = json!({
"id": "{DID}",
"@context": ["https://www.w3.org/ns/did/v1"],
"verificationMethod": [{
"id": "{DID}#key-0",
"type": "Multikey",
"publicKeyMultibase": "abcd",
"controller": "{DID}"
}],
"authentication": ["{DID}#key-0"],
"assertionMethod": ["{DID}#key-0"],
});
let expected_document = json!({
"id": "did:webvh:abc:example.com",
"@context": ["https://www.w3.org/ns/did/v1"],
"verificationMethod": [{
"id": "did:webvh:abc:example.com#key-0",
"type": "Multikey",
"publicKeyMultibase": "abcd",
"controller": did
}],
"authentication": ["did:webvh:abc:example.com#key-0"],
"assertionMethod": ["did:webvh:abc:example.com#key-0"],
});
replace_did_placeholder(&mut did_document, &did);
assert_eq!(did_document, expected_document);
}
#[test]
fn replace_did_placeholder_no_op() {
let did = "did:webvh:abc:example.com".to_string();
let mut did_document = json!({
"a": 1,
"b": {
"c": null
}
});
let expected_document = json!({
"a": 1,
"b": {
"c": null
}
});
replace_did_placeholder(&mut did_document, &did);
assert_eq!(did_document, expected_document);
}
}