use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactType {
CertificateIssuance,
CertificateRevocation,
ThresholdSignature,
ThresholdEncryption,
DirectorRotation,
QuorumPolicy,
DirectorIdentity,
ArchiveRenewal,
}
impl ArtifactType {
pub const fn as_str(self) -> &'static str {
match self {
ArtifactType::CertificateIssuance => "certificate_issuance",
ArtifactType::CertificateRevocation => "certificate_revocation",
ArtifactType::ThresholdSignature => "threshold_signature",
ArtifactType::ThresholdEncryption => "threshold_encryption",
ArtifactType::DirectorRotation => "director_rotation",
ArtifactType::QuorumPolicy => "quorum_policy",
ArtifactType::DirectorIdentity => "director_identity",
ArtifactType::ArchiveRenewal => "archive_renewal",
}
}
pub const ALL: &[ArtifactType] = &[
ArtifactType::CertificateIssuance,
ArtifactType::CertificateRevocation,
ArtifactType::ThresholdSignature,
ArtifactType::ThresholdEncryption,
ArtifactType::DirectorRotation,
ArtifactType::QuorumPolicy,
ArtifactType::DirectorIdentity,
ArtifactType::ArchiveRenewal,
];
}
impl std::fmt::Display for ArtifactType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for ArtifactType {
type Err = UnknownArtifactType;
fn from_str(s: &str) -> Result<Self, Self::Err> {
for variant in ArtifactType::ALL {
if variant.as_str() == s {
return Ok(*variant);
}
}
Err(UnknownArtifactType {
input: s.to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown artifact_type '{input}' (expected one of: {})", ArtifactType::ALL.iter().map(|v| v.as_str()).collect::<Vec<_>>().join(", "))]
pub struct UnknownArtifactType {
input: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MerkleEntry {
pub sequence: u64,
pub timestamp: DateTime<Utc>,
pub artifact_type: ArtifactType,
pub artifact_hash: [u8; 32],
#[serde(default)]
pub metadata: serde_json::Value,
}
impl MerkleEntry {
pub fn new(sequence: u64, artifact_type: ArtifactType, artifact_hash: [u8; 32]) -> Self {
Self {
sequence,
timestamp: Utc::now(),
artifact_type,
artifact_hash,
metadata: serde_json::Value::Null,
}
}
pub fn entry_hash(&self) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(self.sequence.to_le_bytes());
let ts_micros = self.timestamp.timestamp_micros();
hasher.update(ts_micros.to_le_bytes());
hasher.update(self.artifact_hash);
let result = hasher.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&result);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_hash_is_deterministic() {
let e1 = MerkleEntry::new(1, ArtifactType::CertificateIssuance, [0u8; 32]);
let e2 = MerkleEntry::new(1, ArtifactType::CertificateIssuance, [0u8; 32]);
let now = Utc::now();
let mut a = e1;
let mut b = e2;
a.timestamp = now;
b.timestamp = now;
assert_eq!(a.entry_hash(), b.entry_hash());
}
#[test]
fn different_entries_different_hashes() {
let now = Utc::now();
let mut e1 = MerkleEntry::new(1, ArtifactType::CertificateIssuance, [0u8; 32]);
let mut e2 = MerkleEntry::new(2, ArtifactType::CertificateIssuance, [0u8; 32]);
e1.timestamp = now;
e2.timestamp = now;
assert_ne!(e1.entry_hash(), e2.entry_hash());
}
#[test]
fn artifact_type_as_str_roundtrips() {
use std::str::FromStr;
for variant in ArtifactType::ALL {
let s = variant.as_str();
let parsed = ArtifactType::from_str(s).unwrap();
assert_eq!(parsed, *variant);
}
}
#[test]
fn artifact_type_display_matches_as_str() {
for variant in ArtifactType::ALL {
assert_eq!(variant.to_string(), variant.as_str());
}
}
#[test]
fn artifact_type_unknown_string_fails() {
use std::str::FromStr;
let result = ArtifactType::from_str("not_a_real_type");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("not_a_real_type"));
assert!(err.to_string().contains("certificate_issuance"));
}
}