use super::LogEntry;
use crate::{DIDWebVHError, SCID_HOLDER, log_entry::MetaData, parameters::Parameters};
use affinidi_data_integrity::verification_proof::verify_data;
use chrono::{DateTime, Utc};
use std::{
fs::File,
io::{self, BufRead},
};
use tracing::{debug, warn};
impl LogEntry {
pub(crate) fn load_from_file(file_path: &str) -> Result<Vec<LogEntry>, DIDWebVHError> {
let file = File::open(file_path)
.map_err(|e| DIDWebVHError::LogEntryError(format!("Failed to open log file: {e}")))?;
let buf_reader = io::BufReader::new(file);
let mut entries = Vec::new();
for line in buf_reader.lines() {
match line {
Ok(line) => {
let log_entry: LogEntry = serde_json::from_str(&line).map_err(|e| {
DIDWebVHError::LogEntryError(format!(
"Failed to deserialize log entry: {e}",
))
})?;
entries.push(log_entry);
}
Err(e) => {
return Err(DIDWebVHError::LogEntryError(format!(
"Failed to read line from log file: {e}",
)));
}
}
}
Ok(entries)
}
pub fn verify_log_entry(
&self,
previous_log_entry: Option<&LogEntry>,
previous_parameters: Option<&Parameters>,
previous_meta_data: Option<&MetaData>,
) -> Result<(Parameters, MetaData), DIDWebVHError> {
debug!("Verifiying LogEntry: {}", self.version_id);
let Some(proof) = &self.proof.first() else {
return Err(DIDWebVHError::ValidationError(
"Missing proof in the signed LogEntry!".to_string(),
));
};
let parameters = match self.parameters.validate(previous_parameters) {
Ok(params) => params,
Err(e) => {
return Err(DIDWebVHError::LogEntryError(format!(
"Failed to validate parameters: {e}",
)));
}
};
debug!("Validated parameters: {parameters:#?}");
if !LogEntry::check_signing_key_authorized(
¶meters.active_update_keys,
&proof.verification_method,
) {
warn!(
"Signing key {} is not authorized",
&proof.verification_method
);
return Err(DIDWebVHError::ValidationError(format!(
"Signing key ({}) is not authorized",
&proof.verification_method
)));
}
let verify_doc = LogEntry {
proof: Vec::new(),
..self.clone()
};
let verified = verify_data(&verify_doc, None, proof).map_err(|e| {
DIDWebVHError::LogEntryError(format!("Signature verification failed: {e}"))
})?;
if !verified.verified {
return Err(DIDWebVHError::LogEntryError(
"Signature verification failed".to_string(),
));
}
let mut working_entry = self.clone();
working_entry.proof.clear();
working_entry.verify_version_id(previous_log_entry)?;
self.verify_version_time(previous_log_entry)?;
if previous_log_entry.is_none() {
working_entry.verify_scid()?;
}
let (created, portable, scid) = if let Some(metadata) = previous_meta_data {
(
metadata.created.clone(),
metadata.portable,
metadata.scid.clone(),
)
} else {
(
self.version_time.clone(),
parameters.portable.unwrap_or(false),
parameters.scid.clone().unwrap(),
)
};
debug!("LogEntry {} successfully verified", self.version_id);
Ok((
parameters.clone(),
MetaData {
version_id: self.version_id.clone(),
version_time: self.version_time.clone(),
created,
updated: self.version_time.clone(),
deactivated: parameters.deactivated,
portable,
scid,
watchers: if let Some(Some(watchers)) = parameters.watchers {
Some(watchers)
} else {
None
},
witness: if let Some(Some(witnesses)) = parameters.active_witness {
Some(witnesses)
} else {
None
},
},
))
}
fn check_signing_key_authorized(authorized_keys: &[String], proof_key: &str) -> bool {
if let Some((_, key)) = proof_key.split_once('#') {
authorized_keys.iter().any(|f| f.as_str() == key)
} else {
false
}
}
fn verify_version_id(&mut self, previous: Option<&LogEntry>) -> Result<(), DIDWebVHError> {
let (current_id, current_hash) = self.get_version_id_fields()?;
if let Some(previous) = previous {
let Some((id, _)) = previous.version_id.split_once('-') else {
return Err(DIDWebVHError::ValidationError(format!(
"versionID ({}) doesn't match format <int>-<hash>",
previous.version_id
)));
};
let id = id.parse::<u32>().map_err(|e| {
DIDWebVHError::ValidationError(format!(
"Failed to parse version ID ({id}) as u32: {e}",
))
})?;
if current_id != id + 1 {
return Err(DIDWebVHError::ValidationError(format!(
"Current LogEntry version ID ({current_id}) must be one greater than previous version ID ({id})",
)));
}
self.version_id = previous.version_id.clone();
} else if current_id != 1 {
return Err(DIDWebVHError::ValidationError(format!(
"First LogEntry must have version ID 1, got {current_id}",
)));
} else {
self.version_id = if let Some(scid) = &self.parameters.scid {
scid.to_string()
} else {
return Err(DIDWebVHError::ValidationError(
"First LogEntry must have a valid SCID".to_string(),
));
}
};
let entry_hash = self.generate_log_entry_hash()?;
if entry_hash != current_hash {
return Err(DIDWebVHError::ValidationError(format!(
"Current LogEntry version ID ({current_id}) hash ({current_hash}) does not match calculated hash ({entry_hash})",
)));
}
Ok(())
}
fn verify_version_time(&self, previous: Option<&LogEntry>) -> Result<(), DIDWebVHError> {
let current_time = self.version_time.parse::<DateTime<Utc>>().map_err(|e| {
DIDWebVHError::ValidationError(format!(
"Failed to parse versionTime ({}) as DateTime<Utc>: {}",
self.version_time, e
))
})?;
if current_time > Utc::now() {
return Err(DIDWebVHError::ValidationError(format!(
"versionTime ({}) cannot be in the future",
self.version_time
)));
}
if let Some(previous) = previous {
let previous_time = previous
.version_time
.parse::<DateTime<Utc>>()
.map_err(|e| {
DIDWebVHError::ValidationError(format!(
"Failed to parse previous versionTime ({}) as DateTime<Utc>: {}",
self.version_time, e
))
})?;
if current_time < previous_time {
return Err(DIDWebVHError::ValidationError(format!(
"Current versionTime ({}) must be greater than previous versionTime ({})",
self.version_time, previous.version_time
)));
}
}
Ok(())
}
fn verify_scid(&mut self) -> Result<(), DIDWebVHError> {
self.version_id = SCID_HOLDER.to_string();
let Some(scid) = self.parameters.scid.clone() else {
return Err(DIDWebVHError::ValidationError(
"First LogEntry must have a valid SCID".to_string(),
));
};
let temp = serde_json::to_string(&self).map_err(|e| {
DIDWebVHError::LogEntryError(format!("Failed to serialize log entry: {e}"))
})?;
let scid_entry: LogEntry = serde_json::from_str(&temp.replace(&scid, SCID_HOLDER))
.map_err(|e| {
DIDWebVHError::LogEntryError(format!("Failed to deserialize log entry: {e}"))
})?;
let verify_scid = scid_entry.generate_scid()?;
if scid != verify_scid {
return Err(DIDWebVHError::ValidationError(format!(
"SCID ({scid}) does not match calculated SCID ({verify_scid})",
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::log_entry::LogEntry;
#[test]
fn test_authorized_keys_fail() {
let authorized_keys: Vec<String> = Vec::new();
assert!(!LogEntry::check_signing_key_authorized(
&authorized_keys,
"did:key:z6Mkr46vzpmne5FJTE1TgRHrWkoc5j9Kb1suMYtxkdvgMu15#z6Mkr46vzpmne5FJTE1TgRHrWkoc5j9Kb1suMYtxkdvgMu15"
));
}
#[test]
fn test_authorized_keys_missing_key_id_fail() {
let authorized_keys: Vec<String> = Vec::new();
assert!(!LogEntry::check_signing_key_authorized(
&authorized_keys,
"did:key:z6Mkr46vzpmne5FJTE1TgRHrWkoc5j9Kb1suMYtxkdvgMu15"
));
}
#[test]
fn test_authorized_keys_ok() {
let authorized_keys: Vec<String> =
vec!["z6Mkr46vzpmne5FJTE1TgRHrWkoc5j9Kb1suMYtxkdvgMu15".to_string()];
assert!(LogEntry::check_signing_key_authorized(
&authorized_keys,
"did:key:z6Mkr46vzpmne5FJTE1TgRHrWkoc5j9Kb1suMYtxkdvgMu15#z6Mkr46vzpmne5FJTE1TgRHrWkoc5j9Kb1suMYtxkdvgMu15"
));
}
}