synta-python 0.2.5

Python extension module for the synta ASN.1 library
Documentation
//! Python bindings for RFC 8737 ACME TLS-ALPN-01 extension types.
//!
//! Exposes `AcmeAuthorization` and the `ID_PE_ACME_IDENTIFIER` OID constant
//! into the `synta.acme` submodule.
//!
//! RFC 8737 §3 defines a critical X.509 extension (`id-pe-acmeIdentifier`,
//! OID `1.3.6.1.5.5.7.1.31`) whose value is a 32-byte SHA-256 digest of
//! the ACME key authorization string for the TLS-ALPN-01 challenge.

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

use pyo3::prelude::*;
use pyo3::types::PyBytes;

use synta::OctetString;
use synta_certificate::acme_types::Authorization;

use crate::error::SyntaErr;

// ── PyAcmeAuthorization ───────────────────────────────────────────────────────

/// ACME TLS-ALPN-01 authorization digest (RFC 8737 §3).
///
/// Wraps the 32-byte SHA-256 digest that forms the ``Authorization``
/// ``OCTET STRING (SIZE (32))`` extension value for the ``id-pe-acmeIdentifier``
/// extension (OID ``1.3.6.1.5.5.7.1.31``).
///
/// The digest is computed as::
///
///     key_authorization = token + "." + base64url(thumbprint(account_key))
///     digest = SHA-256(key_authorization.encode("ascii"))
///
/// The DER encoding of an ``AcmeAuthorization`` is the OCTET STRING TLV:
/// ``04 20 <32 bytes>``.
///
/// Example::
///
///     import synta
///     import synta.acme as acme
///
///     digest = synta.digest("sha256", b"token.thumbprint")
///     auth = acme.AcmeAuthorization(digest)
///     print(auth.hex_digest)
///     der = auth.to_der()
///     auth2 = acme.AcmeAuthorization.from_der(der)
///     assert bytes(auth2) == digest
#[pyclass(frozen, name = "AcmeAuthorization", module = "synta.acme")]
pub struct PyAcmeAuthorization {
    /// Stored directly to avoid re-encoding overhead on every `to_der` call.
    digest: [u8; 32],
}

#[pymethods]
impl PyAcmeAuthorization {
    /// Construct an ``AcmeAuthorization`` from 32 raw digest bytes.
    ///
    /// :param digest: exactly 32 bytes (the SHA-256 output).
    /// :raises ValueError: if ``digest`` is not exactly 32 bytes.
    #[new]
    fn new(digest: &[u8]) -> PyResult<Self> {
        if digest.len() != 32 {
            return Err(pyo3::exceptions::PyValueError::new_err(format!(
                "AcmeAuthorization requires exactly 32 bytes, got {}",
                digest.len()
            )));
        }
        let mut arr = [0u8; 32];
        arr.copy_from_slice(digest);
        Ok(Self { digest: arr })
    }

    /// Parse a DER-encoded ``Authorization`` OCTET STRING (04 20 ...).
    ///
    /// Accepts exactly 34 bytes — the DER encoding of ``OCTET STRING (SIZE (32))``:
    /// tag ``0x04``, length ``0x20`` (32), followed by the 32-byte digest.
    /// Extra trailing bytes are rejected.
    ///
    /// :param data: exactly 34-byte DER buffer.
    /// :raises ValueError: if the bytes cannot be decoded, are the wrong size,
    ///     or contain trailing data after the OCTET STRING.
    #[staticmethod]
    fn from_der(data: &[u8]) -> PyResult<Self> {
        if data.len() != 34 {
            return Err(pyo3::exceptions::PyValueError::new_err(format!(
                "AcmeAuthorization.from_der: expected 34-byte DER \
                 (04 20 <32-byte SHA-256 digest>), got {} bytes. \
                 Pass the raw extnValue contents, not a wrapped SEQUENCE.",
                data.len()
            )));
        }
        let auth = Authorization::from_der(data).map_err(SyntaErr)?;
        let bytes = auth.get().as_bytes();
        let mut arr = [0u8; 32];
        arr.copy_from_slice(bytes);
        Ok(Self { digest: arr })
    }

    /// Return the DER encoding of this ``Authorization`` value.
    ///
    /// The encoding is the OCTET STRING TLV: ``04 20 <32 bytes>``.
    ///
    /// :returns: ``bytes`` of length 34 (tag + length + 32-byte digest).
    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
        let auth = Authorization::new_unchecked(OctetString::new(self.digest.to_vec()));
        auth.to_der()
            .map(|bytes| PyBytes::new(py, &bytes))
            .map_err(|e| {
                pyo3::exceptions::PyRuntimeError::new_err(format!(
                    "AcmeAuthorization.to_der: DER encoder error: {:?}",
                    e
                ))
            })
    }

    /// The raw 32-byte digest as :class:`bytes`.
    ///
    /// Equivalent to ``bytes(auth)`` via :meth:`__bytes__`.
    #[getter]
    fn digest<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.digest)
    }

    /// Lowercase hexadecimal representation of the 32-byte digest (64 characters).
    #[getter]
    fn hex_digest(&self) -> String {
        synta::bytes_to_hex(&self.digest)
    }

    /// Return the raw 32-byte digest as :class:`bytes`.
    fn __bytes__<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.digest)
    }

    fn __repr__(&self) -> String {
        format!("AcmeAuthorization(digest={})", self.hex_digest())
    }

    fn __eq__(&self, other: &Self) -> bool {
        self.digest == other.digest
    }

    fn __hash__(&self) -> u64 {
        let mut h = DefaultHasher::new();
        self.digest.hash(&mut h);
        h.finish()
    }

    fn __len__(&self) -> usize {
        self.digest.len()
    }
}

// ── Module registration ───────────────────────────────────────────────────────

/// Register the ``synta.acme`` submodule.
///
/// Exposes:
/// - ``AcmeAuthorization`` — the 32-byte ACME key-authorization digest.
/// - ``ID_PE_ACME_IDENTIFIER`` — the ``id-pe-acmeIdentifier`` OID
///   (``1.3.6.1.5.5.7.1.31``, RFC 8737 §3).
///   Also accessible as ``synta.oids.PE_ACME_IDENTIFIER``.
pub(super) fn register_acme_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
    let py = parent.py();
    let m = PyModule::new(py, "acme")?;

    m.add_class::<PyAcmeAuthorization>()?;

    // RFC 8737 §3: id-pe-acmeIdentifier OID (1.3.6.1.5.5.7.1.31)
    m.add(
        "ID_PE_ACME_IDENTIFIER",
        super::oid_const(py, synta_certificate::oids::PE_ACME_IDENTIFIER),
    )?;

    crate::install_submodule(
        parent,
        &m,
        "synta.acme",
        Some(concat!(
            "synta.acme — RFC 8737 ACME TLS-ALPN-01 extension types.\n\n",
            "Provides AcmeAuthorization for the id-pe-acmeIdentifier extension\n",
            "(OID 1.3.6.1.5.5.7.1.31) used during ACME TLS-ALPN-01 domain\n",
            "validation.  The extension value is a 32-byte SHA-256 digest of\n",
            "the ACME key authorization string: SHA-256(token + '.' + base64url(thumbprint)).",
        )),
    )
}