use super::LogEntry;
use crate::{DIDWebVHError, MetaData, SCID_HOLDER};
use affinidi_data_integrity::verification_proof::verify_data;
use chrono::{DateTime, Utc};
use std::{
fs::File,
io::{self, BufRead},
path::Path,
};
impl LogEntry {
fn read_from_json_file<P>(file_path: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(file_path)?;
Ok(io::BufReader::new(file).lines())
}
pub fn get_log_entry_from_file<P>(
file_path: P,
version_id: Option<&str>,
version_number: Option<u32>,
version_time: Option<&DateTime<Utc>>,
) -> Result<(LogEntry, MetaData), DIDWebVHError>
where
P: AsRef<Path>,
{
if let Ok(lines) = LogEntry::read_from_json_file(file_path) {
let mut previous_log_entry: Option<LogEntry> = None;
let mut previous_metadata: Option<MetaData> = None;
for line in lines.map_while(Result::ok) {
let log_entry: LogEntry = serde_json::from_str(&line).map_err(|e| {
DIDWebVHError::LogEntryError(format!("Failed to deserialize log entry: {}", e))
})?;
let current_metadata = match log_entry
.verify_log_entry(previous_log_entry.as_ref(), previous_metadata.as_ref())
{
Ok(metadata) => metadata,
Err(e) => {
if let Some(log_entry) = previous_log_entry {
if let Some(metadata) = previous_metadata {
return Ok((log_entry, metadata));
}
}
return Err(DIDWebVHError::ValidationError(format!(
"No valid LogEntry found! Reason: {}",
e
)));
}
};
if current_metadata.deactivated {
return Ok((log_entry, current_metadata));
}
if let Some(version_id) = version_id {
if current_metadata.version_id == version_id {
return Ok((log_entry, current_metadata));
}
}
if let Some(version_number) = version_number {
if let Some((id, _)) = current_metadata.version_id.split_once('-') {
if let Ok(id) = id.parse::<u32>() {
if id == version_number {
return Ok((log_entry, current_metadata));
}
}
}
}
if let Some(version_time) = version_time {
let current_time: DateTime<Utc> =
current_metadata.version_time.parse().unwrap();
let create_time: DateTime<Utc> = current_metadata.created.parse().unwrap();
if (&create_time < version_time) && (¤t_time > version_time) {
return Ok((log_entry, current_metadata));
}
}
previous_log_entry = Some(log_entry);
previous_metadata = Some(current_metadata);
}
if let Some(log_entry) = previous_log_entry {
if let Some(metadata) = previous_metadata {
if version_id.is_some() || version_number.is_some() {
return Err(DIDWebVHError::NotFound);
}
return Ok((log_entry, metadata));
}
}
Err(DIDWebVHError::ValidationError(
"Empty LogEntry returned for DID".to_string(),
))
} else {
Err(DIDWebVHError::LogEntryError(
"Failed to read log entry from file".to_string(),
))
}
}
pub fn verify_log_entry(
&self,
previous_log_entry: Option<&LogEntry>,
previous_meta_data: Option<&MetaData>,
) -> Result<MetaData, DIDWebVHError> {
let Some(proof) = &self.proof else {
return Err(DIDWebVHError::ValidationError(
"Missing proof in the signed LogEntry!".to_string(),
));
};
let parameters = match self
.parameters
.validate(previous_log_entry.map(|p| &p.parameters))
{
Ok(params) => params,
Err(e) => {
return Err(DIDWebVHError::LogEntryError(format!(
"Failed to validate parameters: {}",
e
)));
}
};
if !LogEntry::check_signing_key_authorized(
¶meters.active_update_keys,
&proof.verification_method,
) {
return Err(DIDWebVHError::ValidationError(format!(
"Signing key ({}) is not authorized",
&proof.verification_method
)));
}
let values = serde_json::to_value(self).map_err(|e| {
DIDWebVHError::LogEntryError(format!("Failed to serialize log entry: {}", e))
})?;
let verified = verify_data(&serde_json::from_value(values).map_err(|e| {
DIDWebVHError::LogEntryError(format!(
"Failed to convert log entry to GenericDocument: {}",
e
))
})?)
.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 = None;
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.unwrap(),
)
};
Ok(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) = LogEntry::get_version_id_fields(&self.version_id)?;
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 ({}) as u32: {}",
id, e
))
})?;
if current_id != id + 1 {
return Err(DIDWebVHError::ValidationError(format!(
"Current LogEntry version ID ({}) must be one greater than previous version ID ({})",
current_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 ({}) hash ({}) does not match calculated hash ({})",
current_id, current_hash, entry_hash
)));
}
Ok(())
}
fn get_version_id_fields(version_id: &str) -> Result<(u32, String), DIDWebVHError> {
let Some((id, hash)) = version_id.split_once('-') else {
return Err(DIDWebVHError::ValidationError(format!(
"versionID ({}) doesn't match format <int>-<hash>",
version_id
)));
};
let id = id.parse::<u32>().map_err(|e| {
DIDWebVHError::ValidationError(format!(
"Failed to parse version ID ({}) as u32: {}",
id, e
))
})?;
Ok((id, hash.to_string()))
}
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 ({}) does not match calculated SCID ({})",
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"
));
}
}