Skip to main content

_synta/certificate/
cert_builder.rs

1//! Python bindings for building X.509 certificates natively.
2//!
3//! Delegates all DER construction and signing to
4//! [`synta_certificate::builder::CertificateBuilder`].  OpenSSL is only used
5//! for the signing step; all ASN.1 encoding is done inside the Rust builder.
6//!
7//! # Chaining
8//!
9//! Each setter returns the same `CertificateBuilder` object, enabling a
10//! single-expression build:
11//!
12//! ```python,ignore
13//! import synta, datetime
14//!
15//! cert = (
16//!     synta.CertificateBuilder()
17//!     .issuer_name(ca_cert.subject_raw_der)      # zero re-encode
18//!     .subject_name(csr.subject_raw_der)         # zero re-encode
19//!     .public_key_der(csr.subject_public_key_info_der)  # zero re-encode
20//!     .serial_number(42)
21//!     .not_valid_before_utc(datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc))
22//!     .not_valid_after_utc(datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc))
23//!     .add_extension("2.5.29.19", True, basic_constraints_der)
24//!     .sign(ca_key, "sha256")
25//! )
26//! ```
27
28use pyo3::exceptions::PyValueError;
29use pyo3::prelude::*;
30use pyo3::types::PyBytes;
31use pyo3::Py;
32use synta::{Integer, ObjectIdentifier};
33use synta_certificate::{CertificateBuilder, PrivateKey as _, Time, UnsignedCertificateSigner};
34
35use crate::certificate::cert::PyCertificate;
36use crate::crypto_keys::PyPrivateKey;
37
38// ── Helpers ───────────────────────────────────────────────────────────────────
39
40/// Convert a Python `datetime.datetime` (or any object with year/month/day/
41/// hour/minute/second attributes) to a synta `Time`.
42///
43/// Uses `UtcTime` for years 1950–2049 (RFC 5280 §4.1.2.5), `GeneralizedTime`
44/// otherwise.
45fn py_to_synta_time(dt: &Bound<'_, PyAny>) -> PyResult<Time> {
46    // Reject naive datetimes (tzinfo is None) to prevent silent UTC misinterpretation.
47    if dt.getattr("tzinfo")?.is_none() {
48        return Err(PyValueError::new_err(
49            "datetime must be timezone-aware (tzinfo must not be None); \
50             use datetime.timezone.utc to specify UTC",
51        ));
52    }
53    let year = dt.getattr("year")?.extract::<u32>()?;
54    let month = dt.getattr("month")?.extract::<u8>()?;
55    let day = dt.getattr("day")?.extract::<u8>()?;
56    let hour = dt.getattr("hour")?.extract::<u8>()?;
57    let minute = dt.getattr("minute")?.extract::<u8>()?;
58    let second = dt.getattr("second")?.extract::<u8>()?;
59    if (1950..=2049).contains(&year) {
60        Ok(Time::UtcTime(
61            synta::UtcTime::new(year as u16, month, day, hour, minute, second)
62                .map_err(|e| PyValueError::new_err(format!("invalid UTCTime: {e}")))?,
63        ))
64    } else {
65        Ok(Time::GeneralTime(
66            synta::GeneralizedTime::new(year as u16, month, day, hour, minute, second, None)
67                .map_err(|e| PyValueError::new_err(format!("invalid GeneralizedTime: {e}")))?,
68        ))
69    }
70}
71
72// ── CertificateBuilder ────────────────────────────────────────────────────────
73
74/// Builder for X.509 v3 certificates.
75///
76/// All Name, SubjectPublicKeyInfo, and extension-value bytes are stored as-is
77/// and spliced verbatim into the TBS DER at signing time — no re-parse or
78/// re-encode.  The only allocation per field is the initial copy of the byte
79/// slice into the builder's owned storage.
80///
81/// Each setter method returns the **same** builder object, so calls can be
82/// chained with ``.``:
83///
84/// ```python,ignore
85/// import synta, datetime
86///
87/// now = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
88/// then = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
89///
90/// cert = (
91///     synta.CertificateBuilder()
92///     .issuer_name(ca_cert.subject_raw_der)
93///     .subject_name(csr.subject_raw_der)
94///     .public_key_der(csr.subject_public_key_info_der)
95///     .serial_number(42)
96///     .not_valid_before_utc(now)
97///     .not_valid_after_utc(then)
98///     .add_extension("2.5.29.19", True, bc_der)
99///     .sign(ca_key, "sha256")
100/// )
101/// ```
102#[pyclass(name = "CertificateBuilder")]
103pub struct PyCertificateBuilder {
104    /// Issuer Name TLV (pre-encoded DER).
105    issuer: Option<Vec<u8>>,
106    /// Subject Name TLV (pre-encoded DER).
107    subject: Option<Vec<u8>>,
108    /// SubjectPublicKeyInfo TLV (pre-encoded DER).
109    spki: Option<Vec<u8>>,
110    /// Certificate serial number.
111    serial: Option<Integer>,
112    /// notBefore validity time.
113    not_before: Option<Time>,
114    /// notAfter validity time.
115    not_after: Option<Time>,
116    /// Extensions: (OID, critical, extension-value DER bytes).
117    extensions: Vec<(ObjectIdentifier, bool, Vec<u8>)>,
118}
119
120#[pymethods]
121impl PyCertificateBuilder {
122    /// Create a new, empty ``CertificateBuilder``.
123    #[new]
124    fn new() -> Self {
125        Self {
126            issuer: None,
127            subject: None,
128            spki: None,
129            serial: None,
130            not_before: None,
131            not_after: None,
132            extensions: Vec::new(),
133        }
134    }
135
136    /// Set the issuer Name from pre-encoded DER bytes.
137    ///
138    /// Accepts any bytes object that is a valid DER-encoded ``Name``
139    /// SEQUENCE TLV.  The bytes from ``Certificate.subject_raw_der`` or
140    /// ``Certificate.issuer_raw_der`` are suitable directly:
141    ///
142    /// ```python,ignore
143    /// builder.issuer_name(ca_cert.subject_raw_der)
144    /// ```
145    fn issuer_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
146        slf.borrow_mut().issuer = Some(name_der.to_vec());
147        slf
148    }
149
150    /// Set the subject Name from pre-encoded DER bytes.
151    ///
152    /// ```python,ignore
153    /// builder.subject_name(csr.subject_raw_der)
154    /// ```
155    fn subject_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
156        slf.borrow_mut().subject = Some(name_der.to_vec());
157        slf
158    }
159
160    /// Set the SubjectPublicKeyInfo from a :class:`PublicKey` object.
161    ///
162    /// Serializes the key to SPKI DER once and stores the bytes.
163    ///
164    /// ```python,ignore
165    /// builder.public_key(csr_pub_key)
166    /// ```
167    fn public_key<'py>(
168        slf: Bound<'py, Self>,
169        key: &crate::crypto_keys::PyPublicKey,
170    ) -> PyResult<Bound<'py, Self>> {
171        let der = key
172            .inner
173            .to_der()
174            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
175        slf.borrow_mut().spki = Some(der);
176        Ok(slf)
177    }
178
179    /// Set the SubjectPublicKeyInfo from pre-encoded SPKI DER bytes.
180    ///
181    /// Stores the bytes verbatim — no re-parsing.  Suitable for passing
182    /// ``Certificate.subject_public_key_info_der`` or
183    /// ``csr.subject_public_key_info_der`` directly:
184    ///
185    /// ```python,ignore
186    /// builder.public_key_der(csr.subject_public_key_info_der)
187    /// ```
188    fn public_key_der<'py>(slf: Bound<'py, Self>, spki_der: &[u8]) -> Bound<'py, Self> {
189        slf.borrow_mut().spki = Some(spki_der.to_vec());
190        slf
191    }
192
193    /// Set the certificate serial number.
194    ///
195    /// Accepts a Python ``int`` (any size) or ``bytes``
196    /// (big-endian two's-complement encoding).
197    ///
198    /// ```python,ignore
199    /// import os
200    /// builder.serial_number(42)
201    /// builder.serial_number(int.from_bytes(os.urandom(20), "big"))
202    /// ```
203    fn serial_number<'py>(
204        slf: Bound<'py, Self>,
205        n: &Bound<'py, PyAny>,
206    ) -> PyResult<Bound<'py, Self>> {
207        let serial = if let Ok(i) = n.extract::<i64>() {
208            Integer::from_i64(i)
209        } else if let Ok(b) = n.cast::<PyBytes>() {
210            Integer::from_bytes(b.as_bytes())
211        } else if n.is_instance_of::<pyo3::types::PyInt>() {
212            // Large Python int that doesn't fit in i64 — convert via to_bytes().
213            let bit_length: usize = n.call_method0("bit_length")?.extract()?;
214            let byte_length = bit_length.div_ceil(8).max(1);
215            let bytes_obj = n.call_method1("to_bytes", (byte_length, "big"))?;
216            let b = bytes_obj.cast::<PyBytes>()?;
217            Integer::from_bytes(b.as_bytes())
218        } else {
219            return Err(PyValueError::new_err("serial_number expects int or bytes"));
220        };
221        slf.borrow_mut().serial = Some(serial);
222        Ok(slf)
223    }
224
225    /// Set the ``notBefore`` validity time.
226    ///
227    /// Accepts a timezone-aware ``datetime.datetime`` object (or any object
228    /// with ``year``, ``month``, ``day``, ``hour``, ``minute``, ``second``,
229    /// and ``tzinfo`` attributes).  A naive datetime (``tzinfo=None``) raises
230    /// :exc:`ValueError`.  The time fields are treated as UTC.
231    ///
232    /// ```python,ignore
233    /// import datetime
234    /// builder.not_valid_before_utc(
235    ///     datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
236    /// )
237    /// ```
238    fn not_valid_before_utc<'py>(
239        slf: Bound<'py, Self>,
240        dt: &Bound<'py, PyAny>,
241    ) -> PyResult<Bound<'py, Self>> {
242        slf.borrow_mut().not_before = Some(py_to_synta_time(dt)?);
243        Ok(slf)
244    }
245
246    /// Set the ``notAfter`` validity time.
247    ///
248    /// ```python,ignore
249    /// import datetime
250    /// builder.not_valid_after_utc(
251    ///     datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
252    /// )
253    /// ```
254    fn not_valid_after_utc<'py>(
255        slf: Bound<'py, Self>,
256        dt: &Bound<'py, PyAny>,
257    ) -> PyResult<Bound<'py, Self>> {
258        slf.borrow_mut().not_after = Some(py_to_synta_time(dt)?);
259        Ok(slf)
260    }
261
262    /// Add an X.509 v3 extension.
263    ///
264    /// ``oid`` is the extension OID in dotted-decimal notation.
265    /// ``critical`` is ``True`` if the extension is critical.
266    /// ``value_der`` is the DER-encoded extension value — the raw bytes that
267    /// ``Certificate.get_extension_value_der(oid)`` returns (i.e., the
268    /// content of the OCTET STRING, *not* the OCTET STRING TLV itself).
269    ///
270    /// ```python,ignore
271    /// # Copy an extension from an existing cert:
272    /// bc_der = ca_cert.get_extension_value_der("2.5.29.19")
273    /// builder.add_extension("2.5.29.19", True, bc_der)
274    /// ```
275    fn add_extension<'py>(
276        slf: Bound<'py, Self>,
277        oid: &str,
278        critical: bool,
279        value_der: &[u8],
280    ) -> PyResult<Bound<'py, Self>> {
281        use std::str::FromStr;
282        let oid = ObjectIdentifier::from_str(oid)
283            .map_err(|_| PyValueError::new_err(format!("invalid OID: {oid}")))?;
284        slf.borrow_mut()
285            .extensions
286            .push((oid, critical, value_der.to_vec()));
287        Ok(slf)
288    }
289
290    /// Sign the certificate and return a :class:`Certificate`.
291    ///
292    /// ``key`` is the issuer's :class:`PrivateKey`.  ``algorithm`` is the
293    /// hash algorithm name — one of ``"sha1"``, ``"sha256"``, ``"sha384"``,
294    /// ``"sha512"`` for RSA and ECDSA keys.  For Ed25519 / Ed448 keys the
295    /// argument is ignored (no pre-hash is used).
296    ///
297    /// All required fields (issuer name, subject name, public key, serial
298    /// number, notBefore, notAfter) must have been set; a :exc:`ValueError`
299    /// is raised if any is missing.
300    ///
301    /// Raises :exc:`ValueError` on encoding or signing errors.
302    ///
303    /// ```python,ignore
304    /// cert = builder.sign(ca_key, "sha256")
305    /// ```
306    fn sign<'py>(
307        &self,
308        py: Python<'py>,
309        key: &PyPrivateKey,
310        algorithm: &str,
311    ) -> PyResult<Bound<'py, PyCertificate>> {
312        // Build the Rust-level builder from our stored fields.
313        let mut builder = CertificateBuilder::new();
314        if let Some(ref b) = self.issuer {
315            builder = builder.issuer_name(b);
316        }
317        if let Some(ref b) = self.subject {
318            builder = builder.subject_name(b);
319        }
320        if let Some(ref b) = self.spki {
321            builder = builder.public_key_der(b);
322        }
323        if let Some(ref s) = self.serial {
324            builder = builder.serial_number(s.clone());
325        }
326        if let Some(ref t) = self.not_before {
327            builder = builder.not_valid_before(t.clone());
328        }
329        if let Some(ref t) = self.not_after {
330            builder = builder.not_valid_after(t.clone());
331        }
332        for (oid, critical, value_bytes) in &self.extensions {
333            builder = builder.add_extension(oid.clone(), *critical, value_bytes);
334        }
335
336        // Sign and assemble the certificate DER using the backend-agnostic signer.
337        let signer = key.as_signer(algorithm);
338        let cert_der = builder
339            .sign(&signer)
340            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
341
342        // Parse the assembled DER back into a PyCertificate.
343        let py_bytes = PyBytes::new(py, &cert_der);
344        let cert = PyCertificate::new_from_der(py, py_bytes)?;
345        Py::new(py, cert).map(|py_cert| py_cert.into_bound(py))
346    }
347
348    /// Sign the certificate using the RFC 9925 unsigned algorithm and return a
349    /// :class:`Certificate`.
350    ///
351    /// No private key is required.  The resulting certificate carries
352    /// ``id-alg-unsigned`` (1.3.6.1.5.5.7.6.36) in both
353    /// ``TBSCertificate.signature`` and the outer ``signatureAlgorithm``, with
354    /// a zero-length BIT STRING (``03 01 00``) as the ``signatureValue``.
355    ///
356    /// All required fields (issuer name, subject name, public key, serial
357    /// number, notBefore, notAfter) must have been set; a :exc:`ValueError`
358    /// is raised if any is missing.
359    ///
360    /// ```python,ignore
361    /// # Build an unsigned root CA certificate:
362    /// unsigned_root = (
363    ///     synta.CertificateBuilder()
364    ///     .issuer_name(issuer_name_der)
365    ///     .subject_name(subject_name_der)
366    ///     .public_key(root_key.public_key)
367    ///     .serial_number(1)
368    ///     .not_valid_before_utc(now)
369    ///     .not_valid_after_utc(expires)
370    ///     .add_extension("2.5.29.19", True, bc_der)
371    ///     .sign_unsigned()
372    /// )
373    /// ```
374    fn sign_unsigned<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyCertificate>> {
375        let mut builder = CertificateBuilder::new();
376        if let Some(ref b) = self.issuer {
377            builder = builder.issuer_name(b);
378        }
379        if let Some(ref b) = self.subject {
380            builder = builder.subject_name(b);
381        }
382        if let Some(ref b) = self.spki {
383            builder = builder.public_key_der(b);
384        }
385        if let Some(ref s) = self.serial {
386            builder = builder.serial_number(s.clone());
387        }
388        if let Some(ref t) = self.not_before {
389            builder = builder.not_valid_before(t.clone());
390        }
391        if let Some(ref t) = self.not_after {
392            builder = builder.not_valid_after(t.clone());
393        }
394        for (oid, critical, value_bytes) in &self.extensions {
395            builder = builder.add_extension(oid.clone(), *critical, value_bytes);
396        }
397
398        let cert_der = builder
399            .sign(&UnsignedCertificateSigner)
400            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
401
402        let py_bytes = PyBytes::new(py, &cert_der);
403        let cert = PyCertificate::new_from_der(py, py_bytes)?;
404        Py::new(py, cert).map(|py_cert| py_cert.into_bound(py))
405    }
406
407    fn __repr__(&self) -> String {
408        let subject = self
409            .subject
410            .as_ref()
411            .map(|b| format!("{} bytes", b.len()))
412            .unwrap_or_else(|| "not set".to_string());
413        format!("CertificateBuilder(subject={subject})")
414    }
415}