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<'static>>()
234            .map_err(SyntaErr)?;
235        let _ = self.inner.set(Box::new(decoded));
236        Ok(self.inner.get().unwrap().as_ref())
237    }
238}
239
240#[pymethods]
241impl PyTimeStampResp {
242    /// Parse a DER-encoded ``TimeStampResp`` SEQUENCE.
243    ///
244    /// Validates the envelope tag and length eagerly; field decoding is deferred
245    /// to the first property access.
246    ///
247    /// :param data: DER bytes of the ``TimeStampResp``.
248    /// :raises ValueError: if the bytes cannot be decoded.
249    #[staticmethod]
250    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
251        let py_bytes = data.unbind();
252        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that keeps
253        // the Python bytes object alive for the lifetime of this struct.
254        // CPython bytes objects have a fixed-address, non-relocating payload
255        // buffer (CPython has no moving GC).  The slice lifetime is extended to
256        // 'static; the actual safety invariants are:
257        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
258        //       can outlive the struct, so `raw` is never read after drop begins.
259        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
260        //       allocation), so field drop order does not cause use-after-free.
261        //   (3) `inner` contains only borrow-typed fields; dropping
262        //       Box<TimeStampResp<'static>> does not read through the contained
263        //       &'static slices (borrows have no destructors).
264        // CPython-only: does not hold for PyPy or GraalPy.
265        let raw: &'static [u8] = unsafe {
266            let s = py_bytes.bind(py).as_bytes();
267            std::slice::from_raw_parts(s.as_ptr(), s.len())
268        };
269        {
270            let mut d = Decoder::new(raw, Encoding::Der);
271            d.read_tag()
272                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
273            d.read_length()
274                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
275        }
276        Ok(Self {
277            _data: py_bytes,
278            raw,
279            inner: OnceLock::new(),
280            status_cache: OnceLock::new(),
281            time_stamp_token_cache: OnceLock::new(),
282        })
283    }
284
285    /// Return the original DER bytes passed to :meth:`from_der`.
286    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
287        Ok(PyBytes::new(py, self.raw))
288    }
289
290    /// ``PKIStatus`` integer value.
291    ///
292    /// Named values per RFC 3161 §2.4.2:
293    ///
294    /// - ``0`` — ``granted``
295    /// - ``1`` — ``grantedWithMods``
296    /// - ``2`` — ``rejection``
297    /// - ``3`` — ``waiting``
298    /// - ``4`` — ``revocationWarning``
299    /// - ``5`` — ``revocationNotification``
300    #[getter]
301    fn status(&self) -> PyResult<i64> {
302        if let Some(v) = self.status_cache.get() {
303            return Ok(*v);
304        }
305        let v = self.tsr()?.status.status.as_i64().map_err(SyntaErr)?;
306        let _ = self.status_cache.set(v);
307        Ok(v)
308    }
309
310    /// Raw DER bytes of the ``timeStampToken`` ``ContentInfo``, or ``None`` if
311    /// the response carries no token (e.g. rejection or waiting status).
312    #[getter]
313    fn time_stamp_token<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
314        if let Some(cached) = self.time_stamp_token_cache.get() {
315            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
316        }
317        let computed: Option<Py<PyBytes>> = match self.tsr()?.time_stamp_token.as_ref() {
318            None => None,
319            Some(ci) => {
320                let der = ci.to_der().map_err(SyntaErr)?;
321                Some(PyBytes::new(py, &der).unbind())
322            }
323        };
324        let cached = computed.as_ref().map(|b| b.clone_ref(py));
325        let _ = self.time_stamp_token_cache.set(cached);
326        Ok(computed.map(|b| b.into_bound(py)))
327    }
328
329    fn __repr__(&self) -> PyResult<String> {
330        let status = self.tsr()?.status.status.as_i64().map_err(SyntaErr)?;
331        Ok(format!("TimeStampResp(status={status})"))
332    }
333}
334
335// ── register ──────────────────────────────────────────────────────────────────
336
337/// Register TSP builder classes into the given module.
338pub(super) fn register_tsp_classes(m: &Bound<'_, PyModule>) -> PyResult<()> {
339    m.add_class::<PyTimeStampReqBuilder>()?;
340    m.add_class::<PyTimeStampResp>()?;
341    Ok(())
342}