_synta/certificate/acme.rs
1//! Python bindings for RFC 8737 ACME TLS-ALPN-01 extension types.
2//!
3//! Exposes `AcmeAuthorization` and the `ID_PE_ACME_IDENTIFIER` OID constant
4//! into the `synta.acme` submodule.
5//!
6//! RFC 8737 §3 defines a critical X.509 extension (`id-pe-acmeIdentifier`,
7//! OID `1.3.6.1.5.5.7.1.31`) whose value is a 32-byte SHA-256 digest of
8//! the ACME key authorization string for the TLS-ALPN-01 challenge.
9
10use std::collections::hash_map::DefaultHasher;
11use std::hash::{Hash, Hasher};
12
13use pyo3::prelude::*;
14use pyo3::types::PyBytes;
15
16use synta::OctetString;
17use synta_certificate::acme_types::Authorization;
18
19use crate::error::SyntaErr;
20
21// ── PyAcmeAuthorization ───────────────────────────────────────────────────────
22
23/// ACME TLS-ALPN-01 authorization digest (RFC 8737 §3).
24///
25/// Wraps the 32-byte SHA-256 digest that forms the ``Authorization``
26/// ``OCTET STRING (SIZE (32))`` extension value for the ``id-pe-acmeIdentifier``
27/// extension (OID ``1.3.6.1.5.5.7.1.31``).
28///
29/// The digest is computed as::
30///
31/// key_authorization = token + "." + base64url(thumbprint(account_key))
32/// digest = SHA-256(key_authorization.encode("ascii"))
33///
34/// The DER encoding of an ``AcmeAuthorization`` is the OCTET STRING TLV:
35/// ``04 20 <32 bytes>``.
36///
37/// Example::
38///
39/// import synta
40/// import synta.acme as acme
41///
42/// digest = synta.digest("sha256", b"token.thumbprint")
43/// auth = acme.AcmeAuthorization(digest)
44/// print(auth.hex_digest)
45/// der = auth.to_der()
46/// auth2 = acme.AcmeAuthorization.from_der(der)
47/// assert bytes(auth2) == digest
48#[pyclass(frozen, name = "AcmeAuthorization", module = "synta.acme")]
49pub struct PyAcmeAuthorization {
50 /// Stored directly to avoid re-encoding overhead on every `to_der` call.
51 digest: [u8; 32],
52}
53
54#[pymethods]
55impl PyAcmeAuthorization {
56 /// Construct an ``AcmeAuthorization`` from 32 raw digest bytes.
57 ///
58 /// :param digest: exactly 32 bytes (the SHA-256 output).
59 /// :raises ValueError: if ``digest`` is not exactly 32 bytes.
60 #[new]
61 fn new(digest: &[u8]) -> PyResult<Self> {
62 if digest.len() != 32 {
63 return Err(pyo3::exceptions::PyValueError::new_err(format!(
64 "AcmeAuthorization requires exactly 32 bytes, got {}",
65 digest.len()
66 )));
67 }
68 let mut arr = [0u8; 32];
69 arr.copy_from_slice(digest);
70 Ok(Self { digest: arr })
71 }
72
73 /// Parse a DER-encoded ``Authorization`` OCTET STRING (04 20 ...).
74 ///
75 /// Accepts exactly 34 bytes — the DER encoding of ``OCTET STRING (SIZE (32))``:
76 /// tag ``0x04``, length ``0x20`` (32), followed by the 32-byte digest.
77 /// Extra trailing bytes are rejected.
78 ///
79 /// :param data: exactly 34-byte DER buffer.
80 /// :raises ValueError: if the bytes cannot be decoded, are the wrong size,
81 /// or contain trailing data after the OCTET STRING.
82 #[staticmethod]
83 fn from_der(data: &[u8]) -> PyResult<Self> {
84 if data.len() != 34 {
85 return Err(pyo3::exceptions::PyValueError::new_err(format!(
86 "AcmeAuthorization.from_der: expected 34-byte DER \
87 (04 20 <32-byte SHA-256 digest>), got {} bytes. \
88 Pass the raw extnValue contents, not a wrapped SEQUENCE.",
89 data.len()
90 )));
91 }
92 let auth = Authorization::from_der(data).map_err(SyntaErr)?;
93 let bytes = auth.get().as_bytes();
94 let mut arr = [0u8; 32];
95 arr.copy_from_slice(bytes);
96 Ok(Self { digest: arr })
97 }
98
99 /// Return the DER encoding of this ``Authorization`` value.
100 ///
101 /// The encoding is the OCTET STRING TLV: ``04 20 <32 bytes>``.
102 ///
103 /// :returns: ``bytes`` of length 34 (tag + length + 32-byte digest).
104 fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
105 let auth = Authorization::new_unchecked(OctetString::new(self.digest.to_vec()));
106 auth.to_der()
107 .map(|bytes| PyBytes::new(py, &bytes))
108 .map_err(|e| {
109 pyo3::exceptions::PyRuntimeError::new_err(format!(
110 "AcmeAuthorization.to_der: DER encoder error: {:?}",
111 e
112 ))
113 })
114 }
115
116 /// The raw 32-byte digest as :class:`bytes`.
117 ///
118 /// Equivalent to ``bytes(auth)`` via :meth:`__bytes__`.
119 #[getter]
120 fn digest<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
121 PyBytes::new(py, &self.digest)
122 }
123
124 /// Lowercase hexadecimal representation of the 32-byte digest (64 characters).
125 #[getter]
126 fn hex_digest(&self) -> String {
127 synta::bytes_to_hex(&self.digest)
128 }
129
130 /// Return the raw 32-byte digest as :class:`bytes`.
131 fn __bytes__<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
132 PyBytes::new(py, &self.digest)
133 }
134
135 fn __repr__(&self) -> String {
136 format!("AcmeAuthorization(digest={})", self.hex_digest())
137 }
138
139 fn __eq__(&self, other: &Self) -> bool {
140 self.digest == other.digest
141 }
142
143 fn __hash__(&self) -> u64 {
144 let mut h = DefaultHasher::new();
145 self.digest.hash(&mut h);
146 h.finish()
147 }
148
149 fn __len__(&self) -> usize {
150 self.digest.len()
151 }
152}
153
154// ── Module registration ───────────────────────────────────────────────────────
155
156/// Register the ``synta.acme`` submodule.
157///
158/// Exposes:
159/// - ``AcmeAuthorization`` — the 32-byte ACME key-authorization digest.
160/// - ``ID_PE_ACME_IDENTIFIER`` — the ``id-pe-acmeIdentifier`` OID
161/// (``1.3.6.1.5.5.7.1.31``, RFC 8737 §3).
162/// Also accessible as ``synta.oids.PE_ACME_IDENTIFIER``.
163pub(super) fn register_acme_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
164 let py = parent.py();
165 let m = PyModule::new(py, "acme")?;
166
167 m.add_class::<PyAcmeAuthorization>()?;
168
169 // RFC 8737 §3: id-pe-acmeIdentifier OID (1.3.6.1.5.5.7.1.31)
170 m.add(
171 "ID_PE_ACME_IDENTIFIER",
172 super::oid_const(py, synta_certificate::oids::PE_ACME_IDENTIFIER),
173 )?;
174
175 crate::install_submodule(
176 parent,
177 &m,
178 "synta.acme",
179 Some(concat!(
180 "synta.acme — RFC 8737 ACME TLS-ALPN-01 extension types.\n\n",
181 "Provides AcmeAuthorization for the id-pe-acmeIdentifier extension\n",
182 "(OID 1.3.6.1.5.5.7.1.31) used during ACME TLS-ALPN-01 domain\n",
183 "validation. The extension value is a 32-byte SHA-256 digest of\n",
184 "the ACME key authorization string: SHA-256(token + '.' + base64url(thumbprint)).",
185 )),
186 )
187}