synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! Builder for RFC 5755 Attribute Certificate TBS DER.
//!
//! [`AttributeCertificateBuilder`] assembles the `AttributeCertificateInfo`
//! (TBS) portion of an Attribute Certificate from individual fields using a
//! fluent, chainable API.
//!
//! The builder encodes only the inner `AttributeCertificateInfo` SEQUENCE;
//! callers are responsible for adding a signature algorithm identifier and
//! signature bit string to form the complete `AttributeCertificate`.
//!
//! # Example
//!
//! ```rust,ignore
//! use synta_certificate::AttributeCertificateBuilder;
//!
//! let tbs_der = AttributeCertificateBuilder::new()
//!     .serial_number(42)
//!     .not_before("20240101120000Z")
//!     .not_after("20250101120000Z")
//!     .issuer_rfc822("ca@example.com")
//!     .holder_entity_name_rfc822("user@example.com")
//!     .build()
//!     .unwrap();
//! ```

use synta::GeneralizedTime;

use crate::attribute_cert_types::{
    AttCertIssuer, AttCertValidityPeriod, AttributeCertificateInfo, Holder,
};
use crate::time_utils::parse_generalized_time;
use crate::GeneralName;

// ── AttributeCertificateBuilder ───────────────────────────────────────────────

/// Fluent builder for the `AttributeCertificateInfo` TBS DER (RFC 5755).
///
/// At minimum, set `serial_number`, `not_before`, `not_after`, then call
/// [`build`][Self::build].  Use `issuer_rfc822` / `issuer_dns` to set the AC
/// issuer `v1Form` GeneralNames, and `holder_entity_name_rfc822` to set the
/// holder's `entityName`.
///
/// The builder produces only the `AttributeCertificateInfo` SEQUENCE (the TBS
/// portion).  A real AC would additionally need a signature algorithm and
/// signature bit string.
#[derive(Default)]
pub struct AttributeCertificateBuilder {
    serial: Option<i64>,
    not_before: Option<GeneralizedTime>,
    not_after: Option<GeneralizedTime>,
    /// GeneralName entries for the `AttCertIssuer` `v1Form` (`GeneralNames`).
    issuer_names: Vec<GeneralName<'static>>,
    /// GeneralName entries for the `Holder.entityName`.
    holder_entity_names: Vec<GeneralName<'static>>,
    error: Option<String>,
}

impl AttributeCertificateBuilder {
    /// Create a new, empty builder.
    pub fn new() -> Self {
        Self::default()
    }

    /// Record the first error; subsequent mutation calls are no-ops.
    fn set_error(&mut self, msg: String) {
        if self.error.is_none() {
            self.error = Some(msg);
        }
    }

    /// Set the AC serial number.
    pub fn serial_number(mut self, n: i64) -> Self {
        self.serial = Some(n);
        self
    }

    /// Set the `notBeforeTime` from a GeneralizedTime string (`"YYYYMMDDHHmmssZ"`).
    ///
    /// Records an error if the string is not a valid GeneralizedTime.
    pub fn not_before(mut self, s: &str) -> Self {
        match parse_generalized_time(s) {
            Ok(t) => self.not_before = Some(t),
            Err(e) => self.set_error(e),
        }
        self
    }

    /// Set the `notAfterTime` from a GeneralizedTime string (`"YYYYMMDDHHmmssZ"`).
    pub fn not_after(mut self, s: &str) -> Self {
        match parse_generalized_time(s) {
            Ok(t) => self.not_after = Some(t),
            Err(e) => self.set_error(e),
        }
        self
    }

    /// Add an `rfc822Name [1]` GeneralName to the `AttCertIssuer` v1Form.
    pub fn issuer_rfc822(mut self, email: &str) -> Self {
        match synta::IA5String::new(email.to_string()) {
            Ok(ia5) => {
                // IA5String contains only owned String; safe to transmute lifetime.
                let gn: GeneralName<'static> =
                    unsafe { std::mem::transmute(GeneralName::Rfc822Name(ia5)) };
                self.issuer_names.push(gn);
            }
            Err(e) => self.set_error(format!("issuer_rfc822: invalid IA5String: {e}")),
        }
        self
    }

    /// Add a `dNSName [2]` GeneralName to the `AttCertIssuer` v1Form.
    pub fn issuer_dns(mut self, name: &str) -> Self {
        match synta::IA5String::new(name.to_string()) {
            Ok(ia5) => {
                let gn: GeneralName<'static> =
                    unsafe { std::mem::transmute(GeneralName::DNSName(ia5)) };
                self.issuer_names.push(gn);
            }
            Err(e) => self.set_error(format!("issuer_dns: invalid IA5String: {e}")),
        }
        self
    }

    /// Add an `rfc822Name [1]` GeneralName to the `Holder.entityName`.
    pub fn holder_entity_name_rfc822(mut self, email: &str) -> Self {
        match synta::IA5String::new(email.to_string()) {
            Ok(ia5) => {
                let gn: GeneralName<'static> =
                    unsafe { std::mem::transmute(GeneralName::Rfc822Name(ia5)) };
                self.holder_entity_names.push(gn);
            }
            Err(e) => self.set_error(format!("holder_entity_name_rfc822: invalid IA5String: {e}")),
        }
        self
    }

    /// Encode the `AttributeCertificateInfo` SEQUENCE to DER bytes.
    ///
    /// Returns `Err(String)` if any required field is missing or encoding fails.
    ///
    /// Required fields: `serial_number`, `not_before`, `not_after`.
    pub fn build(self) -> Result<Vec<u8>, String> {
        if let Some(e) = self.error {
            return Err(e);
        }

        let serial = self.serial.ok_or("serial_number is required")?;
        if serial <= 0 {
            return Err("serial_number must be a positive integer (RFC 5280 §4.1.2.2)".into());
        }
        let not_before = self.not_before.ok_or("not_before is required")?;
        let not_after = self.not_after.ok_or("not_after is required")?;

        // AttCertIssuer: v1Form (GeneralNames)
        let issuer = AttCertIssuer::V1Form(self.issuer_names);

        // Holder: entityName [1] GeneralNames (optional)
        let holder = Holder {
            base_certificate_id: None,
            entity_name: if self.holder_entity_names.is_empty() {
                None
            } else {
                Some(self.holder_entity_names)
            },
            object_digest_info: None,
        };

        // Placeholder signature algorithm — callers set the real algorithm
        // when building a signed AC.  We use sha256WithRSAEncryption here.
        let sig_alg_oid = synta::ObjectIdentifier::new(crate::oids::SHA256_WITH_RSA)
            .map_err(|e| format!("sig alg OID error: {e}"))?;
        let sig_alg = crate::AlgorithmIdentifier {
            algorithm: sig_alg_oid,
            parameters: None,
        };

        let acinfo = AttributeCertificateInfo {
            version: synta::Integer::from(1i64),
            holder,
            issuer,
            signature: sig_alg,
            serial_number: synta::Integer::from(serial),
            attr_cert_validity_period: AttCertValidityPeriod {
                not_before_time: not_before,
                not_after_time: not_after,
            },
            attributes: Vec::new(),
            issuer_unique_id: None,
            extensions: None,
        };

        acinfo
            .to_der()
            .map_err(|e| format!("AttributeCertificateInfo encode error: {e}"))
    }
}