synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! Fluent builder for the RFC 9399 Logotype certificate extension (OID 1.3.6.1.5.5.7.1.12).
//!
//! [`LogotypeExtnBuilder`] assembles a `LogotypeExtn` DER structure for use as
//! the value of the `id-pe-logotype` X.509v3 extension (OID 1.3.6.1.5.5.7.1.12).
//!
//! The extension value goes inside the OCTET STRING wrapper of an X.509
//! `Extension` SEQUENCE.
//!
//! # Example
//!
//! ```rust,ignore
//! use synta_certificate::{LogotypeExtnBuilder, LogotypeDetailsSpec, oids};
//!
//! // Hash of the logotype image
//! let img_hash = [0u8; 32];
//!
//! let extn_der = LogotypeExtnBuilder::new()
//!     .subject_logo_direct(LogotypeDetailsSpec {
//!         media_type: "image/png",
//!         hash_alg_oid: oids::ID_SHA256,
//!         hash_value: &img_hash,
//!         uri: "https://www.example.com/logo.png",
//!     })
//!     .build()
//!     .unwrap();
//! ```

use synta::traits::Encode;

use crate::ext_builder::encode_sequence;
use crate::logotype_cert_extn_types::{
    HashAlgAndValue, LogotypeData, LogotypeDetails, LogotypeExtn, LogotypeImage, LogotypeInfo,
    OtherLogotypeInfo,
};

// ── LogotypeDetailsSpec ───────────────────────────────────────────────────────

/// Input specification for a `LogotypeDetails` structure.
///
/// Describes a single logotype image object: its MIME type, one hash
/// (algorithm OID + raw hash bytes), and one retrieval URI.
#[derive(Debug, Clone, Copy)]
pub struct LogotypeDetailsSpec<'a> {
    /// MIME type string (IA5String), e.g. `"image/png"`.
    pub media_type: &'a str,
    /// Hash algorithm OID component slice, e.g. [`crate::oids::ID_SHA256`].
    pub hash_alg_oid: &'a [u32],
    /// Raw hash bytes (the hash value of the logotype object).
    pub hash_value: &'a [u8],
    /// URI from which the logotype object can be retrieved (IA5String).
    pub uri: &'a str,
}

// ── LogotypeExtnBuilder ───────────────────────────────────────────────────────

