synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! Fluent builder for DER-encoded `TimeStampReq` (RFC 3161 §2.4.1).
//!
//! [`TimeStampReqBuilder`] assembles a complete `TimeStampReq` DER blob.
//! The caller supplies the hash algorithm OID (as a `&[u32]` component slice,
//! e.g. [`crate::oids::ID_SHA256`]) and the raw hash bytes.
//!
//! # Example
//!
//! ```rust,ignore
//! use synta_certificate::{TimeStampReqBuilder, oids};
//!
//! let hash = sha256_of(data);
//! let req_der = TimeStampReqBuilder::new()
//!     .message_imprint(oids::ID_SHA256, &hash)
//!     .nonce(b"\x01\x02\x03\x04\x05\x06\x07\x08")
//!     .cert_req(true)
//!     .build()
//!     .unwrap();
//! ```

use synta::traits::Encode;

use crate::tsp_types::{MessageImprint, TimeStampReq};

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

/// Encode a bare `AlgorithmIdentifier { algorithm OID, parameters NULL }` to DER.
fn encode_alg_id_null(oid_comps: &[u32]) -> Result<Vec<u8>, String> {
    let oid = synta::ObjectIdentifier::new(oid_comps)
        .map_err(|e| format!("invalid algorithm OID: {e}"))?;
    let alg = crate::AlgorithmIdentifier {
        algorithm: oid,
        parameters: Some(synta::Element::Null(synta::Null)),
    };
    let mut enc = synta::Encoder::new(synta::Encoding::Der);
    alg.encode(&mut enc)
        .map_err(|e| format!("AlgorithmIdentifier encode failed: {e}"))?;
    enc.finish()
        .map_err(|e| format!("AlgorithmIdentifier finish failed: {e}"))
}

// ── TimeStampReqBuilder ───────────────────────────────────────────────────────

