Skip to main content

hoike_core/
request.rs

1use der::{Decode, Encode};
2use sha2::{Digest, Sha256};
3use x509_ocsp::OcspRequest;
4
5use crate::error::{CoreError, Result};
6
7/// Maximum allowed OCSP request size in bytes.
8pub const MAX_REQUEST_SIZE: usize = 8192;
9
10/// Parsed OCSP request reduced to what the edge needs for lookup.
11#[derive(Debug)]
12pub struct ParsedRequest {
13    pub cert_ids: Vec<ParsedCertId>,
14    pub nonce: Option<Vec<u8>>,
15    /// Algorithm discriminators from the Preferred Signature Algorithms
16    /// request extension (RFC 6960 §4.4.7.1), in client preference order.
17    pub preferred_sig_algs: Vec<u16>,
18}
19
20/// A CertID extracted from a request, with its precomputed entry key
21/// and the issuer hash fields needed for multi-CA routing.
22#[derive(Debug, Clone)]
23pub struct ParsedCertId {
24    pub entry_key: [u8; 32],
25    pub certid_der: Vec<u8>,
26    pub issuer_name_hash: Vec<u8>,
27    pub issuer_key_hash: Vec<u8>,
28    pub serial_number: Vec<u8>,
29    /// The CertID `hashAlgorithm` OID in dotted-decimal form, retained for
30    /// observability (the `hoike_certid_hash_alg_total` metric). Clients that
31    /// still use SHA-1 CertIDs are visible here.
32    pub hash_alg_oid: String,
33}
34
35/// Parse a DER-encoded OCSPRequest into the fields we need for lookup.
36pub fn parse_ocsp_request(der_bytes: &[u8]) -> Result<ParsedRequest> {
37    if der_bytes.is_empty() {
38        return Err(CoreError::EmptyRequest);
39    }
40    if der_bytes.len() > MAX_REQUEST_SIZE {
41        return Err(CoreError::RequestTooLarge {
42            size: der_bytes.len(),
43            max: MAX_REQUEST_SIZE,
44        });
45    }
46
47    let ocsp_req = OcspRequest::from_der(der_bytes).map_err(|e| CoreError::DerParse {
48        context: "OCSPRequest",
49        detail: e.to_string(),
50    })?;
51
52    let tbs = &ocsp_req.tbs_request;
53
54    if tbs.request_list.is_empty() {
55        return Err(CoreError::EmptyRequestList);
56    }
57
58    let mut cert_ids = Vec::with_capacity(tbs.request_list.len());
59
60    for req in tbs.request_list.iter() {
61        let cert_id = &req.req_cert;
62
63        let certid_der = cert_id.to_der().map_err(|e| CoreError::DerParse {
64            context: "CertID encode",
65            detail: e.to_string(),
66        })?;
67
68        let entry_key: [u8; 32] = Sha256::digest(&certid_der).into();
69
70        cert_ids.push(ParsedCertId {
71            entry_key,
72            certid_der,
73            issuer_name_hash: cert_id.issuer_name_hash.as_bytes().to_vec(),
74            issuer_key_hash: cert_id.issuer_key_hash.as_bytes().to_vec(),
75            serial_number: cert_id.serial_number.as_bytes().to_vec(),
76            hash_alg_oid: cert_id.hash_algorithm.oid.to_string(),
77        });
78    }
79
80    // Extract nonce extension if present.
81    // OID: 1.3.6.1.5.5.7.48.1.2 (id-pkix-ocsp-nonce)
82    // extn_value contains the DER encoding of the extension value,
83    // which for the nonce is an OCTET STRING wrapping the raw nonce.
84    // We parse through that inner OCTET STRING to get the actual nonce bytes
85    // so RFC 9654 length validation applies to the nonce itself, not
86    // its DER wrapper.
87    let nonce_ext = tbs.request_extensions.as_ref().and_then(|exts| {
88        exts.iter()
89            .find(|ext| ext.extn_id == der::oid::db::rfc6960::ID_PKIX_OCSP_NONCE)
90    });
91
92    let nonce = match nonce_ext {
93        Some(ext) => {
94            let raw = ext.extn_value.as_bytes();
95            let inner =
96                der::asn1::OctetStringRef::from_der(raw).map_err(|e| CoreError::DerParse {
97                    context: "nonce extension value",
98                    detail: e.to_string(),
99                })?;
100            Some(inner.as_bytes().to_vec())
101        }
102        None => None,
103    };
104
105    let preferred_sig_algs = parse_preferred_sig_algs(tbs.request_extensions.as_ref());
106
107    Ok(ParsedRequest {
108        cert_ids,
109        nonce,
110        preferred_sig_algs,
111    })
112}
113
114/// RFC 6960 §4.4.7.1 PreferredSignatureAlgorithm.
115#[derive(der::Sequence)]
116struct PreferredSignatureAlgorithm<'a> {
117    sig_identifier: spki::AlgorithmIdentifierRef<'a>,
118    #[asn1(optional = "true")]
119    cert_identifier: Option<spki::AlgorithmIdentifierRef<'a>>,
120}
121
122/// Map a signature algorithm OID to its bundle discriminator.
123/// Compares OID bytes directly to avoid heap-allocating a String on the hot path.
124fn sig_oid_to_discriminator(oid: &const_oid::ObjectIdentifier) -> Option<u16> {
125    const ECDSA_SHA256: const_oid::ObjectIdentifier =
126        const_oid::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
127    const ML_DSA_44: const_oid::ObjectIdentifier =
128        const_oid::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.17");
129    const ML_DSA_65: const_oid::ObjectIdentifier =
130        const_oid::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.18");
131    const ML_DSA_87: const_oid::ObjectIdentifier =
132        const_oid::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.19");
133
134    if *oid == ECDSA_SHA256 {
135        Some(ahu::ALG_DISC_DEFAULT)
136    } else if *oid == ML_DSA_44 {
137        Some(ahu::ALG_DISC_ML_DSA_44)
138    } else if *oid == ML_DSA_65 {
139        Some(ahu::ALG_DISC_ML_DSA_65)
140    } else if *oid == ML_DSA_87 {
141        Some(ahu::ALG_DISC_ML_DSA_87)
142    } else {
143        None
144    }
145}
146
147fn parse_preferred_sig_algs(extensions: Option<&x509_cert::ext::Extensions>) -> Vec<u16> {
148    use der::Decode;
149
150    let pref_sig_algs_oid = const_oid::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.48.1.8");
151
152    let ext = match extensions {
153        Some(exts) => match exts.iter().find(|e| e.extn_id == pref_sig_algs_oid) {
154            Some(e) => e,
155            None => return Vec::new(),
156        },
157        None => return Vec::new(),
158    };
159
160    let raw = ext.extn_value.as_bytes();
161
162    let prefs: Vec<PreferredSignatureAlgorithm<'_>> = match Vec::from_der(raw) {
163        Ok(v) => v,
164        Err(e) => {
165            tracing::debug!("failed to parse PreferredSignatureAlgorithms: {e}");
166            return Vec::new();
167        }
168    };
169
170    prefs
171        .iter()
172        .filter_map(|p| sig_oid_to_discriminator(&p.sig_identifier.oid))
173        .collect()
174}
175
176/// Decode a GET request path into DER bytes.
177///
178/// RFC 9919 §6: the path after the AIA URI is the base64 encoding of the
179/// DER-encoded OCSPRequest, URL-encoded.
180pub fn decode_get_path(path: &str) -> Result<Vec<u8>> {
181    use base64::Engine;
182    use percent_encoding::percent_decode_str;
183
184    let decoded_path = percent_decode_str(path)
185        .decode_utf8()
186        .map_err(|e| CoreError::GetDecode(format!("URL decode: {e}")))?;
187
188    let trimmed = decoded_path.trim_start_matches('/');
189
190    base64::engine::general_purpose::STANDARD
191        .decode(trimmed)
192        .map_err(|e| CoreError::GetDecode(format!("base64: {e}")))
193}
194
195/// Validate nonce length per RFC 9654.
196pub fn validate_nonce(nonce_bytes: &[u8]) -> NonceAction {
197    match nonce_bytes.len() {
198        0 => NonceAction::MalformedRequest,
199        1..=15 => NonceAction::MayOmit,
200        16..=32 => NonceAction::MustAccept,
201        33..=128 => NonceAction::MayOmit,
202        _ => NonceAction::MalformedRequest,
203    }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum NonceAction {
208    MustAccept,
209    MayOmit,
210    MalformedRequest,
211}