/// Fluent builder for a `LogotypeExtn` DER SEQUENCE (RFC 9399).
///
/// At least one field (`communityLogos`, `issuerLogo`, `subjectLogo`, or
/// `otherLogos`) must be set before calling [`build`](Self::build).
#[derive(Debug, Default)]
pub struct LogotypeExtnBuilder {
    /// Pre-encoded `LogotypeInfo` TLVs for `communityLogos [0] EXPLICIT`.
    community_logos_encoded: Vec<u8>,
    /// Pre-encoded `LogotypeInfo` TLV for `issuerLogo [1] EXPLICIT`.
    issuer_logo: Option<Vec<u8>>,
    /// Pre-encoded `LogotypeInfo` TLV for `subjectLogo [2] EXPLICIT`.
    subject_logo: Option<Vec<u8>>,
    /// Pre-encoded `OtherLogotypeInfo` TLVs for `otherLogos [3] EXPLICIT`.
    other_logos_encoded: Vec<u8>,
    /// First error encountered.
    error: Option<String>,
}

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

    /// Set the `issuerLogo` to a direct `LogotypeData` with one image.
    ///
    /// The image is described by a single [`LogotypeDetailsSpec`].
    pub fn issuer_logo_direct(mut self, details: LogotypeDetailsSpec<'_>) -> Self {
        if self.error.is_some() {
            return self;
        }
        match encode_logotype_info_direct_image(&details) {
            Ok(bytes) => self.issuer_logo = Some(bytes),
            Err(e) => self.error = Some(e),
        }
        self
    }

    /// Set the `subjectLogo` to a direct `LogotypeData` with one image.
    ///
    /// The image is described by a single [`LogotypeDetailsSpec`].
    pub fn subject_logo_direct(mut self, details: LogotypeDetailsSpec<'_>) -> Self {
        if self.error.is_some() {
            return self;
        }
        match encode_logotype_info_direct_image(&details) {
            Ok(bytes) => self.subject_logo = Some(bytes),
            Err(e) => self.error = Some(e),
        }
        self
    }

    /// Add one community logo (direct `LogotypeData` with one image) to
    /// the `communityLogos` list.
    pub fn add_community_logo_direct(mut self, details: LogotypeDetailsSpec<'_>) -> Self {
        if self.error.is_some() {
            return self;
        }
        match encode_logotype_info_direct_image(&details) {
            Ok(bytes) => self.community_logos_encoded.extend_from_slice(&bytes),
            Err(e) => self.error = Some(e),
        }
        self
    }

    /// Add an `OtherLogotypeInfo` entry with a direct `LogotypeData`.
    ///
    /// `logotype_type_oid` identifies the logotype type (e.g.
    /// [`crate::logotype_cert_extn_types::ID_LOGO_LOYALTY`],
    /// [`crate::logotype_cert_extn_types::ID_LOGO_BACKGROUND`], or
    /// [`crate::cert_image_module_types::ID_LOGO_CERT_IMAGE`]).
    pub fn add_other_logo_direct(
        mut self,
        logotype_type_oid: &[u32],
        details: LogotypeDetailsSpec<'_>,
    ) -> Self {
        if self.error.is_some() {
            return self;
        }
        let info_bytes = match encode_logotype_info_direct_image(&details) {
            Ok(b) => b,
            Err(e) => {
                self.error = Some(e);
                return self;
            }
        };

        let info: LogotypeInfo<'_> = match synta::Decoder::new(&info_bytes, synta::Encoding::Der)
            .decode::<LogotypeInfo<'_>>()
        {
            Ok(li) => li,
            Err(e) => {
                self.error = Some(format!("LogotypeInfo re-decode failed: {e}"));
                return self;
            }
        };

        let logotype_type = match synta::ObjectIdentifier::new(logotype_type_oid) {
            Ok(oid) => oid,
            Err(e) => {
                self.error = Some(format!("invalid logotype type OID: {e}"));
                return self;
            }
        };

        let other = OtherLogotypeInfo {
            logotype_type,
            info,
        };

        let mut enc = synta::Encoder::new(synta::Encoding::Der);
        match other.encode(&mut enc).and_then(|()| enc.finish()) {
            Ok(bytes) => self.other_logos_encoded.extend_from_slice(&bytes),
            Err(e) => {
                self.error = Some(format!("OtherLogotypeInfo DER encoding failed: {e}"));
            }
        }
        self
    }

    /// Build the DER-encoded `LogotypeExtn` SEQUENCE.
    ///
    /// Returns `Err` if no logotype fields were set or if DER encoding fails.
    pub fn build(self) -> Result<Vec<u8>, String> {
        if let Some(e) = self.error {
            return Err(e);
        }

        let has_community = !self.community_logos_encoded.is_empty();
        let has_issuer = self.issuer_logo.is_some();
        let has_subject = self.subject_logo.is_some();
        let has_other = !self.other_logos_encoded.is_empty();

        if !has_community && !has_issuer && !has_subject && !has_other {
            return Err(
                "at least one of communityLogos, issuerLogo, subjectLogo, or otherLogos must be set"
                    .to_string(),
            );
        }

        // Pre-allocate byte storage that must outlive the decoded borrows.
        let community_seq_storage: Vec<u8>;
        let other_seq_storage: Vec<u8>;

        let community_logos: Option<Vec<LogotypeInfo<'_>>>;
        let issuer_logo_bytes = self.issuer_logo;
        let issuer_logo: Option<LogotypeInfo<'_>>;
        let subject_logo_bytes = self.subject_logo;
        let subject_logo: Option<LogotypeInfo<'_>>;
        let other_logos: Option<Vec<OtherLogotypeInfo<'_>>>;

        if has_community {
            community_seq_storage = encode_sequence(self.community_logos_encoded);
            let logos: Vec<LogotypeInfo<'_>> =
                synta::Decoder::new(&community_seq_storage, synta::Encoding::Der)
                    .decode::<Vec<LogotypeInfo<'_>>>()
                    .map_err(|e| format!("communityLogos Vec decode failed: {e}"))?;
            community_logos = Some(logos);
        } else {
            community_logos = None;
        }

        if let Some(ref bytes) = issuer_logo_bytes {
            issuer_logo = Some(
                synta::Decoder::new(bytes, synta::Encoding::Der)
                    .decode::<LogotypeInfo<'_>>()
                    .map_err(|e| format!("issuerLogo re-decode failed: {e}"))?,
            );
        } else {
            issuer_logo = None;
        }

        if let Some(ref bytes) = subject_logo_bytes {
            subject_logo = Some(
                synta::Decoder::new(bytes, synta::Encoding::Der)
                    .decode::<LogotypeInfo<'_>>()
                    .map_err(|e| format!("subjectLogo re-decode failed: {e}"))?,
            );
        } else {
            subject_logo = None;
        }

        if has_other {
            other_seq_storage = encode_sequence(self.other_logos_encoded);
            let logos: Vec<OtherLogotypeInfo<'_>> =
                synta::Decoder::new(&other_seq_storage, synta::Encoding::Der)
                    .decode::<Vec<OtherLogotypeInfo<'_>>>()
                    .map_err(|e| format!("otherLogos Vec decode failed: {e}"))?;
            other_logos = Some(logos);
        } else {
            other_logos = None;
        }

        let extn = LogotypeExtn {
            community_logos,
            issuer_logo,
            subject_logo,
            other_logos,
        };

        let mut enc = synta::Encoder::new(synta::Encoding::Der);
        extn.encode(&mut enc)
            .map_err(|e| format!("LogotypeExtn DER encoding failed: {e}"))?;
        enc.finish().map_err(|e| format!("DER finish failed: {e}"))
    }
}

