Skip to main content

structured_pkix/ocsp/
nonce.rs

1//! Request nonce binding; randomness and request transport belong to the caller.
2
3use super::{BasicResponse, end};
4use crate::error::{Error, ErrorCode, Located, Result};
5use crate::oid;
6use structured_asn1::memory::{Memory, MemoryError};
7
8/// Caller-generated nonce for a new RFC9654 request. Construction validates
9/// size only; the caller must use a cryptographically strong random generator.
10#[derive(Debug, Clone, Copy)]
11pub struct RequestNonce<'a>(&'a [u8]);
12
13/// The actual sent nonce has a size outside the received wire schema.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct NonceSizeError {
16    /// Actual octet count; no input bytes are copied into diagnostics.
17    pub length: usize,
18}
19impl core::fmt::Display for NonceSizeError {
20    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
21        write!(formatter, "nonce length {} is outside 1..128", self.length)
22    }
23}
24#[cfg(feature = "std")]
25impl std::error::Error for NonceSizeError {}
26
27impl<'a> RequestNonce<'a> {
28    /// Retain an actual sent wire nonce for comparison, including legacy short
29    /// requests. RFC9654 §2.1 permits received sizes 1..128; generation of a
30    /// new compliant request must still use at least 32 strong random octets.
31    /// Invalid input is an error, not an absent request nonce.
32    /// <https://www.rfc-editor.org/rfc/rfc9654#section-2.1>
33    pub fn from_sent(bytes: &'a [u8]) -> core::result::Result<Self, NonceSizeError> {
34        if bytes.is_empty() || bytes.len() > 128 {
35            return Err(NonceSizeError {
36                length: bytes.len(),
37            });
38        }
39        Ok(Self(bytes))
40    }
41    /// Validate a newly generated request value. RFC9654 §2.1 requires new
42    /// requesters to use at least 32 octets; the
43    /// schema maximum is 128. This is not a check of the bytes' randomness.
44    /// <https://www.rfc-editor.org/rfc/rfc9654#section-2.1>
45    #[must_use]
46    pub fn new(bytes: &'a [u8]) -> Option<Self> {
47        if bytes.len() < 32 || bytes.len() > 128 {
48            return None;
49        }
50        Some(Self(bytes))
51    }
52
53    /// Original request bytes; no copy or encoding bridge.
54    #[must_use]
55    pub fn as_bytes(self) -> &'a [u8] {
56        self.0
57    }
58}
59
60/// Completed request-binding fact, not response authenticity or time validity.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum NonceBinding {
63    /// Neither request nor response carries a nonce.
64    NotRequested,
65    /// Responder supplied a nonce without a nonce-bearing request.
66    Unsolicited,
67    /// A nonce-bearing request received a response without one.
68    Missing,
69    /// The original request and response nonce bytes agree.
70    Matches,
71    /// The original request and response nonce bytes differ.
72    Differs,
73}
74
75pub(super) fn read<'a>(extension: &crate::Extension<'a>, execution: &Memory) -> Result<&'a [u8]> {
76    const FIELD: &str = "ocspResponse.nonce";
77    let mut reader = extension.reader(execution).field(FIELD)?;
78    let value = reader
79        .expect(structured_asn1::Tag::primitive(
80            structured_asn1::universal::OCTET_STRING,
81        ))
82        .field(FIELD)?;
83    end(&reader, FIELD)?;
84    let bytes = value.contents();
85    // RFC9654 §2.1: encoded inner OCTET STRING, not raw extnValue bytes.
86    // https://www.rfc-editor.org/rfc/rfc9654#section-2.1
87    if bytes.is_empty() || bytes.len() > 128 {
88        return Err(Error::new(
89            ErrorCode::MalformedValue,
90            FIELD,
91            extension.span().start,
92        ));
93    }
94    Ok(bytes)
95}
96
97pub(super) fn bind(
98    request: Option<RequestNonce<'_>>,
99    response: Option<&[u8]>,
100    execution: &Memory,
101    at: u64,
102) -> Result<NonceBinding> {
103    let (request, response) = match (request, response) {
104        (None, None) => return Ok(NonceBinding::NotRequested),
105        (None, Some(_)) => return Ok(NonceBinding::Unsolicited),
106        (Some(_), None) => return Ok(NonceBinding::Missing),
107        (Some(request), Some(response)) => (request.as_bytes(), response),
108    };
109    let work = u64::try_from(request.len().max(response.len()))
110        .ok()
111        .and_then(|n| n.checked_mul(2))
112        .and_then(|n| n.checked_add(1))
113        .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, "ocspResponse.nonce", at))?;
114    execution
115        .charge_work(work)
116        .map_err(|reason| Error::resource(reason, "ocspResponse.nonce", at))?;
117    Ok(if request == response {
118        NonceBinding::Matches
119    } else {
120        NonceBinding::Differs
121    })
122}
123
124impl<'a> BasicResponse<'a> {
125    /// Decode a response nonce, retaining the producing parser's local work and
126    /// depth ceilings. Received nonce syntax accepts 1..128 octets, including
127    /// legacy short nonces; this does not relax new-request construction.
128    pub fn nonce(&self, execution: &Memory) -> Result<Option<&'a [u8]>> {
129        match self.extensions.get(oid::OCSP_NONCE, execution)? {
130            Some(extension) => read(extension, execution).map(Some),
131            None => Ok(None),
132        }
133    }
134
135    /// Compare the actual request nonce only after authenticating this response.
136    /// Missing/unsolicited values are preserved for the caller's replay policy;
137    /// they are never replaced by a generated or default nonce.
138    pub fn bind_nonce(
139        &self,
140        request: Option<RequestNonce<'_>>,
141        execution: &Memory,
142    ) -> Result<NonceBinding> {
143        bind(request, self.nonce(execution)?, execution, self.span.start)
144    }
145}