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