synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! Fluent builder for the RFC 7773 Authentication Context certificate extension
//! (ACE-88, OID 1.2.752.201.5.1).
//!
//! [`AuthenticationContextsBuilder`] assembles the `AuthenticationContexts`
//! DER SEQUENCE OF for use as the value of the `id-ce-authContext` X.509v3
//! extension.
//!
//! The extension value goes inside the OCTET STRING wrapper of an X.509
//! `Extension` SEQUENCE.
//!
//! # Example
//!
//! ```rust,ignore
//! use synta_certificate::AuthenticationContextsBuilder;
//!
//! let extn_der = AuthenticationContextsBuilder::new()
//!     .add("urn:id:skatteverket:2:1.0", None)
//!     .add("urn:id:skatteverket:1:1.0", Some("https://www.skatteverket.se/ac/context"))
//!     .build()
//!     .unwrap();
//! ```

use synta::traits::Encode;

use crate::ace88_types::AuthenticationContext;
use crate::ext_builder::encode_sequence;

/// Fluent builder for an `AuthenticationContexts` DER SEQUENCE OF
/// (RFC 7773 / ACE-88).
///
/// Each entry is an `AuthenticationContext` with a mandatory `contextType`
/// UTF8String and an optional `contextInfo` UTF8String.
///
/// At least one entry must be added before calling [`build`](Self::build).
#[derive(Debug, Default)]
pub struct AuthenticationContextsBuilder {
    /// Pre-encoded `AuthenticationContext` SEQUENCE TLVs.
    encoded: Vec<u8>,
    /// First error encountered.
    error: Option<String>,
}

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

    /// Add an `AuthenticationContext` entry.
    ///
    /// `context_type` is a URI string identifying the authentication context
    /// (e.g. `"urn:id:skatteverket:2:1.0"`).
    ///
    /// `context_info` is an optional URL pointing to a document describing the
    /// context (e.g. `"https://www.skatteverket.se/ac/context.xml"`).
    pub fn add(mut self, context_type: &str, context_info: Option<&str>) -> Self {
        if self.error.is_some() {
            return self;
        }

        // Utf8StringRef::new() always succeeds (all &str are valid UTF-8).
        let ct = synta::Utf8StringRef::new(context_type);
        let ci: Option<synta::Utf8StringRef<'_>> = context_info.map(synta::Utf8StringRef::new);

        let ctx = AuthenticationContext {
            context_type: ct,
            context_info: ci,
        };

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

    /// Build the DER-encoded `AuthenticationContexts` SEQUENCE OF.
    ///
    /// Returns `Err` if no entries were added or if DER encoding fails.
    pub fn build(self) -> Result<Vec<u8>, String> {
        if let Some(e) = self.error {
            return Err(e);
        }
        if self.encoded.is_empty() {
            return Err("at least one AuthenticationContext entry is required".to_string());
        }
        Ok(encode_sequence(self.encoded))
    }
}

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

    #[test]
    fn build_single_context_no_info() {
        let der = AuthenticationContextsBuilder::new()
            .add("urn:id:skatteverket:2:1.0", None)
            .build()
            .expect("build should succeed");

        // Round-trip: decode as Vec<AuthenticationContext>.
        let decoded: Vec<crate::ace88_types::AuthenticationContext<'_>> =
            synta::Decoder::new(&der, synta::Encoding::Der)
                .decode()
                .expect("round-trip decode failed");
        assert_eq!(decoded.len(), 1);
        assert!(decoded[0].context_info.is_none());
    }

    #[test]
    fn build_two_contexts_with_info() {
        let der = AuthenticationContextsBuilder::new()
            .add("urn:id:ctx:1", Some("https://example.com/ctx1"))
            .add("urn:id:ctx:2", Some("https://example.com/ctx2"))
            .build()
            .expect("build should succeed");

        let decoded: Vec<crate::ace88_types::AuthenticationContext<'_>> =
            synta::Decoder::new(&der, synta::Encoding::Der)
                .decode()
                .expect("round-trip decode failed");
        assert_eq!(decoded.len(), 2);
        assert!(decoded[0].context_info.is_some());
        assert!(decoded[1].context_info.is_some());
    }

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