// ── helpers ───────────────────────────────────────────────────────────────────

/// Encode a `LogotypeInfo::Direct` containing a single image from a spec.
fn encode_logotype_info_direct_image(details: &LogotypeDetailsSpec<'_>) -> Result<Vec<u8>, String> {
    let hash_oid = synta::ObjectIdentifier::new(details.hash_alg_oid)
        .map_err(|e| format!("invalid hash algorithm OID: {e}"))?;
    let hash_alg = crate::AlgorithmIdentifier {
        algorithm: hash_oid,
        parameters: Some(synta::Element::Null(synta::Null)),
    };
    let hash_value = synta::OctetStringRef::new(details.hash_value);
    let hash_alg_and_value = HashAlgAndValue {
        hash_alg,
        hash_value,
    };

    let media_type = synta::IA5StringRef::new(details.media_type)
        .map_err(|e| format!("media_type is not valid IA5String: {e}"))?;
    let uri = synta::IA5StringRef::new(details.uri)
        .map_err(|e| format!("URI is not valid IA5String: {e}"))?;

    let logotype_details = LogotypeDetails {
        media_type,
        logotype_hash: vec![hash_alg_and_value],
        logotype_uri: vec![uri],
    };

    let logotype_image = LogotypeImage {
        image_details: logotype_details,
        image_info: None,
    };

    let logotype_data = LogotypeData {
        image: Some(vec![logotype_image]),
        audio: None,
    };

    let info = LogotypeInfo::Direct(logotype_data);

    let mut enc = synta::Encoder::new(synta::Encoding::Der);
    info.encode(&mut enc)
        .map_err(|e| format!("LogotypeInfo DER encoding failed: {e}"))?;
    enc.finish().map_err(|e| format!("DER finish failed: {e}"))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_spec() -> LogotypeDetailsSpec<'static> {
        LogotypeDetailsSpec {
            media_type: "image/png",
            hash_alg_oid: crate::oids::ID_SHA256,
            hash_value: &[0xabu8; 32],
            uri: "https://example.com/logo.png",
        }
    }

    #[test]
    fn build_subject_logo() {
        let der = LogotypeExtnBuilder::new()
            .subject_logo_direct(test_spec())
            .build()
            .expect("build should succeed");

        let decoded = crate::logotype_cert_extn_types::LogotypeExtn::from_der(&der)
            .expect("round-trip decode failed");
        assert!(decoded.subject_logo.is_some());
        assert!(decoded.issuer_logo.is_none());
        assert!(decoded.community_logos.is_none());
    }

    #[test]
    fn build_issuer_logo() {
        let der = LogotypeExtnBuilder::new()
            .issuer_logo_direct(test_spec())
            .build()
            .expect("build should succeed");

        let decoded = crate::logotype_cert_extn_types::LogotypeExtn::from_der(&der)
            .expect("round-trip decode failed");
        assert!(decoded.issuer_logo.is_some());
    }

    #[test]
    fn build_community_logos() {
        let der = LogotypeExtnBuilder::new()
            .add_community_logo_direct(test_spec())
            .add_community_logo_direct(test_spec())
            .build()
            .expect("build should succeed");

        let decoded = crate::logotype_cert_extn_types::LogotypeExtn::from_der(&der)
            .expect("round-trip decode failed");
        assert_eq!(decoded.community_logos.as_ref().map(|v| v.len()), Some(2));
    }

    #[test]
    fn build_other_logo() {
        let der = LogotypeExtnBuilder::new()
            .add_other_logo_direct(
                crate::logotype_cert_extn_types::ID_LOGO_BACKGROUND,
                test_spec(),
            )
            .build()
            .expect("build should succeed");

        let decoded = crate::logotype_cert_extn_types::LogotypeExtn::from_der(&der)
            .expect("round-trip decode failed");
        assert_eq!(decoded.other_logos.as_ref().map(|v| v.len()), Some(1));
    }

    #[test]
    fn build_without_any_logo_returns_error() {
        let err = LogotypeExtnBuilder::new().build();
        assert!(err.is_err());
    }
}