Skip to main content

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