use crate::{
log_entry::{LogEntry, MetaData},
log_entry_state::{LogEntryState, LogEntryValidationStatus},
parameters::Parameters,
witness::proofs::WitnessProofCollection,
};
use affinidi_data_integrity::DataIntegrityProof;
use affinidi_secrets_resolver::secrets::Secret;
use chrono::Utc;
use serde_json::Value;
use thiserror::Error;
use tracing::debug;
pub mod log_entry;
pub mod log_entry_state;
pub mod parameters;
pub mod resolve;
pub mod url;
pub mod validate;
pub mod witness;
pub const SCID_HOLDER: &str = "{SCID}";
#[derive(Error, Debug)]
pub enum DIDWebVHError {
#[error("DeactivatedError: {0}")]
DeactivatedError(String),
#[error("DIDError: {0}")]
DIDError(String),
#[error("Invalid method identifier: {0}")]
InvalidMethodIdentifier(String),
#[error("LogEntryError: {0}")]
LogEntryError(String),
#[error("DID Query NotFound")]
NotFound,
#[error("NotImplemented: {0}")]
NotImplemented(String),
#[error("ParametersError: {0}")]
ParametersError(String),
#[error("SCIDError: {0}")]
SCIDError(String),
#[error("ServerError: {0}")]
ServerError(String),
#[error("UnsupportedMethod: Must be did:webvh")]
UnsupportedMethod,
#[error("ValidationError: {0}")]
ValidationError(String),
#[error("WitnessProofError: {0}")]
WitnessProofError(String),
}
#[derive(Debug, Default)]
pub struct DIDWebVHState {
pub log_entries: Vec<LogEntryState>,
pub witness_proofs: WitnessProofCollection,
}
impl DIDWebVHState {
pub fn load_log_entries_from_file(&mut self, file_path: &str) -> Result<(), DIDWebVHError> {
for log_entry in LogEntry::load_from_file(file_path)? {
self.log_entries.push(LogEntryState {
log_entry: log_entry.clone(),
metadata: MetaData::default(),
version_number: log_entry.get_version_id_fields()?.0,
validation_status: LogEntryValidationStatus::NotValidated,
validated_parameters: Parameters::default(),
});
}
Ok(())
}
pub fn load_witness_proofs_from_file(&mut self, file_path: &str) {
if let Ok(proofs) = WitnessProofCollection::read_from_file(file_path) {
self.witness_proofs = proofs;
}
}
pub fn create_log_entry(
&mut self,
version_time: Option<String>,
document: &Value,
parameters: &Parameters,
signing_key: &Secret,
) -> Result<Option<&LogEntryState>, DIDWebVHError> {
let now = Utc::now();
if let Some(Some(value)) = ¶meters.update_keys
&& !parameters.deactivated
{
let vm_id = if let Some(key) = value.iter().next() {
["did:key:", key, "#", key].concat()
} else {
return Err(DIDWebVHError::SCIDError(
"No update keys provided in parameters".to_string(),
));
};
if signing_key.id != vm_id {
return Err(DIDWebVHError::SCIDError(format!(
"Secret key ID {} does not match VerificationMethod ID {}",
signing_key.id, vm_id
)));
}
} else if parameters.deactivated {
} else {
return Err(DIDWebVHError::SCIDError(
"No update keys provided in parameters".to_string(),
));
}
let last_log_entry = self.log_entries.last();
let mut log_entry = if let Some(last_log_entry) = last_log_entry {
debug!(
"previous.validated parameters: {:#?}",
last_log_entry.validated_parameters
);
LogEntry {
version_id: last_log_entry.log_entry.version_id.clone(),
version_time: version_time.unwrap_or_else(|| {
Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}),
parameters: last_log_entry.validated_parameters.diff(parameters)?,
state: document.clone(),
proof: Vec::new(),
}
} else {
let mut log_entry = LogEntry {
version_id: SCID_HOLDER.to_string(),
version_time: version_time
.unwrap_or_else(|| now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
parameters: parameters.clone(),
state: document.clone(),
proof: Vec::new(),
};
log_entry.parameters.scid = Some(SCID_HOLDER.to_string());
let scid = log_entry.generate_scid()?;
let le_str = serde_json::to_string(&log_entry).map_err(|e| {
DIDWebVHError::SCIDError(format!(
"Couldn't serialize LogEntry to JSON. Reason: {e}",
))
})?;
serde_json::from_str(&le_str.replace(SCID_HOLDER, &scid)).map_err(|e| {
DIDWebVHError::SCIDError(format!(
"Couldn't deserialize LogEntry from SCID conversion. Reason: {e}",
))
})?
};
let entry_hash = log_entry.generate_log_entry_hash().map_err(|e| {
DIDWebVHError::SCIDError(format!(
"Couldn't generate entryHash for first LogEntry. Reason: {e}",
))
})?;
let (created, scid, portable, validated_parameters) =
if let Some(last_entry) = last_log_entry {
let (current_id, _) = log_entry.get_version_id_fields()?;
log_entry.version_id = [&(current_id + 1).to_string(), "-", &entry_hash].concat();
if let Some(first_entry) = self.log_entries.first() {
let Some(scid) = first_entry.log_entry.parameters.scid.clone() else {
return Err(DIDWebVHError::LogEntryError(
"First LogEntry does not have a SCID!".to_string(),
));
};
(
first_entry.log_entry.version_time.clone(),
scid,
first_entry
.log_entry
.parameters
.portable
.unwrap_or_default(),
log_entry
.parameters
.validate(Some(&last_entry.validated_parameters))?,
)
} else {
return Err(DIDWebVHError::LogEntryError(
"Expected a First LogEntry, but none exist!".to_string(),
));
}
} else {
log_entry.version_id = ["1-", &entry_hash].concat();
let Some(scid) = log_entry.parameters.scid.clone() else {
return Err(DIDWebVHError::LogEntryError(
"First LogEntry does not have a SCID!".to_string(),
));
};
let mut validated_params = log_entry.parameters.clone();
validated_params.active_witness = log_entry.parameters.witness.clone();
(
log_entry.version_time.clone(),
scid,
log_entry.parameters.portable.unwrap_or_default(),
validated_params,
)
};
let proof = DataIntegrityProof::sign_jcs_data(&log_entry, None, signing_key, None)
.map_err(|e| {
DIDWebVHError::SCIDError(format!(
"Couldn't generate Data Integrity Proof for LogEntry. Reason: {e}"
))
})?;
log_entry.proof.push(proof);
let metadata = MetaData {
version_id: log_entry.version_id.clone(),
version_time: log_entry.version_time.clone(),
created,
updated: log_entry.version_time.clone(),
deactivated: parameters.deactivated,
portable,
scid,
watchers: if let Some(Some(watchers)) = ¶meters.watchers {
Some(watchers.clone())
} else {
None
},
witness: if let Some(Some(witnesses)) = ¶meters.active_witness {
Some(witnesses.clone())
} else {
None
},
};
let id_number = log_entry.get_version_id_fields()?.0;
self.log_entries.push(LogEntryState {
log_entry,
metadata,
version_number: id_number,
validation_status: LogEntryValidationStatus::Ok,
validated_parameters,
});
Ok(self.log_entries.last())
}
}
#[cfg(test)]
mod tests {
use crate::parameters::Parameters;
#[test]
fn check_serialization_field_action() {
let watchers = vec!["did:webvh:watcher1".to_string()];
let params = Parameters {
pre_rotation_active: false,
method: None,
scid: None,
update_keys: None,
active_update_keys: Vec::new(),
portable: None,
next_key_hashes: None,
witness: Some(None),
active_witness: Some(None),
watchers: Some(Some(watchers)),
deactivated: false,
ttl: None,
};
let parsed = serde_json::to_value(¶ms).expect("Couldn't parse parameters");
let pretty = serde_json::to_string_pretty(¶ms).expect("Couldn't parse parameters");
println!("Parsed: {pretty}");
assert_eq!(parsed.get("next_key_hashes"), None);
assert!(parsed.get("witness").is_some_and(|s| s.is_null()));
assert!(parsed.get("watchers").is_some_and(|s| s.is_array()));
}
}