_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}