Skip to main content

_synta/certificate/
tsp.rs

1//! Python bindings for RFC 3161 Time-Stamp Protocol (TSP) builders and parsed types.
2//!
3//! Exposes [`PyTimeStampReqBuilder`] as a Python class for constructing
4//! DER-encoded `TimeStampReq` structures, and [`PyTimeStampResp`] for
5//! decoding DER-encoded `TimeStampResp` structures.
6
7use std::sync::OnceLock;
8
9use pyo3::prelude::*;
10use pyo3::types::PyBytes;
11
12use synta::{Decoder, Encoding};
13
14use crate::error::SyntaErr;
15
16// ── PyTimeStampReqBuilder ─────────────────────────────────────────────────────
17
18/// Python-facing wrapper for [`synta_certificate::TimeStampReqBuilder`].
19///
20/// Builds a DER-encoded ``TimeStampReq`` (RFC 3161 §2.4.1).
21///
22/// The only required field is ``message_imprint`` (set via
23/// :meth:`message_imprint` or :meth:`message_imprint_with_alg_der`).
24/// All other fields are optional.
25///
26/// Example::
27///
28///     import synta
29///     import synta.oids as oids
30///
31///     # SHA-256 hash of the content to timestamp
32///     import hashlib
33///     digest = hashlib.sha256(b"data").digest()
34///
35///     req_der = (
36///         synta.TimeStampReqBuilder()
37///         .message_imprint(list(oids.SHA256.components()), digest)
38///         .nonce(b"\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08")
39///         .cert_req(True)
40///         .build()
41///     )
42#[pyclass(name = "TimeStampReqBuilder")]
43pub struct PyTimeStampReqBuilder {
44    inner: synta_certificate::TimeStampReqBuilder,
45    built: bool,
46}
47
48#[pymethods]
49impl PyTimeStampReqBuilder {
50    /// Create a new, empty ``TimeStampReqBuilder``.
51    #[new]
52    fn new() -> Self {
53        Self {
54            inner: synta_certificate::TimeStampReqBuilder::new(),
55            built: false,
56        }
57    }
58
59    /// Set the ``messageImprint`` from a hash algorithm OID (as a list of
60    /// integer arcs) and raw hash bytes.
61    ///
62    /// A ``NULL`` parameters field is added automatically (standard for all
63    /// SHA-2 hash algorithms).
64    ///
65    /// :param hash_alg_oid: OID arc components as a list of ints, e.g.
66    ///     ``list(synta.oids.SHA256.components())``.
67    /// :param hashed_message: raw hash bytes (no OCTET STRING wrapper).
68    /// :raises ValueError: if the OID is invalid or encoding fails
69    ///     (deferred to :meth:`build`).
70    fn message_imprint<'py>(
71        slf: Bound<'py, Self>,
72        hash_alg_oid: Vec<u32>,
73        hashed_message: &[u8],
74    ) -> Bound<'py, Self> {
75        {
76            let mut guard = slf.borrow_mut();
77            let old = std::mem::replace(
78                &mut guard.inner,
79                synta_certificate::TimeStampReqBuilder::new(),
80            );
81            guard.inner = old.message_imprint(&hash_alg_oid, hashed_message);
82        }
83        slf
84    }
85
86    /// Set the ``messageImprint`` from a pre-encoded ``AlgorithmIdentifier``
87    /// DER TLV and raw hash bytes.
88    ///
89    /// :param alg_der: complete ``AlgorithmIdentifier`` SEQUENCE TLV bytes.
90    /// :param hashed_message: raw hash bytes (no OCTET STRING wrapper).
91    /// :raises ValueError: if decoding fails (deferred to :meth:`build`).
92    fn message_imprint_with_alg_der<'py>(
93        slf: Bound<'py, Self>,
94        alg_der: &[u8],
95        hashed_message: &[u8],
96    ) -> Bound<'py, Self> {
97        {
98            let mut guard = slf.borrow_mut();
99            let old = std::mem::replace(
100                &mut guard.inner,
101                synta_certificate::TimeStampReqBuilder::new(),
102            );
103            guard.inner = old.message_imprint_with_alg_der(alg_der, hashed_message);
104        }
105        slf
106    }
107
108    /// Set the optional ``reqPolicy`` OID by arc components.
109    ///
110    /// :param oid_components: OID arc components as a list of ints.
111    /// :raises ValueError: if the OID is invalid (deferred to :meth:`build`).
112    fn req_policy<'py>(slf: Bound<'py, Self>, oid_components: Vec<u32>) -> Bound<'py, Self> {
113        {
114            let mut guard = slf.borrow_mut();
115            let old = std::mem::replace(
116                &mut guard.inner,
117                synta_certificate::TimeStampReqBuilder::new(),
118            );
119            guard.inner = old.req_policy(&oid_components);
120        }
121        slf
122    }
123
124    /// Set the optional ``nonce`` value.
125    ///
126    /// :param nonce_bytes: big-endian two's-complement representation of the
127    ///     nonce integer.
128    fn nonce<'py>(slf: Bound<'py, Self>, nonce_bytes: &[u8]) -> Bound<'py, Self> {
129        {
130            let mut guard = slf.borrow_mut();
131            let old = std::mem::replace(
132                &mut guard.inner,
133                synta_certificate::TimeStampReqBuilder::new(),
134            );
135            guard.inner = old.nonce(nonce_bytes);
136        }
137        slf
138    }
139
140    /// Set the ``certReq`` flag.
141    ///
142    /// When ``True``, requests that the TSA include its signing certificate
143    /// in the time-stamp response.
144    ///
145    /// :param val: ``True`` to request certificate inclusion.
146    fn cert_req<'py>(slf: Bound<'py, Self>, val: bool) -> Bound<'py, Self> {
147        {
148            let mut guard = slf.borrow_mut();
149            let old = std::mem::replace(
150                &mut guard.inner,
151                synta_certificate::TimeStampReqBuilder::new(),
152            );
153            guard.inner = old.cert_req(val);
154        }
155        slf
156    }
157
158    /// Build the DER-encoded ``TimeStampReq`` SEQUENCE.
159    ///
160    /// :returns: DER bytes of the complete ``TimeStampReq``.
161    /// :raises ValueError: if ``message_imprint`` was not set, if any OID
162    ///     was invalid, if DER encoding fails, or if called more than once.
163    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
164        if self.built {
165            return Err(pyo3::exceptions::PyValueError::new_err(
166                "build() has already been called; create a new builder",
167            ));
168        }
169        self.built = true;
170        let inner = std::mem::replace(
171            &mut self.inner,
172            synta_certificate::TimeStampReqBuilder::new(),
173        );
174        let der = inner
175            .build()
176            .map_err(pyo3::exceptions::PyValueError::new_err)?;
177        Ok(PyBytes::new(py, &der))
178    }
179
180    fn __repr__(&self) -> String {
181        "TimeStampReqBuilder()".to_string()
182    }
183}
184
185// ── PyTimeStampResp ───────────────────────────────────────────────────────────
186
187/// RFC 3161 Time-Stamp Response (TSP, RFC 3161 §2.4.2).
188///
189/// A ``TimeStampResp`` carries a ``PKIStatusInfo`` (indicating whether the
190/// request was granted or rejected) and an optional ``timeStampToken``
191/// (a CMS ``ContentInfo`` wrapping a DER-encoded ``SignedData``).
192///
193/// ```python,ignore
194/// import synta
195///
196/// with open("response.tsr", "rb") as f:
197///     resp = synta.TimeStampResp.from_der(f.read())
198///
199/// print(resp.status)          # 0 = granted
200/// token = resp.time_stamp_token  # bytes or None
201/// ```
202#[pyclass(frozen, name = "TimeStampResp")]
203pub struct PyTimeStampResp {
204    /// Owning reference to the Python ``bytes`` object whose buffer backs `raw`.
205    ///
206    /// Kept alive for the entire lifetime of this struct so that `raw` is
207    /// never dangling.
208    _data: Py<PyBytes>,
209    /// Slice into the payload of `_data`.  Typed `'static` because CPython
210    /// bytes objects have non-relocating buffers and `_data` keeps the object
211    /// alive; see the safety comments in [`Self::from_der`].
212    raw: &'static [u8],
213    /// Lazily decoded `TimeStampResp`; initialised on the first property access.
214    inner: OnceLock<Box<synta_certificate::tsp_types::TimeStampResp<'static>>>,
215    /// Cached value of `status.status` as an `i64` (avoids re-decoding).
216    status_cache: OnceLock<i64>,
217    /// Cached re-encoded `timeStampToken` DER bytes, or `None` if absent.
218    time_stamp_token_cache: OnceLock<Option<Py<PyBytes>>>,
219}
220
221impl PyTimeStampResp {
222    /// Decode and cache the `TimeStampResp` from `self.raw`.
223    ///
224    /// The first call decodes the DER bytes and stores the result in
225    /// `self.inner`; subsequent calls return the cached value immediately.
226    /// Returns `Err(ValueError)` if DER decoding fails.
227    fn tsr(&self) -> PyResult<&synta_certificate::tsp_types::TimeStampResp<'static>> {
228        if let Some(v) = self.inner.get() {
229            return Ok(v.as_ref());
230        }
231        let mut dec = Decoder::new(self.raw, Encoding::Der);
232        let decoded = dec
233            .decode::<synta_certificate::tsp_types::TimeStampResp<'_>>()
234            .map_err(SyntaErr)?;
235        // SAFETY: Four invariants hold together:
236        //   1. `raw` points into the payload of `_data` (a CPython `bytes` object),
237        //      whose buffer is non-relocating — CPython has no moving GC.
238        //   2. `_data` is a `Py<PyBytes>` stored in `self`; it keeps the underlying
239        //      Python object alive for at least as long as `self` lives.
240        //   3. `#[pyclass(frozen)]` prevents any `&mut self` method, so no mutation
241        //      can invalidate the borrow while a reference exists.
242        //   4. The decoded struct borrows only from `raw`, so transmuting its
243        //      lifetime from `'_` to `'static` is sound under (1)–(3).
244        let decoded: synta_certificate::tsp_types::TimeStampResp<'static> =
245            unsafe { std::mem::transmute(decoded) };
246        let _ = self.inner.set(Box::new(decoded));
247        Ok(self.inner.get().unwrap().as_ref())
248    }
249}
250
251#[pymethods]
252impl PyTimeStampResp {
253    /// Parse a DER-encoded ``TimeStampResp`` SEQUENCE.
254    ///
255    /// Validates the envelope tag and length eagerly; field decoding is deferred
256    /// to the first property access.
257    ///
258    /// :param data: DER bytes of the ``TimeStampResp``.
259    /// :raises ValueError: if the bytes cannot be decoded.
260    #[staticmethod]
261    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
262        let py_bytes = data.unbind();
263        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that keeps
264        // the Python bytes object alive for the lifetime of this struct.
265        // CPython bytes objects have a fixed-address, non-relocating payload
266        // buffer (CPython has no moving GC).  The slice lifetime is extended to
267        // 'static; the actual safety invariants are:
268        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
269        //       can outlive the struct, so `raw` is never read after drop begins.
270        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
271        //       allocation), so field drop order does not cause use-after-free.
272        //   (3) `inner` contains only borrow-typed fields; dropping
273        //       Box<TimeStampResp<'static>> does not read through the contained
274        //       &'static slices (borrows have no destructors).
275        // CPython-only: does not hold for PyPy or GraalPy.
276        let raw: &'static [u8] = unsafe {
277            let s = py_bytes.bind(py).as_bytes();
278            std::slice::from_raw_parts(s.as_ptr(), s.len())
279        };
280        {
281            let mut d = Decoder::new(raw, Encoding::Der);
282            d.read_tag()
283                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
284            d.read_length()
285                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
286        }
287        Ok(Self {
288            _data: py_bytes,
289            raw,
290            inner: OnceLock::new(),
291            status_cache: OnceLock::new(),
292            time_stamp_token_cache: OnceLock::new(),
293        })
294    }
295
296    /// Return the original DER bytes passed to :meth:`from_der`.
297    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
298        Ok(PyBytes::new(py, self.raw))
299    }
300
301    /// ``PKIStatus`` integer value.
302    ///
303    /// Named values per RFC 3161 §2.4.2:
304    ///
305    /// - ``0`` — ``granted``
306    /// - ``1`` — ``grantedWithMods``
307    /// - ``2`` — ``rejection``
308    /// - ``3`` — ``waiting``
309    /// - ``4`` — ``revocationWarning``
310    /// - ``5`` — ``revocationNotification``
311    #[getter]
312    fn status(&self) -> PyResult<i64> {
313        if let Some(v) = self.status_cache.get() {
314            return Ok(*v);
315        }
316        let v = self.tsr()?.status.status.as_i64().map_err(SyntaErr)?;
317        let _ = self.status_cache.set(v);
318        Ok(v)
319    }
320
321    /// Raw DER bytes of the ``timeStampToken`` ``ContentInfo``, or ``None`` if
322    /// the response carries no token (e.g. rejection or waiting status).
323    #[getter]
324    fn time_stamp_token<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
325        if let Some(cached) = self.time_stamp_token_cache.get() {
326            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
327        }
328        let computed: Option<Py<PyBytes>> = match self.tsr()?.time_stamp_token.as_ref() {
329            None => None,
330            Some(ci) => {
331                let der = ci.to_der().map_err(SyntaErr)?;
332                Some(PyBytes::new(py, &der).unbind())
333            }
334        };
335        let cached = computed.as_ref().map(|b| b.clone_ref(py));
336        let _ = self.time_stamp_token_cache.set(cached);
337        Ok(computed.map(|b| b.into_bound(py)))
338    }
339
340    fn __repr__(&self) -> PyResult<String> {
341        let status = self.tsr()?.status.status.as_i64().map_err(SyntaErr)?;
342        Ok(format!("TimeStampResp(status={status})"))
343    }
344}
345
346// ── register ──────────────────────────────────────────────────────────────────
347
348/// Register TSP builder classes into the given module.
349pub(super) fn register_tsp_classes(m: &Bound<'_, PyModule>) -> PyResult<()> {
350    m.add_class::<PyTimeStampReqBuilder>()?;
351    m.add_class::<PyTimeStampResp>()?;
352    Ok(())
353}