Skip to main content

confium_pki_tc/
ct_log.rs

1//! Certificate Transparency (CT) log integration.
2//!
3//! Submit threshold-signed events to public CT logs for independent
4//! auditability. CT logs (RFC 6962) provide append-only proof of
5//! certificate issuance.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// A CT log entry to be submitted.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CtEntry {
13    /// Certificate or event hash (SHA-256).
14    pub cert_hash_hex: String,
15    /// Issuer name.
16    pub issuer: String,
17    /// Submission timestamp.
18    pub submitted_at: DateTime<Utc>,
19    /// Optional signature (e.g., from threshold key).
20    pub signature_hex: Option<String>,
21}
22
23/// CT log submission status.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum CtSubmissionStatus {
27    Pending,
28    Accepted,
29    Rejected { reason: String },
30    AlreadyIncluded,
31}
32
33/// Result of a CT submission.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CtSubmissionResult {
36    pub entry: CtEntry,
37    pub status: CtSubmissionStatus,
38    pub log_id: String,
39    pub sct_hex: Option<String>, // Signed Certificate Timestamp
40}
41
42/// Configuration for a CT log.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct CtLogConfig {
45    pub log_id: String,
46    pub url: String,
47    pub public_key_hex: String,
48    pub max_entry_size: usize,
49}
50
51/// A CT log client (simplified, mock-friendly).
52pub struct CtClient {
53    pub log: CtLogConfig,
54    pending: std::sync::Mutex<Vec<CtEntry>>,
55}
56
57impl CtClient {
58    pub fn new(log: CtLogConfig) -> Self {
59        Self {
60            log,
61            pending: std::sync::Mutex::new(Vec::new()),
62        }
63    }
64
65    /// Queue an entry for submission.
66    pub fn queue_entry(&self, entry: CtEntry) {
67        self.pending.lock().unwrap().push(entry);
68    }
69
70    /// Process all pending entries. Returns results.
71    pub fn process_pending(&self) -> Vec<CtSubmissionResult> {
72        let mut pending = self.pending.lock().unwrap();
73        let results: Vec<CtSubmissionResult> = pending
74            .drain(..)
75            .map(|entry| {
76                // Simplified: auto-accept if within size limit
77                let status = if entry.cert_hash_hex.len() / 2 <= self.log.max_entry_size {
78                    CtSubmissionStatus::Accepted
79                } else {
80                    CtSubmissionStatus::Rejected {
81                        reason: "too large".into(),
82                    }
83                };
84                let sct = if matches!(status, CtSubmissionStatus::Accepted) {
85                    Some(format!("sct-{}", entry.cert_hash_hex))
86                } else {
87                    None
88                };
89                CtSubmissionResult {
90                    entry,
91                    status,
92                    log_id: self.log.log_id.clone(),
93                    sct_hex: sct,
94                }
95            })
96            .collect();
97        results
98    }
99
100    /// Compute an inclusion proof for an entry (mock).
101    pub fn inclusion_proof(&self, entry_hash: &str) -> Vec<String> {
102        // Real implementation would query the CT log via its API.
103        // Simplified mock proof.
104        vec![format!("proof-{}", entry_hash)]
105    }
106
107    /// Get the number of pending entries.
108    pub fn pending_count(&self) -> usize {
109        self.pending.lock().unwrap().len()
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    fn make_entry(hash: impl Into<String>) -> CtEntry {
118        CtEntry {
119            cert_hash_hex: hash.into(),
120            issuer: "confium-test".into(),
121            submitted_at: Utc::now(),
122            signature_hex: Some("sig".into()),
123        }
124    }
125
126    fn make_log() -> CtLogConfig {
127        CtLogConfig {
128            log_id: "log-1".into(),
129            url: "https://ct.example.com/log".into(),
130            public_key_hex: "pubkey".into(),
131            max_entry_size: 1024,
132        }
133    }
134
135    #[test]
136    fn queue_and_process() {
137        let client = CtClient::new(make_log());
138        client.queue_entry(make_entry("a".repeat(64)));
139        client.queue_entry(make_entry("b".repeat(64)));
140        assert_eq!(client.pending_count(), 2);
141        let results = client.process_pending();
142        assert_eq!(results.len(), 2);
143        assert_eq!(client.pending_count(), 0);
144    }
145
146    #[test]
147    fn accepted_entries_have_sct() {
148        let client = CtClient::new(make_log());
149        client.queue_entry(make_entry("a".repeat(64)));
150        let results = client.process_pending();
151        assert!(results[0].sct_hex.is_some());
152    }
153
154    #[test]
155    fn oversized_entries_rejected() {
156        let client = CtClient::new(make_log());
157        let oversized = "a".repeat(3000);
158        client.queue_entry(make_entry(&oversized));
159        let results = client.process_pending();
160        assert!(matches!(
161            results[0].status,
162            CtSubmissionStatus::Rejected { .. }
163        ));
164    }
165
166    #[test]
167    fn inclusion_proof_generated() {
168        let client = CtClient::new(make_log());
169        let proof = client.inclusion_proof("hash123");
170        assert_eq!(proof.len(), 1);
171        assert!(proof[0].contains("hash123"));
172    }
173
174    #[test]
175    fn entry_serializes() {
176        let entry = make_entry("a".repeat(64));
177        let json = serde_json::to_string(&entry).unwrap();
178        assert!(json.contains("confium-test"));
179    }
180
181    #[test]
182    fn status_serialization() {
183        let status = CtSubmissionStatus::Pending;
184        let json = serde_json::to_string(&status).unwrap();
185        assert_eq!(json, "\"pending\"");
186    }
187}