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