Skip to main content

confium_transparency/
entry.rs

1//! Transparency log entries.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6
7/// Type of artifact recorded in the transparency log.
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10pub enum ArtifactType {
11    /// Certificate issuance.
12    CertificateIssuance,
13    /// Certificate revocation.
14    CertificateRevocation,
15    /// Threshold signature produced.
16    ThresholdSignature,
17    /// Threshold encryption produced.
18    ThresholdEncryption,
19    /// Quorum committee re-shared.
20    DirectorRotation,
21    /// Quorum policy changed (T, N, predicates).
22    QuorumPolicy,
23    /// Director identity added/removed.
24    DirectorIdentity,
25    /// Archive renewal (re-quorum of long-term archival).
26    ArchiveRenewal,
27}
28
29impl ArtifactType {
30    /// Stable string identifier (snake_case). Round-trips with the
31    /// [`FromStr`](std::str::FromStr) impl below. Used by every
32    /// language binding so there's a single source of truth for the
33    /// variant names.
34    pub const fn as_str(self) -> &'static str {
35        match self {
36            ArtifactType::CertificateIssuance => "certificate_issuance",
37            ArtifactType::CertificateRevocation => "certificate_revocation",
38            ArtifactType::ThresholdSignature => "threshold_signature",
39            ArtifactType::ThresholdEncryption => "threshold_encryption",
40            ArtifactType::DirectorRotation => "director_rotation",
41            ArtifactType::QuorumPolicy => "quorum_policy",
42            ArtifactType::DirectorIdentity => "director_identity",
43            ArtifactType::ArchiveRenewal => "archive_renewal",
44        }
45    }
46
47    /// All variants in declaration order — useful for binding iterators
48    /// and CLI argument completion.
49    pub const ALL: &[ArtifactType] = &[
50        ArtifactType::CertificateIssuance,
51        ArtifactType::CertificateRevocation,
52        ArtifactType::ThresholdSignature,
53        ArtifactType::ThresholdEncryption,
54        ArtifactType::DirectorRotation,
55        ArtifactType::QuorumPolicy,
56        ArtifactType::DirectorIdentity,
57        ArtifactType::ArchiveRenewal,
58    ];
59}
60
61impl std::fmt::Display for ArtifactType {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.write_str(self.as_str())
64    }
65}
66
67impl std::str::FromStr for ArtifactType {
68    type Err = UnknownArtifactType;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        for variant in ArtifactType::ALL {
72            if variant.as_str() == s {
73                return Ok(*variant);
74            }
75        }
76        Err(UnknownArtifactType {
77            input: s.to_string(),
78        })
79    }
80}
81
82/// Error returned by the [`FromStr`](std::str::FromStr) impl on
83/// [`ArtifactType`] when the input doesn't match any known variant.
84#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
85#[error("unknown artifact_type '{input}' (expected one of: {})", ArtifactType::ALL.iter().map(|v| v.as_str()).collect::<Vec<_>>().join(", "))]
86pub struct UnknownArtifactType {
87    input: String,
88}
89
90/// A single entry in the transparency log.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct MerkleEntry {
93    /// Monotonically increasing sequence number.
94    pub sequence: u64,
95    /// When the entry was appended.
96    pub timestamp: DateTime<Utc>,
97    /// Type of artifact.
98    pub artifact_type: ArtifactType,
99    /// SHA-256 hash of the artifact being recorded.
100    pub artifact_hash: [u8; 32],
101    /// Optional deployment-specific metadata (JSON).
102    #[serde(default)]
103    pub metadata: serde_json::Value,
104}
105
106impl MerkleEntry {
107    /// Construct a new entry with the current timestamp.
108    pub fn new(sequence: u64, artifact_type: ArtifactType, artifact_hash: [u8; 32]) -> Self {
109        Self {
110            sequence,
111            timestamp: Utc::now(),
112            artifact_type,
113            artifact_hash,
114            metadata: serde_json::Value::Null,
115        }
116    }
117
118    /// Compute the SHA-256 hash of this entry's contents (sequence + timestamp + artifact_hash).
119    pub fn entry_hash(&self) -> [u8; 32] {
120        let mut hasher = Sha256::new();
121        hasher.update(self.sequence.to_le_bytes());
122        // Encode timestamp as a fixed-size byte sequence
123        let ts_micros = self.timestamp.timestamp_micros();
124        hasher.update(ts_micros.to_le_bytes());
125        hasher.update(self.artifact_hash);
126        let result = hasher.finalize();
127        let mut out = [0u8; 32];
128        out.copy_from_slice(&result);
129        out
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn entry_hash_is_deterministic() {
139        let e1 = MerkleEntry::new(1, ArtifactType::CertificateIssuance, [0u8; 32]);
140        let e2 = MerkleEntry::new(1, ArtifactType::CertificateIssuance, [0u8; 32]);
141        // Same content, but timestamp differs — hash will differ. Test that.
142        // For deterministic hash test, set timestamp explicitly:
143        let now = Utc::now();
144        let mut a = e1;
145        let mut b = e2;
146        a.timestamp = now;
147        b.timestamp = now;
148        assert_eq!(a.entry_hash(), b.entry_hash());
149    }
150
151    #[test]
152    fn different_entries_different_hashes() {
153        let now = Utc::now();
154        let mut e1 = MerkleEntry::new(1, ArtifactType::CertificateIssuance, [0u8; 32]);
155        let mut e2 = MerkleEntry::new(2, ArtifactType::CertificateIssuance, [0u8; 32]);
156        e1.timestamp = now;
157        e2.timestamp = now;
158        assert_ne!(e1.entry_hash(), e2.entry_hash());
159    }
160
161    #[test]
162    fn artifact_type_as_str_roundtrips() {
163        use std::str::FromStr;
164        for variant in ArtifactType::ALL {
165            let s = variant.as_str();
166            let parsed = ArtifactType::from_str(s).unwrap();
167            assert_eq!(parsed, *variant);
168        }
169    }
170
171    #[test]
172    fn artifact_type_display_matches_as_str() {
173        for variant in ArtifactType::ALL {
174            assert_eq!(variant.to_string(), variant.as_str());
175        }
176    }
177
178    #[test]
179    fn artifact_type_unknown_string_fails() {
180        use std::str::FromStr;
181        let result = ArtifactType::from_str("not_a_real_type");
182        assert!(result.is_err());
183        let err = result.unwrap_err();
184        assert!(err.to_string().contains("not_a_real_type"));
185        assert!(err.to_string().contains("certificate_issuance"));
186    }
187}