Skip to main content

hoike_sign/
live.rs

1//! On-demand OCSP response signing with a client-supplied nonce.
2//!
3//! Used by `nonce_policy = "live"` on signer/combined nodes. The signer
4//! already has the certificate's status (from the loaded bundle) and the
5//! signing key — it re-signs a fresh response with the nonce embedded,
6//! without round-tripping to the CA.
7
8use der::asn1::OctetString;
9use der::{Decode, Encode};
10use sha1::Sha1;
11use sha2::Digest;
12use signature::Signer;
13use spki::{DynSignatureAlgorithmIdentifier, SignatureBitStringEncoding};
14use x509_ocsp::builder::OcspResponseBuilder;
15use x509_ocsp::{
16    BasicOcspResponse, CertId, CertStatus, OcspGeneralizedTime, OcspResponse, ResponderId,
17    SingleResponse, ext::Nonce,
18};
19
20use crate::error::{Result, SignError};
21use crate::generate::ocsp_time;
22use x509_cert::Certificate as CertificateForLive;
23
24/// Certificate status for live signing.
25#[derive(Debug, Clone)]
26pub enum LiveCertStatus {
27    Good,
28    Unknown,
29    Revoked {
30        revocation_time: u64,
31        reason: Option<x509_cert::ext::pkix::CrlReason>,
32    },
33}
34
35/// Sign a fresh OCSP response on demand with the client's nonce embedded.
36///
37/// This is the hot path for `nonce_policy = "live"`. It builds a
38/// `BasicOCSPResponse` containing one `SingleResponse` for the requested
39/// CertID, adds the nonce as a response extension, and signs it.
40#[allow(clippy::too_many_arguments)]
41pub fn sign_live_response<S, Sig>(
42    cert_id_der: &[u8],
43    status: LiveCertStatus,
44    nonce_bytes: &[u8],
45    responder_key_bytes: &[u8],
46    signer: &mut S,
47    now: u64,
48    validity_secs: u64,
49    responder_cert_der: Option<&[u8]>,
50) -> Result<Vec<u8>>
51where
52    S: Signer<Sig> + DynSignatureAlgorithmIdentifier,
53    Sig: SignatureBitStringEncoding,
54{
55    let next = now
56        .checked_add(validity_secs)
57        .ok_or_else(|| SignError::Config("live validity overflow".into()))?;
58    sign_live_response_with_window(
59        cert_id_der,
60        status,
61        nonce_bytes,
62        responder_key_bytes,
63        signer,
64        now,
65        now,
66        next,
67        responder_cert_der,
68    )
69}
70
71/// Sign only within the authenticated source window; producedAt is signing time,
72/// thisUpdate remains the time the source actually established the status.
73#[allow(clippy::too_many_arguments)]
74pub fn sign_live_response_with_window<S, Sig>(
75    cert_id_der: &[u8],
76    status: LiveCertStatus,
77    nonce_bytes: &[u8],
78    responder_key_bytes: &[u8],
79    signer: &mut S,
80    now: u64,
81    source_this_update: u64,
82    source_next_update: u64,
83    responder_cert_der: Option<&[u8]>,
84) -> Result<Vec<u8>>
85where
86    S: Signer<Sig> + DynSignatureAlgorithmIdentifier,
87    Sig: SignatureBitStringEncoding,
88{
89    if source_this_update > now
90        || source_next_update <= now
91        || source_next_update <= source_this_update
92    {
93        return Err(SignError::Config(
94            "live source is not currently valid".into(),
95        ));
96    }
97    let mut next_update_epoch = source_next_update;
98    if let Some(bytes) = responder_cert_der {
99        let cert = CertificateForLive::from_der(bytes).map_err(SignError::Der)?;
100        let validity = &cert.tbs_certificate.validity;
101        let before = validity.not_before.to_unix_duration().as_secs();
102        let after = validity.not_after.to_unix_duration().as_secs();
103        if now < before || now >= after {
104            return Err(SignError::Config(
105                "responder certificate is not currently valid".into(),
106            ));
107        }
108        next_update_epoch = next_update_epoch.min(after);
109    }
110    let cert_id = CertId::from_der(cert_id_der).map_err(SignError::Der)?;
111
112    let cert_status = match status {
113        LiveCertStatus::Good => CertStatus::good(),
114        LiveCertStatus::Unknown => CertStatus::unknown(),
115        LiveCertStatus::Revoked {
116            revocation_time,
117            reason,
118        } => {
119            let revoked_info = x509_ocsp::RevokedInfo {
120                revocation_time: ocsp_time(revocation_time)?,
121                revocation_reason: reason,
122            };
123            CertStatus::revoked(revoked_info)
124        }
125    };
126
127    let this_update = ocsp_time(source_this_update)?;
128    let next_update = ocsp_time(next_update_epoch)?;
129    let produced_at = ocsp_time(now)?;
130
131    let single =
132        SingleResponse::new(cert_id, cert_status, this_update).with_next_update(next_update);
133
134    let responder_key_hash = Sha1::digest(responder_key_bytes);
135    let responder_id =
136        ResponderId::ByKey(OctetString::new(responder_key_hash.to_vec()).map_err(SignError::Der)?);
137
138    let nonce = Nonce::new(nonce_bytes.to_vec()).map_err(SignError::Der)?;
139
140    let builder = OcspResponseBuilder::new(responder_id)
141        .with_single_response(single)
142        .with_extension(nonce)
143        .map_err(|e| SignError::OcspBuilder(e.to_string()))?;
144
145    let certs = responder_cert_der
146        .map(|c| {
147            let cert = x509_cert::Certificate::from_der(c).map_err(SignError::Der)?;
148            Ok::<_, SignError>(vec![cert])
149        })
150        .transpose()?;
151    let ocsp_response = builder
152        .sign(signer, certs, produced_at)
153        .map_err(SignError::from)?;
154
155    ocsp_response.to_der().map_err(SignError::Der)
156}
157
158/// Validated software-key material, shared by startup and rotation.
159pub struct LiveSigningMaterial {
160    pub key: p256::ecdsa::SigningKey,
161    pub responder_key_bytes: Vec<u8>,
162    pub responder_cert_der: Option<Vec<u8>>,
163}
164
165pub fn load_live_material(
166    ca: &hoike_core::config::CaConfig,
167) -> std::result::Result<LiveSigningMaterial, String> {
168    use hoike_core::config::SigningKeyConfig;
169    let key = match &ca.signing_key {
170        Some(SigningKeyConfig::File { path }) => {
171            crate::load_ecdsa_p256_key(path).map_err(|e| e.to_string())?
172        }
173        Some(SigningKeyConfig::Demo) => crate::demo_ecdsa_p256_key(),
174        _ => return Err("live signing requires an ECDSA file or explicit demo key".into()),
175    };
176    let cert_der = crate::orchestrate::load_responder_cert(ca)?;
177    let responder_key_bytes = if let Some(bytes) = &cert_der {
178        let cert = CertificateForLive::from_der(bytes).map_err(|e| e.to_string())?;
179        let now = std::time::SystemTime::now()
180            .duration_since(std::time::UNIX_EPOCH)
181            .map_err(|e| e.to_string())?
182            .as_secs();
183        if now
184            < cert
185                .tbs_certificate
186                .validity
187                .not_before
188                .to_unix_duration()
189                .as_secs()
190            || now
191                >= cert
192                    .tbs_certificate
193                    .validity
194                    .not_after
195                    .to_unix_duration()
196                    .as_secs()
197        {
198            return Err("live responder certificate is outside its validity period".into());
199        }
200        let bytes = cert
201            .tbs_certificate
202            .subject_public_key_info
203            .subject_public_key
204            .as_bytes()
205            .ok_or_else(|| "responder public key has unused bits".to_string())?;
206        let certificate_key =
207            p256::ecdsa::VerifyingKey::from_sec1_bytes(bytes).map_err(|e| e.to_string())?;
208        if certificate_key != *key.verifying_key() {
209            return Err("live signing key does not match responder certificate".into());
210        }
211        bytes.to_vec()
212    } else {
213        let bytes = crate::orchestrate::decode_issuer_key(ca)?;
214        if !matches!(ca.signing_key, Some(SigningKeyConfig::Demo)) {
215            let issuer_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&bytes)
216                .map_err(|e| format!("CA-direct issuer key: {e}"))?;
217            if issuer_key != *key.verifying_key() {
218                return Err("CA-direct signing key does not match issuer key".into());
219            }
220        }
221        bytes
222    };
223    Ok(LiveSigningMaterial {
224        key,
225        responder_key_bytes,
226        responder_cert_der: cert_der,
227    })
228}
229
230/// Status and authenticated freshness from exactly one matching SingleResponse.
231pub struct LiveResponseSource {
232    pub status: LiveCertStatus,
233    pub this_update: u64,
234    pub next_update: u64,
235}
236
237pub fn extract_status_for_cert(
238    response_der: &[u8],
239    cert_id_der: &[u8],
240) -> Result<LiveResponseSource> {
241    let requested = CertId::from_der(cert_id_der).map_err(SignError::Der)?;
242    let basic = parse_basic(response_der)?;
243    let mut matches = basic
244        .tbs_response_data
245        .responses
246        .iter()
247        .filter(|r| r.cert_id == requested);
248    let single = matches
249        .next()
250        .ok_or_else(|| SignError::OcspBuilder("requested CertID absent from response".into()))?;
251    if matches.next().is_some() {
252        return Err(SignError::OcspBuilder(
253            "ambiguous duplicate CertID in response".into(),
254        ));
255    }
256    let next = single
257        .next_update
258        .as_ref()
259        .ok_or_else(|| SignError::OcspBuilder("live source requires nextUpdate".into()))?;
260    Ok(LiveResponseSource {
261        status: status_from_single(single),
262        this_update: generalized_time_to_epoch(&single.this_update),
263        next_update: generalized_time_to_epoch(next),
264    })
265}
266
267fn parse_basic(response_der: &[u8]) -> Result<BasicOcspResponse> {
268    let response = OcspResponse::from_der(response_der).map_err(SignError::Der)?;
269    if response.response_status != x509_ocsp::OcspResponseStatus::Successful {
270        return Err(SignError::OcspBuilder(
271            "source OCSP response was not successful".into(),
272        ));
273    }
274    let bytes = response
275        .response_bytes
276        .ok_or_else(|| SignError::OcspBuilder("missing responseBytes".into()))?;
277    if bytes.response_type.to_string() != "1.3.6.1.5.5.7.48.1.1" {
278        return Err(SignError::OcspBuilder("unsupported response type".into()));
279    }
280    BasicOcspResponse::from_der(bytes.response.as_bytes()).map_err(SignError::Der)
281}
282
283fn status_from_single(single: &SingleResponse) -> LiveCertStatus {
284    match &single.cert_status {
285        CertStatus::Good(_) => LiveCertStatus::Good,
286        CertStatus::Unknown(_) => LiveCertStatus::Unknown,
287        CertStatus::Revoked(info) => LiveCertStatus::Revoked {
288            revocation_time: generalized_time_to_epoch(&info.revocation_time),
289            reason: info.revocation_reason,
290        },
291    }
292}
293
294/// Diagnostic compatibility API. Batched sources require explicit CertID selection.
295pub fn extract_status_from_response(response_der: &[u8]) -> Result<LiveCertStatus> {
296    let basic = parse_basic(response_der)?;
297    if basic.tbs_response_data.responses.len() != 1 {
298        return Err(SignError::OcspBuilder(
299            "CertID required for a batched response".into(),
300        ));
301    }
302    Ok(status_from_single(&basic.tbs_response_data.responses[0]))
303}
304
305fn generalized_time_to_epoch(gt: &OcspGeneralizedTime) -> u64 {
306    gt.0.to_unix_duration().as_secs()
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use der::Encode;
313    use p256::ecdsa::SigningKey;
314
315    fn test_key() -> SigningKey {
316        SigningKey::from_bytes((&[1u8; 32]).into()).unwrap()
317    }
318
319    #[test]
320    fn sign_live_good_with_nonce() {
321        let sha256_oid = const_oid::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
322        let cert_id = CertId {
323            hash_algorithm: spki::AlgorithmIdentifierOwned {
324                oid: sha256_oid,
325                parameters: Some(der::asn1::Null.into()),
326            },
327            issuer_name_hash: OctetString::new(vec![0xAA; 32]).unwrap(),
328            issuer_key_hash: OctetString::new(vec![0xBB; 32]).unwrap(),
329            serial_number: x509_cert::serial_number::SerialNumber::new(&[42u8]).unwrap(),
330        };
331        let cert_id_der = cert_id.to_der().unwrap();
332
333        let nonce = vec![
334            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
335            0x0F, 0x10,
336        ];
337
338        let mut key = test_key();
339        let resp_der = sign_live_response::<_, p256::ecdsa::DerSignature>(
340            &cert_id_der,
341            LiveCertStatus::Good,
342            &nonce,
343            &[0xBB; 32],
344            &mut key,
345            1700000000,
346            86400,
347            None,
348        )
349        .unwrap();
350
351        let resp = OcspResponse::from_der(&resp_der).unwrap();
352        assert_eq!(
353            resp.response_status,
354            x509_ocsp::OcspResponseStatus::Successful
355        );
356
357        let basic =
358            BasicOcspResponse::from_der(resp.response_bytes.unwrap().response.as_bytes()).unwrap();
359
360        // Verify nonce is in the response
361        let resp_nonce = basic.nonce().expect("response should contain nonce");
362        assert_eq!(resp_nonce.0.as_bytes(), &nonce);
363    }
364
365    #[test]
366    fn sign_live_revoked_with_nonce() {
367        let sha256_oid = const_oid::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
368        let cert_id = CertId {
369            hash_algorithm: spki::AlgorithmIdentifierOwned {
370                oid: sha256_oid,
371                parameters: Some(der::asn1::Null.into()),
372            },
373            issuer_name_hash: OctetString::new(vec![0xAA; 32]).unwrap(),
374            issuer_key_hash: OctetString::new(vec![0xBB; 32]).unwrap(),
375            serial_number: x509_cert::serial_number::SerialNumber::new(&[100u8]).unwrap(),
376        };
377        let cert_id_der = cert_id.to_der().unwrap();
378
379        let nonce = vec![0xAA; 16];
380
381        let mut key = test_key();
382        let resp_der = sign_live_response::<_, p256::ecdsa::DerSignature>(
383            &cert_id_der,
384            LiveCertStatus::Revoked {
385                revocation_time: 1699900000,
386                reason: Some(x509_cert::ext::pkix::CrlReason::KeyCompromise),
387            },
388            &nonce,
389            &[0xBB; 32],
390            &mut key,
391            1700000000,
392            86400,
393            None,
394        )
395        .unwrap();
396
397        let resp = OcspResponse::from_der(&resp_der).unwrap();
398        assert_eq!(
399            resp.response_status,
400            x509_ocsp::OcspResponseStatus::Successful
401        );
402    }
403
404    #[test]
405    fn extract_status_round_trip() {
406        let sha256_oid = const_oid::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
407        let cert_id = CertId {
408            hash_algorithm: spki::AlgorithmIdentifierOwned {
409                oid: sha256_oid,
410                parameters: Some(der::asn1::Null.into()),
411            },
412            issuer_name_hash: OctetString::new(vec![0xAA; 32]).unwrap(),
413            issuer_key_hash: OctetString::new(vec![0xBB; 32]).unwrap(),
414            serial_number: x509_cert::serial_number::SerialNumber::new(&[42u8]).unwrap(),
415        };
416        let cert_id_der = cert_id.to_der().unwrap();
417
418        let mut key = test_key();
419        let resp_der = sign_live_response::<_, p256::ecdsa::DerSignature>(
420            &cert_id_der,
421            LiveCertStatus::Good,
422            &[0xFF; 16],
423            &[0xBB; 32],
424            &mut key,
425            1700000000,
426            86400,
427            None,
428        )
429        .unwrap();
430
431        let extracted = extract_status_from_response(&resp_der).unwrap();
432        assert!(matches!(extracted, LiveCertStatus::Good));
433    }
434}