Skip to main content

confium_pki/cms/
envelope.rs

1//! CMS envelope construction.
2//!
3//! Builds a SignedData envelope from a payload, signer, and optional
4//! certificate chain. For DER-encoded output compatible with OpenSSL,
5//! Thunderbird/RNP, etc., the consumer should serialize via the `der`
6//! crate (not done here — this crate provides the semantic model only).
7
8use crate::cms::signed_data::{AlgorithmIdentifier, SignedData, SignerIdentifier, SignerInfo};
9
10/// Builder for `SignedData`.
11#[derive(Debug, Default)]
12pub struct SignedDataBuilder {
13    content_type: Option<String>,
14    content: Option<Vec<u8>>,
15    signers: Vec<SignerInfo>,
16    certificates: Vec<Vec<u8>>,
17}
18
19impl SignedDataBuilder {
20    /// Construct a new builder.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Set the encapsulated content type OID.
26    pub fn content_type(mut self, oid: impl Into<String>) -> Self {
27        self.content_type = Some(oid.into());
28        self
29    }
30
31    /// Set the encapsulated content (None = detached signature).
32    pub fn content(mut self, content: Option<Vec<u8>>) -> Self {
33        self.content = content;
34        self
35    }
36
37    /// Add a signer.
38    pub fn signer(mut self, signer: SignerInfo) -> Self {
39        self.signers.push(signer);
40        self
41    }
42
43    /// Add a certificate (DER bytes).
44    pub fn certificate(mut self, cert_der: Vec<u8>) -> Self {
45        self.certificates.push(cert_der);
46        self
47    }
48
49    /// Build the SignedData.
50    pub fn build(self) -> Result<SignedData, CmsError> {
51        let content_type = self
52            .content_type
53            .ok_or(CmsError::MissingField("content_type"))?;
54        let mut sd = SignedData::new(content_type, self.content);
55        for cert in self.certificates {
56            sd.add_certificate(cert);
57        }
58        for signer in self.signers {
59            sd.add_signer(signer);
60        }
61        Ok(sd)
62    }
63}
64
65/// Errors during CMS envelope construction.
66#[derive(Debug, thiserror::Error)]
67pub enum CmsError {
68    /// Required field missing.
69    #[error("missing required field: {0}")]
70    MissingField(&'static str),
71    /// Serialization failure.
72    #[error("serialization error: {0}")]
73    Serialize(String),
74    /// Verification failure.
75    #[error("verification failure: {0}")]
76    Verify(String),
77    /// JSON encode/decode error.
78    #[error("JSON error: {0}")]
79    Json(#[from] serde_json::Error),
80}
81
82/// Convenience: build a minimal detached CMS signature with one signer.
83pub fn build_detached_signature(
84    payload_hash: Vec<u8>,
85    signer_algorithm: impl Into<String>,
86    signature: Vec<u8>,
87    cert_chain_der: Vec<Vec<u8>>,
88) -> Result<SignedData, CmsError> {
89    let _ = payload_hash; // payload hash goes in signedAttrs (real impl); skipped here
90    let signer = SignerInfo {
91        version: 1,
92        sid: SignerIdentifier::SubjectKeyIdentifier {
93            key_identifier: cert_chain_der
94                .first()
95                .map(|c| c[..20].to_vec())
96                .unwrap_or_default(),
97        },
98        digest_algorithm: AlgorithmIdentifier {
99            oid: "2.16.840.1.101.3.4.2.1".into(), // SHA-256
100            parameters: None,
101        },
102        signed_attrs: Vec::new(),
103        signature_algorithm: AlgorithmIdentifier {
104            oid: signer_algorithm.into(),
105            parameters: None,
106        },
107        signature,
108        unsigned_attrs: Vec::new(),
109    };
110    let mut builder = SignedDataBuilder::new()
111        .content_type("1.2.840.113549.1.7.1")
112        .content(None)
113        .signer(signer);
114    for cert in cert_chain_der {
115        builder = builder.certificate(cert);
116    }
117    builder.build()
118}
119
120/// Re-export EncapContentInfo so consumers can build it without reaching
121/// into the signed_data module directly.
122pub use crate::cms::signed_data::EncapContentInfo as _ReExportEncapContentInfo;
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn builder_constructs_signed_data() {
130        let sd = SignedDataBuilder::new()
131            .content_type("1.2.840.113549.1.7.1")
132            .content(Some(b"hello".to_vec()))
133            .build()
134            .unwrap();
135        assert_eq!(sd.encap_content_info.content_type, "1.2.840.113549.1.7.1");
136        assert_eq!(sd.encap_content_info.content, Some(b"hello".to_vec()));
137    }
138
139    #[test]
140    fn detached_signature_helper() {
141        let sd = build_detached_signature(
142            vec![0u8; 32],
143            "1.2.840.113549.1.1.11",
144            vec![0u8; 256],
145            vec![vec![0u8; 100]],
146        )
147        .unwrap();
148        assert_eq!(sd.signer_count(), 1);
149        assert!(sd.encap_content_info.content.is_none()); // detached
150    }
151
152    #[test]
153    fn missing_content_type_fails() {
154        let result = SignedDataBuilder::new().build();
155        assert!(result.is_err());
156    }
157}