/// Fluent builder for a `TimeStampReq` DER structure (RFC 3161 §2.4.1).
///
/// Required field: `message_imprint`.  All other fields are optional per the
/// RFC.  Use [`crate::oids::ID_SHA256`] (or `ID_SHA384`, `ID_SHA512`, etc.)
/// as the hash algorithm OID component slice.
#[derive(Debug, Default)]
pub struct TimeStampReqBuilder {
    /// Pre-encoded `MessageImprint` DER bytes.
    message_imprint: Option<Vec<u8>>,
    /// Optional `reqPolicy` OID component array.
    req_policy: Option<Vec<u32>>,
    /// Optional `nonce` bytes (big-endian INTEGER value).
    nonce: Option<Vec<u8>>,
    /// Optional `certReq` flag.
    cert_req: Option<bool>,
    /// First error encountered; subsequent builder calls are skipped.
    error: Option<String>,
}

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

    /// Set the `messageImprint` from a hash algorithm OID and raw hash bytes.
    ///
    /// `hash_alg_oid` is the OID component slice for the hash algorithm, e.g.
    /// [`crate::oids::ID_SHA256`] for SHA-256.  A `NULL` parameters field is
    /// added automatically (standard for all SHA-2 hash algorithms).
    /// `hashed_message` is the raw hash value (no OCTET STRING wrapper).
    ///
    /// Use [`message_imprint_with_alg_der`](Self::message_imprint_with_alg_der)
    /// to supply a pre-encoded `AlgorithmIdentifier` DER directly.
    pub fn message_imprint(mut self, hash_alg_oid: &[u32], hashed_message: &[u8]) -> Self {
        if self.error.is_some() {
            return self;
        }
        let alg_bytes = match encode_alg_id_null(hash_alg_oid) {
            Ok(b) => b,
            Err(e) => {
                self.error = Some(e);
                return self;
            }
        };
        self.set_message_imprint_from_alg_der(&alg_bytes, hashed_message)
    }

    /// Set the `messageImprint` from a pre-encoded `AlgorithmIdentifier` DER
    /// TLV and raw hash bytes.
    ///
    /// `alg_der` must be a complete `AlgorithmIdentifier` SEQUENCE TLV.
    pub fn message_imprint_with_alg_der(self, alg_der: &[u8], hashed_message: &[u8]) -> Self {
        if self.error.is_some() {
            return self;
        }
        self.set_message_imprint_from_alg_der(alg_der, hashed_message)
    }

    fn set_message_imprint_from_alg_der(mut self, alg_der: &[u8], hashed_message: &[u8]) -> Self {
        let alg_bytes = alg_der.to_vec();
        let msg_bytes = hashed_message.to_vec();

        let alg = match synta::Decoder::new(&alg_bytes, synta::Encoding::Der)
            .decode::<crate::AlgorithmIdentifier<'_>>()
        {
            Ok(a) => a,
            Err(e) => {
                self.error = Some(format!("AlgorithmIdentifier decode failed: {e}"));
                return self;
            }
        };
        let hash_ref = synta::OctetStringRef::new(&msg_bytes);
        let imprint = MessageImprint {
            hash_algorithm: alg,
            hashed_message: hash_ref,
        };
        let mut enc = synta::Encoder::new(synta::Encoding::Der);
        match imprint.encode(&mut enc).and_then(|()| enc.finish()) {
            Ok(bytes) => self.message_imprint = Some(bytes),
            Err(e) => self.error = Some(format!("MessageImprint DER encoding failed: {e}")),
        }
        self
    }

    /// Set the optional `reqPolicy` OID by component slice.
    ///
    /// Use this to request that the TSA use a specific policy.
    pub fn req_policy(mut self, oid_components: &[u32]) -> Self {
        if self.error.is_some() {
            return self;
        }
        self.req_policy = Some(oid_components.to_vec());
        self
    }

    /// Set the optional `nonce` value.
    ///
    /// `nonce_bytes` is the big-endian two's-complement representation of
    /// the nonce integer.
    pub fn nonce(mut self, nonce_bytes: &[u8]) -> Self {
        if self.error.is_some() {
            return self;
        }
        self.nonce = Some(nonce_bytes.to_vec());
        self
    }

    /// Set the `certReq` flag.
    ///
    /// When `true`, requests that the TSA include its signing certificate
    /// (and chain) in the time-stamp response.
    pub fn cert_req(mut self, val: bool) -> Self {
        if self.error.is_some() {
            return self;
        }
        self.cert_req = Some(val);
        self
    }

    /// Build the DER-encoded `TimeStampReq` SEQUENCE.
    ///
    /// Returns `Err` if `message_imprint` was not set, if any OID was invalid,
    /// or if DER encoding fails.
    pub fn build(self) -> Result<Vec<u8>, String> {
        if let Some(e) = self.error {
            return Err(e);
        }

        let imprint_bytes = self
            .message_imprint
            .ok_or_else(|| "message_imprint is required".to_string())?;

        // Re-decode the pre-encoded MessageImprint bytes.
        let message_imprint = synta::Decoder::new(&imprint_bytes, synta::Encoding::Der)
            .decode::<MessageImprint<'_>>()
            .map_err(|e| format!("MessageImprint re-decode failed: {e}"))?;

        let req_policy = if let Some(comps) = self.req_policy {
            Some(
                synta::ObjectIdentifier::new(&comps)
                    .map_err(|e| format!("invalid reqPolicy OID: {e}"))?,
            )
        } else {
            None
        };

        let nonce = self.nonce.map(|b| synta::Integer::from_bytes(&b));
        let cert_req = self.cert_req.map(synta::Boolean);

        let req = TimeStampReq {
            version: synta::Integer::from(1i64),
            message_imprint,
            req_policy,
            nonce,
            cert_req,
            extensions: None,
        };

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

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

    #[test]
    fn build_minimal_req() {
        let hash = [0xabu8; 32]; // fake SHA-256 hash
        let der = TimeStampReqBuilder::new()
            .message_imprint(crate::oids::ID_SHA256, &hash)
            .build()
            .expect("build should succeed");

        let decoded =
            crate::tsp_types::TimeStampReq::from_der(&der).expect("round-trip decode failed");
        assert_eq!(decoded.version.as_i64().ok(), Some(1));
        assert!(decoded.nonce.is_none());
        assert!(decoded.cert_req.is_none());
    }

    #[test]
    fn build_with_nonce_and_cert_req() {
        let hash = [0xbbu8; 32];
        let der = TimeStampReqBuilder::new()
            .message_imprint(crate::oids::ID_SHA256, &hash)
            .nonce(&[0x01, 0x02, 0x03, 0x04])
            .cert_req(true)
            .build()
            .expect("build with nonce should succeed");

        let decoded =
            crate::tsp_types::TimeStampReq::from_der(&der).expect("round-trip decode failed");
        assert!(decoded.nonce.is_some());
        assert_eq!(decoded.cert_req, Some(synta::Boolean(true)));
    }

    #[test]
    fn build_with_req_policy() {
        let hash = [0xccu8; 32];
        // Use a test policy OID (id-TEST-certPolicyOne = 1.3.6.1.5.5.7.13.1)
        let policy_oid = &[1u32, 3, 6, 1, 5, 5, 7, 13, 1];
        let der = TimeStampReqBuilder::new()
            .message_imprint(crate::oids::ID_SHA256, &hash)
            .req_policy(policy_oid)
            .build()
            .expect("build with req_policy should succeed");

        let decoded =
            crate::tsp_types::TimeStampReq::from_der(&der).expect("round-trip decode failed");
        assert!(decoded.req_policy.is_some());
    }

    #[test]
    fn missing_message_imprint_returns_error() {
        let err = TimeStampReqBuilder::new().build();
        assert!(err.is_err());
        assert!(err.unwrap_err().contains("message_imprint"));
    }

    #[test]
    fn invalid_policy_oid_returns_error() {
        let hash = [0xddu8; 32];
        // OID cannot start with arc > 2
        let der = TimeStampReqBuilder::new()
            .message_imprint(crate::oids::ID_SHA256, &hash)
            .req_policy(&[99, 0, 1])
            .build();
        assert!(der.is_err());
    }
}