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