1use der::{Decode, DecodePem};
2use tracing::{error, info, warn};
3use x509_cert::Certificate;
4
5use crate::error::{Result, SignError};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum RotationStatus {
9 Ok { expires_in_secs: u64 },
10 RenewSoon { expires_in_secs: u64 },
11 Expired,
12}
13
14pub fn parse_certificate(bytes: &[u8]) -> der::Result<Certificate> {
15 if bytes.starts_with(b"-----BEGIN") {
16 Certificate::from_pem(bytes)
17 } else {
18 Certificate::from_der(bytes)
19 }
20}
21
22pub fn check_rotation_needed(cert_der: &[u8], renew_before_secs: u64) -> Result<RotationStatus> {
23 let cert = parse_certificate(cert_der)
24 .map_err(|e| SignError::Config(format!("failed to parse responder certificate: {e}")))?;
25
26 let not_after = cert.tbs_certificate.validity.not_after;
27 let not_after_epoch = time_to_epoch(not_after);
28
29 let now = std::time::SystemTime::now()
30 .duration_since(std::time::UNIX_EPOCH)
31 .unwrap_or_default()
32 .as_secs();
33
34 if now >= not_after_epoch {
35 return Ok(RotationStatus::Expired);
36 }
37
38 let expires_in_secs = not_after_epoch - now;
39
40 if expires_in_secs <= renew_before_secs {
41 Ok(RotationStatus::RenewSoon { expires_in_secs })
42 } else {
43 Ok(RotationStatus::Ok { expires_in_secs })
44 }
45}
46
47pub fn check_and_log_rotation(
48 ca_label: &str,
49 cert_der: &[u8],
50 renew_before_secs: u64,
51) -> Result<RotationStatus> {
52 let status = check_rotation_needed(cert_der, renew_before_secs)?;
53
54 match &status {
55 RotationStatus::Ok { expires_in_secs } => {
56 let days = expires_in_secs / 86400;
57 info!(
58 ca = ca_label,
59 expires_in_days = days,
60 "OCSP signing certificate valid"
61 );
62 }
63 RotationStatus::RenewSoon { expires_in_secs } => {
64 let days = expires_in_secs / 86400;
65 let hours = (expires_in_secs % 86400) / 3600;
66 warn!(
67 ca = ca_label,
68 expires_in_days = days,
69 expires_in_hours = hours,
70 "OCSP signing certificate approaching expiry — rotation needed"
71 );
72 }
73 RotationStatus::Expired => {
74 error!(
75 ca = ca_label,
76 "OCSP signing certificate has EXPIRED — responses will be rejected by clients"
77 );
78 }
79 }
80
81 Ok(status)
82}
83
84pub fn run_rotation_command(ca_label: &str, command: &str) -> std::result::Result<(), String> {
85 info!(ca = ca_label, command, "executing rotation command");
86
87 let output = std::process::Command::new("sh")
88 .arg("-c")
89 .arg(command)
90 .output()
91 .map_err(|e| format!("failed to execute rotation command: {e}"))?;
92
93 if output.status.success() {
94 info!(ca = ca_label, "rotation command completed successfully");
95 Ok(())
96 } else {
97 let stderr = String::from_utf8_lossy(&output.stderr);
98 error!(
99 ca = ca_label,
100 exit_code = output.status.code().unwrap_or(-1),
101 stderr = %stderr,
102 "rotation command failed"
103 );
104 Err(format!(
105 "rotation command exited with {}: {}",
106 output.status,
107 stderr.trim()
108 ))
109 }
110}
111
112pub fn format_cert_info(cert_der: &[u8]) -> std::result::Result<CertInfo, String> {
113 let cert =
114 parse_certificate(cert_der).map_err(|e| format!("failed to parse certificate: {e}"))?;
115
116 let tbs = &cert.tbs_certificate;
117
118 let not_before = time_to_epoch(tbs.validity.not_before);
119 let not_after = time_to_epoch(tbs.validity.not_after);
120 let now = std::time::SystemTime::now()
121 .duration_since(std::time::UNIX_EPOCH)
122 .unwrap_or_default()
123 .as_secs();
124
125 let has_ocsp_signing = tbs
126 .extensions
127 .as_ref()
128 .and_then(|exts| {
129 exts.iter()
130 .find(|ext| ext.extn_id == const_oid::ObjectIdentifier::new_unwrap("2.5.29.37"))
131 })
132 .map(|ext| {
133 ext.extn_value
134 .as_bytes()
135 .windows(9)
136 .any(|w| w == [0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, 0x09])
137 || ext
138 .extn_value
139 .as_bytes()
140 .windows(8)
141 .any(|w| w == [43, 6, 1, 5, 5, 7, 3, 9])
142 })
143 .unwrap_or(false);
144
145 Ok(CertInfo {
146 subject: format!("{}", tbs.subject),
147 issuer: format!("{}", tbs.issuer),
148 not_before,
149 not_after,
150 is_expired: now >= not_after,
151 days_remaining: if now < not_after {
152 (not_after - now) / 86400
153 } else {
154 0
155 },
156 has_ocsp_signing_eku: has_ocsp_signing,
157 })
158}
159
160#[derive(Debug)]
161pub struct CertInfo {
162 pub subject: String,
163 pub issuer: String,
164 pub not_before: u64,
165 pub not_after: u64,
166 pub is_expired: bool,
167 pub days_remaining: u64,
168 pub has_ocsp_signing_eku: bool,
169}
170
171fn time_to_epoch(time: x509_cert::time::Time) -> u64 {
172 let dt = match time {
173 x509_cert::time::Time::UtcTime(t) => t.to_date_time(),
174 x509_cert::time::Time::GeneralTime(t) => t.to_date_time(),
175 };
176 crate::generate::datetime_to_epoch(dt)
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use der::Encode;
183
184 fn build_test_cert(not_before_dt: der::DateTime, not_after_dt: der::DateTime) -> Vec<u8> {
185 use der::asn1::BitString;
186 use spki::AlgorithmIdentifierOwned;
187 use x509_cert::name::RdnSequence;
188 use x509_cert::time::Time;
189
190 let sha256_ecdsa = const_oid::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
191
192 let nb = Time::UtcTime(der::asn1::UtcTime::from_date_time(not_before_dt).unwrap());
193 let na = Time::UtcTime(der::asn1::UtcTime::from_date_time(not_after_dt).unwrap());
194
195 let tbs = x509_cert::TbsCertificate {
196 version: x509_cert::Version::V3,
197 serial_number: x509_cert::serial_number::SerialNumber::new(&[1]).unwrap(),
198 signature: AlgorithmIdentifierOwned {
199 oid: sha256_ecdsa,
200 parameters: None,
201 },
202 issuer: RdnSequence::default(),
203 validity: x509_cert::time::Validity {
204 not_before: nb,
205 not_after: na,
206 },
207 subject: RdnSequence::default(),
208 subject_public_key_info: spki::SubjectPublicKeyInfoOwned {
209 algorithm: AlgorithmIdentifierOwned {
210 oid: sha256_ecdsa,
211 parameters: None,
212 },
213 subject_public_key: BitString::from_bytes(&[0u8; 65]).unwrap(),
214 },
215 issuer_unique_id: None,
216 subject_unique_id: None,
217 extensions: None,
218 };
219
220 let cert = Certificate {
221 tbs_certificate: tbs,
222 signature_algorithm: AlgorithmIdentifierOwned {
223 oid: sha256_ecdsa,
224 parameters: None,
225 },
226 signature: BitString::from_bytes(&[0u8; 64]).unwrap(),
227 };
228
229 cert.to_der().expect("cert encode failed")
230 }
231
232 #[test]
233 fn rotation_check_ok() {
234 let not_before = der::DateTime::new(2026, 1, 1, 0, 0, 0).unwrap();
235 let not_after = der::DateTime::new(2027, 6, 1, 0, 0, 0).unwrap();
236 let cert_der = build_test_cert(not_before, not_after);
237
238 let status = check_rotation_needed(&cert_der, 604800).unwrap();
239 assert!(
240 matches!(status, RotationStatus::Ok { .. }),
241 "cert valid until 2027 should be Ok"
242 );
243 }
244
245 #[test]
246 fn rotation_check_expired() {
247 let not_before = der::DateTime::new(2020, 1, 1, 0, 0, 0).unwrap();
248 let not_after = der::DateTime::new(2021, 1, 1, 0, 0, 0).unwrap();
249 let cert_der = build_test_cert(not_before, not_after);
250
251 let status = check_rotation_needed(&cert_der, 604800).unwrap();
252 assert_eq!(status, RotationStatus::Expired);
253 }
254
255 #[test]
256 fn rotation_check_renew_soon() {
257 let not_before = der::DateTime::new(2026, 1, 1, 0, 0, 0).unwrap();
258 let not_after = der::DateTime::new(2030, 1, 1, 0, 0, 0).unwrap();
259 let cert_der = build_test_cert(not_before, not_after);
260
261 let ten_years_secs = 365 * 24 * 3600 * 10;
263 let status = check_rotation_needed(&cert_der, ten_years_secs).unwrap();
264 assert!(
265 matches!(status, RotationStatus::RenewSoon { .. }),
266 "cert within huge threshold should be RenewSoon, got {status:?}"
267 );
268 }
269}
270
271#[cfg(test)]
272mod regression_tests {
273 use super::*;
274 use base64::Engine;
275
276 #[test]
277 fn pem_and_der_have_identical_rotation_status_and_invalid_cert_fails() {
278 let key = crate::SealKey::EcdsaP256(crate::demo_ecdsa_p256_key());
279 let cert = crate::generate_seal_cert_for_key(&key).unwrap();
280 let encoded = base64::engine::general_purpose::STANDARD.encode(&cert);
281 let lines = encoded
282 .as_bytes()
283 .chunks(64)
284 .map(|line| std::str::from_utf8(line).unwrap())
285 .collect::<Vec<_>>()
286 .join("\n");
287 let pem = format!("-----BEGIN CERTIFICATE-----\n{lines}\n-----END CERTIFICATE-----\n");
288 assert_eq!(
289 check_rotation_needed(&cert, u64::MAX).unwrap(),
290 check_rotation_needed(pem.as_bytes(), u64::MAX).unwrap()
291 );
292 assert!(check_rotation_needed(b"broken", 3600).is_err());
293 }
294}