1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// Certificate Transparency (CT) verification
// RFC 6962 - Certificate Transparency
use crate::Result;
use crate::certificates::parser::CertificateInfo;
use reqwest;
use serde::{Deserialize, Serialize};
use x509_parser::prelude::*;
/// Certificate Transparency verification result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CtVerificationResult {
pub has_sct: bool,
pub sct_count: usize,
pub sct_sources: Vec<SctSource>,
pub compliant: bool,
pub details: Vec<String>,
}
/// SCT (Signed Certificate Timestamp) source
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SctSource {
/// SCT embedded in certificate extension
X509Extension,
/// SCT in TLS extension
TlsExtension,
/// SCT stapled in OCSP response
OcspStapling,
}
/// Certificate Transparency verifier
pub struct CtVerifier {
phone_out: bool,
}
impl CtVerifier {
pub fn new(phone_out: bool) -> Self {
Self { phone_out }
}
/// Verify Certificate Transparency for a certificate
pub async fn verify(&self, cert: &CertificateInfo) -> Result<CtVerificationResult> {
let mut result = CtVerificationResult {
has_sct: false,
sct_count: 0,
sct_sources: Vec::new(),
compliant: false,
details: Vec::new(),
};
// Check for SCT in X.509 certificate extension
if let Some(sct_count) = self.check_x509_sct_extension(cert)? {
result.has_sct = true;
result.sct_count += sct_count;
result.sct_sources.push(SctSource::X509Extension);
result
.details
.push(format!("Found {} SCT(s) in X.509 extension", sct_count));
}
// Check if certificate is in CT logs (requires network access)
if self.phone_out
&& let Ok(in_logs) = self.check_ct_logs(cert).await
{
if in_logs {
result
.details
.push("Certificate found in public CT logs".to_string());
} else {
result
.details
.push("Certificate NOT found in public CT logs".to_string());
}
}
// Determine compliance
// Modern browsers require at least 2 SCTs from different logs
result.compliant = result.sct_count >= 2;
if result.sct_count == 0 {
result
.details
.push("WARNING: No SCTs found - not CT compliant".to_string());
} else if result.sct_count == 1 {
result
.details
.push("WARNING: Only 1 SCT found - browsers require at least 2".to_string());
} else {
result.details.push(format!(
"✓ Certificate is CT compliant ({} SCTs)",
result.sct_count
));
}
Ok(result)
}
/// Check for SCT extension in X.509 certificate
fn check_x509_sct_extension(&self, cert: &CertificateInfo) -> Result<Option<usize>> {
// SCT extension OID: 1.3.6.1.4.1.11129.2.4.2
const SCT_EXTENSION_OID: &str = "1.3.6.1.4.1.11129.2.4.2";
// Parse the raw certificate to check extensions
let (_rem, parsed_cert) = X509Certificate::from_der(&cert.der_bytes)
.map_err(|_| anyhow::anyhow!("Failed to parse certificate"))?;
// Look for SCT extension
for ext in parsed_cert.extensions() {
let oid_str = ext.oid.to_id_string();
if oid_str == SCT_EXTENSION_OID {
// Parse SCT list from extension value
let sct_list = ext.value;
// SCT list format (simplified):
// - 2 bytes: total length
// - N bytes: SCT entries
if sct_list.len() >= 2 {
// Count SCTs (each SCT has specific structure)
// This is a simplified count - in production, should parse full structure
let count = self.count_scts_in_list(sct_list)?;
return Ok(Some(count));
}
}
}
Ok(None)
}
/// Count SCTs in SCT list
fn count_scts_in_list(&self, sct_list: &[u8]) -> Result<usize> {
if sct_list.len() < 2 {
return Ok(0);
}
// Read total length (big-endian u16)
let _total_len = u16::from_be_bytes([sct_list[0], sct_list[1]]);
let mut count = 0;
let mut pos = 2;
// Parse each SCT entry
while pos + 2 <= sct_list.len() {
// Each SCT starts with 2-byte length
if pos + 2 > sct_list.len() {
break;
}
let sct_len = u16::from_be_bytes([sct_list[pos], sct_list[pos + 1]]) as usize;
pos += 2;
if pos + sct_len > sct_list.len() {
break;
}
// Skip SCT data
pos += sct_len;
count += 1;
}
Ok(count)
}
/// Check if certificate appears in public CT logs
async fn check_ct_logs(&self, cert: &CertificateInfo) -> Result<bool> {
if !self.phone_out {
return Ok(false);
}
// Use crt.sh API to check if certificate is logged
// This is a public service that indexes CT logs
let url = format!("https://crt.sh/?q={}&output=json", cert.serial_number);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
match client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
let text = response.text().await?;
// If we get a JSON array back with entries, cert is in CT logs
Ok(text.len() > 2 && text.starts_with('['))
} else {
Ok(false)
}
}
Err(_) => Ok(false),
}
}
/// Check CT policy compliance for different contexts
pub fn check_policy_compliance(
&self,
result: &CtVerificationResult,
validity_months: i64,
) -> CtPolicyCompliance {
// Google CT Policy (Chrome requirement)
// https://github.com/chromium/ct-policy
let required_scts = if validity_months > 39 {
3 // Certificates valid for > 39 months need 3 SCTs
} else {
2 // Certificates valid for ≤ 39 months need 2 SCTs
};
let chrome_compliant = result.sct_count >= required_scts;
// Apple CT Policy (Safari requirement)
// Similar to Google but with some differences
let apple_compliant = result.sct_count >= 2;
CtPolicyCompliance {
chrome_compliant,
safari_compliant: apple_compliant,
required_scts,
actual_scts: result.sct_count,
}
}
}
/// CT Policy compliance status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CtPolicyCompliance {
pub chrome_compliant: bool,
pub safari_compliant: bool,
pub required_scts: usize,
pub actual_scts: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ct_verifier_creation() {
let verifier = CtVerifier::new(false);
assert!(!verifier.phone_out);
}
#[test]
fn test_sct_source() {
let source = SctSource::X509Extension;
assert!(matches!(source, SctSource::X509Extension));
}
#[test]
fn test_policy_compliance() {
let verifier = CtVerifier::new(false);
let result = CtVerificationResult {
has_sct: true,
sct_count: 3,
sct_sources: vec![SctSource::X509Extension],
compliant: true,
details: vec![],
};
let compliance = verifier.check_policy_compliance(&result, 24);
assert!(compliance.chrome_compliant);
assert!(compliance.safari_compliant);
}
}