Skip to main content

openssl/
cms.rs

1//! SMIME implementation using CMS
2//!
3//! CMS (PKCS#7) is an encryption standard.  It allows signing and encrypting data using
4//! X.509 certificates.  The OpenSSL implementation of CMS is used in email encryption
5//! generated from a `Vec` of bytes.  This `Vec` follows the smime protocol standards.
6//! Data accepted by this module will be smime type `enveloped-data`.
7
8use bitflags::bitflags;
9use foreign_types::{ForeignType, ForeignTypeRef};
10use libc::c_uint;
11use std::ptr;
12
13use crate::bio::{MemBio, MemBioSlice};
14use crate::error::ErrorStack;
15use crate::pkey::{HasPrivate, PKeyRef};
16use crate::stack::StackRef;
17use crate::symm::Cipher;
18use crate::x509::{store::X509StoreRef, X509Ref, X509};
19use crate::{cvt, cvt_p};
20use openssl_macros::corresponds;
21
22bitflags! {
23    #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
24    #[repr(transparent)]
25    pub struct CMSOptions : c_uint {
26        const TEXT = ffi::CMS_TEXT;
27        const CMS_NOCERTS = ffi::CMS_NOCERTS;
28        const NO_CONTENT_VERIFY = ffi::CMS_NO_CONTENT_VERIFY;
29        const NO_ATTR_VERIFY = ffi::CMS_NO_ATTR_VERIFY;
30        const NOSIGS = ffi::CMS_NOSIGS;
31        const NOINTERN = ffi::CMS_NOINTERN;
32        const NO_SIGNER_CERT_VERIFY = ffi::CMS_NO_SIGNER_CERT_VERIFY;
33        const NOVERIFY = ffi::CMS_NOVERIFY;
34        const DETACHED = ffi::CMS_DETACHED;
35        const BINARY = ffi::CMS_BINARY;
36        const NOATTR = ffi::CMS_NOATTR;
37        const NOSMIMECAP = ffi::CMS_NOSMIMECAP;
38        const NOOLDMIMETYPE = ffi::CMS_NOOLDMIMETYPE;
39        const CRLFEOL = ffi::CMS_CRLFEOL;
40        const STREAM = ffi::CMS_STREAM;
41        const NOCRL = ffi::CMS_NOCRL;
42        const PARTIAL = ffi::CMS_PARTIAL;
43        const REUSE_DIGEST = ffi::CMS_REUSE_DIGEST;
44        const USE_KEYID = ffi::CMS_USE_KEYID;
45        const DEBUG_DECRYPT = ffi::CMS_DEBUG_DECRYPT;
46        const KEY_PARAM = ffi::CMS_KEY_PARAM;
47        const ASCIICRLF = ffi::CMS_ASCIICRLF;
48    }
49}
50
51foreign_type_and_impl_send_sync! {
52    type CType = ffi::CMS_ContentInfo;
53    fn drop = ffi::CMS_ContentInfo_free;
54
55    /// High level CMS wrapper
56    ///
57    /// CMS supports nesting various types of data, including signatures, certificates,
58    /// encrypted data, smime messages (encrypted email), and data digest.  The ContentInfo
59    /// content type is the encapsulation of all those content types.  [`RFC 5652`] describes
60    /// CMS and OpenSSL follows this RFC's implementation.
61    ///
62    /// [`RFC 5652`]: https://tools.ietf.org/html/rfc5652#page-6
63    pub struct CmsContentInfo;
64    /// Reference to [`CMSContentInfo`]
65    ///
66    /// [`CMSContentInfo`]:struct.CmsContentInfo.html
67    pub struct CmsContentInfoRef;
68}
69
70impl CmsContentInfoRef {
71    /// Given the sender's private key, `pkey` and the recipient's certificate, `cert`,
72    /// decrypt the data in `self`.
73    #[corresponds(CMS_decrypt)]
74    pub fn decrypt<T>(&self, pkey: &PKeyRef<T>, cert: &X509) -> Result<Vec<u8>, ErrorStack>
75    where
76        T: HasPrivate,
77    {
78        unsafe {
79            let pkey = pkey.as_ptr();
80            let cert = cert.as_ptr();
81            let out = MemBio::new()?;
82
83            cvt(ffi::CMS_decrypt(
84                self.as_ptr(),
85                pkey,
86                cert,
87                ptr::null_mut(),
88                out.as_ptr(),
89                0,
90            ))?;
91
92            Ok(out.get_buf().to_owned())
93        }
94    }
95
96    /// Given the sender's private key, `pkey`,
97    /// decrypt the data in `self` without validating the recipient certificate.
98    ///
99    /// *Warning*: Not checking the recipient certificate may leave you vulnerable to Bleichenbacher's attack on PKCS#1 v1.5 RSA padding.
100    #[corresponds(CMS_decrypt)]
101    // FIXME merge into decrypt
102    pub fn decrypt_without_cert_check<T>(&self, pkey: &PKeyRef<T>) -> Result<Vec<u8>, ErrorStack>
103    where
104        T: HasPrivate,
105    {
106        unsafe {
107            let pkey = pkey.as_ptr();
108            let out = MemBio::new()?;
109
110            cvt(ffi::CMS_decrypt(
111                self.as_ptr(),
112                pkey,
113                ptr::null_mut(),
114                ptr::null_mut(),
115                out.as_ptr(),
116                0,
117            ))?;
118
119            Ok(out.get_buf().to_owned())
120        }
121    }
122
123    to_der! {
124        /// Serializes this CmsContentInfo using DER.
125        #[corresponds(i2d_CMS_ContentInfo)]
126        to_der,
127        ffi::i2d_CMS_ContentInfo
128    }
129
130    to_pem! {
131        /// Serializes this CmsContentInfo using DER.
132        #[corresponds(PEM_write_bio_CMS)]
133        to_pem,
134        ffi::PEM_write_bio_CMS
135    }
136}
137
138impl CmsContentInfo {
139    /// Parses a smime formatted `vec` of bytes into a `CmsContentInfo`.
140    #[corresponds(SMIME_read_CMS)]
141    pub fn smime_read_cms(smime: &[u8]) -> Result<CmsContentInfo, ErrorStack> {
142        unsafe {
143            let bio = MemBioSlice::new(smime)?;
144
145            let cms = cvt_p(ffi::SMIME_read_CMS(bio.as_ptr(), ptr::null_mut()))?;
146
147            Ok(CmsContentInfo::from_ptr(cms))
148        }
149    }
150
151    from_der! {
152        /// Deserializes a DER-encoded ContentInfo structure.
153        #[corresponds(d2i_CMS_ContentInfo)]
154        from_der,
155        CmsContentInfo,
156        ffi::d2i_CMS_ContentInfo
157    }
158
159    from_pem! {
160        /// Deserializes a PEM-encoded ContentInfo structure.
161        #[corresponds(PEM_read_bio_CMS)]
162        from_pem,
163        CmsContentInfo,
164        ffi::PEM_read_bio_CMS
165    }
166
167    /// Given a signing cert `signcert`, private key `pkey`, a certificate stack `certs`,
168    /// data `data` and flags `flags`, create a CmsContentInfo struct.
169    ///
170    /// All arguments are optional.
171    #[corresponds(CMS_sign)]
172    pub fn sign<T>(
173        signcert: Option<&X509Ref>,
174        pkey: Option<&PKeyRef<T>>,
175        certs: Option<&StackRef<X509>>,
176        data: Option<&[u8]>,
177        flags: CMSOptions,
178    ) -> Result<CmsContentInfo, ErrorStack>
179    where
180        T: HasPrivate,
181    {
182        unsafe {
183            let signcert = signcert.map_or(ptr::null_mut(), |p| p.as_ptr());
184            let pkey = pkey.map_or(ptr::null_mut(), |p| p.as_ptr());
185            let data_bio = match data {
186                Some(data) => Some(MemBioSlice::new(data)?),
187                None => None,
188            };
189            let data_bio_ptr = data_bio.as_ref().map_or(ptr::null_mut(), |p| p.as_ptr());
190            let certs = certs.map_or(ptr::null_mut(), |p| p.as_ptr());
191
192            let cms = cvt_p(ffi::CMS_sign(
193                signcert,
194                pkey,
195                certs,
196                data_bio_ptr,
197                flags.bits(),
198            ))?;
199
200            Ok(CmsContentInfo::from_ptr(cms))
201        }
202    }
203
204    /// Given a certificate stack `certs`, data `data`, cipher `cipher` and flags `flags`,
205    /// create a CmsContentInfo struct.
206    ///
207    #[corresponds(CMS_encrypt)]
208    pub fn encrypt(
209        certs: &StackRef<X509>,
210        data: &[u8],
211        cipher: Cipher,
212        flags: CMSOptions,
213    ) -> Result<CmsContentInfo, ErrorStack> {
214        unsafe {
215            let data_bio = MemBioSlice::new(data)?;
216
217            let cms = cvt_p(ffi::CMS_encrypt(
218                certs.as_ptr(),
219                data_bio.as_ptr(),
220                cipher.as_ptr(),
221                flags.bits(),
222            ))?;
223
224            Ok(CmsContentInfo::from_ptr(cms))
225        }
226    }
227
228    /// Verify this CmsContentInfo's signature,
229    /// This will search the 'certs' list for the signing certificate.
230    /// Additional certificates, needed for building the certificate chain, may be
231    /// given in 'store' as well as additional CRLs.
232    /// A detached signature may be passed in `detached_data`. The signed content
233    /// without signature, will be copied into output_data if it is present.
234    ///
235    #[corresponds(CMS_verify)]
236    pub fn verify(
237        &mut self,
238        certs: Option<&StackRef<X509>>,
239        store: Option<&X509StoreRef>,
240        detached_data: Option<&[u8]>,
241        output_data: Option<&mut Vec<u8>>,
242        flags: CMSOptions,
243    ) -> Result<(), ErrorStack> {
244        unsafe {
245            let certs_ptr = certs.map_or(ptr::null_mut(), |p| p.as_ptr());
246            let store_ptr = store.map_or(ptr::null_mut(), |p| p.as_ptr());
247            let detached_data_bio = match detached_data {
248                Some(data) => Some(MemBioSlice::new(data)?),
249                None => None,
250            };
251            let detached_data_bio_ptr = detached_data_bio
252                .as_ref()
253                .map_or(ptr::null_mut(), |p| p.as_ptr());
254            let out_bio = MemBio::new()?;
255
256            cvt(ffi::CMS_verify(
257                self.as_ptr(),
258                certs_ptr,
259                store_ptr,
260                detached_data_bio_ptr,
261                out_bio.as_ptr(),
262                flags.bits(),
263            ))?;
264
265            if let Some(data) = output_data {
266                data.clear();
267                data.extend_from_slice(out_bio.get_buf());
268            };
269
270            Ok(())
271        }
272    }
273}
274
275#[cfg(test)]
276mod test {
277    use super::*;
278
279    use crate::pkcs12::Pkcs12;
280    use crate::pkey::PKey;
281    use crate::stack::Stack;
282    use crate::x509::{
283        store::{X509Store, X509StoreBuilder},
284        X509,
285    };
286
287    #[test]
288    fn cms_encrypt_decrypt() {
289        #[cfg(ossl300)]
290        let _provider = crate::provider::Provider::try_load(None, "legacy", true).unwrap();
291
292        // load cert with public key only
293        let pub_cert_bytes = include_bytes!("../test/cms_pubkey.der");
294        let pub_cert = X509::from_der(pub_cert_bytes).expect("failed to load pub cert");
295
296        // load cert with private key
297        let priv_cert_bytes = include_bytes!("../test/cms.p12");
298        let priv_cert = Pkcs12::from_der(priv_cert_bytes).expect("failed to load priv cert");
299        let priv_cert = priv_cert
300            .parse2("mypass")
301            .expect("failed to parse priv cert");
302
303        // encrypt cms message using public key cert
304        let input = String::from("My Message");
305        let mut cert_stack = Stack::new().expect("failed to create stack");
306        cert_stack
307            .push(pub_cert)
308            .expect("failed to add pub cert to stack");
309
310        let encrypt = CmsContentInfo::encrypt(
311            &cert_stack,
312            input.as_bytes(),
313            Cipher::des_ede3_cbc(),
314            CMSOptions::empty(),
315        )
316        .expect("failed create encrypted cms");
317
318        // decrypt cms message using private key cert (DER)
319        {
320            let encrypted_der = encrypt.to_der().expect("failed to create der from cms");
321            let decrypt =
322                CmsContentInfo::from_der(&encrypted_der).expect("failed read cms from der");
323
324            let decrypt_with_cert_check = decrypt
325                .decrypt(
326                    priv_cert.pkey.as_ref().unwrap(),
327                    priv_cert.cert.as_ref().unwrap(),
328                )
329                .expect("failed to decrypt cms");
330            let decrypt_with_cert_check = String::from_utf8(decrypt_with_cert_check)
331                .expect("failed to create string from cms content");
332
333            let decrypt_without_cert_check = decrypt
334                .decrypt_without_cert_check(priv_cert.pkey.as_ref().unwrap())
335                .expect("failed to decrypt cms");
336            let decrypt_without_cert_check = String::from_utf8(decrypt_without_cert_check)
337                .expect("failed to create string from cms content");
338
339            assert_eq!(input, decrypt_with_cert_check);
340            assert_eq!(input, decrypt_without_cert_check);
341        }
342
343        // decrypt cms message using private key cert (PEM)
344        {
345            let encrypted_pem = encrypt.to_pem().expect("failed to create pem from cms");
346            let decrypt =
347                CmsContentInfo::from_pem(&encrypted_pem).expect("failed read cms from pem");
348
349            let decrypt_with_cert_check = decrypt
350                .decrypt(
351                    priv_cert.pkey.as_ref().unwrap(),
352                    priv_cert.cert.as_ref().unwrap(),
353                )
354                .expect("failed to decrypt cms");
355            let decrypt_with_cert_check = String::from_utf8(decrypt_with_cert_check)
356                .expect("failed to create string from cms content");
357
358            let decrypt_without_cert_check = decrypt
359                .decrypt_without_cert_check(priv_cert.pkey.as_ref().unwrap())
360                .expect("failed to decrypt cms");
361            let decrypt_without_cert_check = String::from_utf8(decrypt_without_cert_check)
362                .expect("failed to create string from cms content");
363
364            assert_eq!(input, decrypt_with_cert_check);
365            assert_eq!(input, decrypt_without_cert_check);
366        }
367    }
368
369    fn cms_sign_verify_generic_helper(is_detached: bool) {
370        // load cert with private key
371        let cert_bytes = include_bytes!("../test/cert.pem");
372        let cert = X509::from_pem(cert_bytes).expect("failed to load cert.pem");
373
374        let key_bytes = include_bytes!("../test/key.pem");
375        let key = PKey::private_key_from_pem(key_bytes).expect("failed to load key.pem");
376
377        let root_bytes = include_bytes!("../test/root-ca.pem");
378        let root = X509::from_pem(root_bytes).expect("failed to load root-ca.pem");
379
380        // sign cms message using public key cert
381        let data = b"Hello world!";
382
383        let (opt, ext_data): (CMSOptions, Option<&[u8]>) = if is_detached {
384            (CMSOptions::DETACHED | CMSOptions::BINARY, Some(data))
385        } else {
386            (CMSOptions::empty(), None)
387        };
388
389        let mut cms = CmsContentInfo::sign(Some(&cert), Some(&key), None, Some(data), opt)
390            .expect("failed to CMS sign a message");
391
392        // check CMS signature length
393        let pem_cms = cms
394            .to_pem()
395            .expect("failed to pack CmsContentInfo into PEM");
396        assert!(!pem_cms.is_empty());
397
398        // verify CMS signature
399        let mut builder = X509StoreBuilder::new().expect("failed to create X509StoreBuilder");
400        builder
401            .add_cert(root)
402            .expect("failed to add root-ca into X509StoreBuilder");
403        let store: X509Store = builder.build();
404        let mut out_data: Vec<u8> = Vec::new();
405        let res = cms.verify(
406            None,
407            Some(&store),
408            ext_data,
409            Some(&mut out_data),
410            CMSOptions::empty(),
411        );
412
413        // check verification result -  valid signature
414        res.unwrap();
415        assert_eq!(data.to_vec(), out_data);
416    }
417
418    #[test]
419    fn cms_sign_verify_ok() {
420        cms_sign_verify_generic_helper(false);
421    }
422
423    #[test]
424    fn cms_sign_verify_detached_ok() {
425        cms_sign_verify_generic_helper(true);
426    }
427
428    #[test]
429    fn cms_sign_verify_error() {
430        #[cfg(ossl300)]
431        let _provider = crate::provider::Provider::try_load(None, "legacy", true).unwrap();
432
433        // load cert with private key
434        let priv_cert_bytes = include_bytes!("../test/cms.p12");
435        let priv_cert = Pkcs12::from_der(priv_cert_bytes).expect("failed to load priv cert");
436        let priv_cert = priv_cert
437            .parse2("mypass")
438            .expect("failed to parse priv cert");
439
440        // sign cms message using public key cert
441        let data = b"Hello world!";
442        let mut cms = CmsContentInfo::sign(
443            Some(&priv_cert.cert.unwrap()),
444            Some(&priv_cert.pkey.unwrap()),
445            None,
446            Some(data),
447            CMSOptions::empty(),
448        )
449        .expect("failed to CMS sign a message");
450
451        // check CMS signature length
452        let pem_cms = cms
453            .to_pem()
454            .expect("failed to pack CmsContentInfo into PEM");
455        assert!(!pem_cms.is_empty());
456
457        let empty_store = X509StoreBuilder::new()
458            .expect("failed to create X509StoreBuilder")
459            .build();
460
461        // verify CMS signature
462        let res = cms.verify(
463            None,
464            Some(&empty_store),
465            Some(data),
466            None,
467            CMSOptions::empty(),
468        );
469
470        // check verification result - this is an invalid signature
471        // defined in openssl crypto/cms/cms.h
472        const CMS_R_CERTIFICATE_VERIFY_ERROR: i32 = 100;
473        let es = res.unwrap_err();
474        let error_array = es.errors();
475        assert_eq!(1, error_array.len());
476        let code = error_array[0].reason_code();
477        assert_eq!(code, CMS_R_CERTIFICATE_VERIFY_ERROR);
478    }
479}