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(any(ossl102, libressl))]
47        const KEY_PARAM = ffi::CMS_KEY_PARAM;
48        #[cfg(any(ossl110, libressl))]
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    #[corresponds(CMS_encrypt)]
210    pub fn encrypt(
211        certs: &StackRef<X509>,
212        data: &[u8],
213        cipher: Cipher,
214        flags: CMSOptions,
215    ) -> Result<CmsContentInfo, ErrorStack> {
216        unsafe {
217            let data_bio = MemBioSlice::new(data)?;
218
219            let cms = cvt_p(ffi::CMS_encrypt(
220                certs.as_ptr(),
221                data_bio.as_ptr(),
222                cipher.as_ptr(),
223                flags.bits(),
224            ))?;
225
226            Ok(CmsContentInfo::from_ptr(cms))
227        }
228    }
229
230    /// Verify this CmsContentInfo's signature,
231    /// This will search the 'certs' list for the signing certificate.
232    /// Additional certificates, needed for building the certificate chain, may be
233    /// given in 'store' as well as additional CRLs.
234    /// A detached signature may be passed in `detached_data`. The signed content
235    /// without signature, will be copied into output_data if it is present.
236    ///
237    #[corresponds(CMS_verify)]
238    pub fn verify(
239        &mut self,
240        certs: Option<&StackRef<X509>>,
241        store: Option<&X509StoreRef>,
242        detached_data: Option<&[u8]>,
243        output_data: Option<&mut Vec<u8>>,
244        flags: CMSOptions,
245    ) -> Result<(), ErrorStack> {
246        unsafe {
247            let certs_ptr = certs.map_or(ptr::null_mut(), |p| p.as_ptr());
248            let store_ptr = store.map_or(ptr::null_mut(), |p| p.as_ptr());
249            let detached_data_bio = match detached_data {
250                Some(data) => Some(MemBioSlice::new(data)?),
251                None => None,
252            };
253            let detached_data_bio_ptr = detached_data_bio
254                .as_ref()
255                .map_or(ptr::null_mut(), |p| p.as_ptr());
256            let out_bio = MemBio::new()?;
257
258            cvt(ffi::CMS_verify(
259                self.as_ptr(),
260                certs_ptr,
261                store_ptr,
262                detached_data_bio_ptr,
263                out_bio.as_ptr(),
264                flags.bits(),
265            ))?;
266
267            if let Some(data) = output_data {
268                data.clear();
269                data.extend_from_slice(out_bio.get_buf());
270            };
271
272            Ok(())
273        }
274    }
275}
276
277#[cfg(test)]
278mod test {
279    use super::*;
280
281    use crate::pkcs12::Pkcs12;
282    use crate::pkey::PKey;
283    use crate::stack::Stack;
284    use crate::x509::{
285        store::{X509Store, X509StoreBuilder},
286        X509,
287    };
288
289    #[test]
290    fn cms_encrypt_decrypt() {
291        #[cfg(ossl300)]
292        let _provider = crate::provider::Provider::try_load(None, "legacy", true).unwrap();
293
294        // load cert with public key only
295        let pub_cert_bytes = include_bytes!("../test/cms_pubkey.der");
296        let pub_cert = X509::from_der(pub_cert_bytes).expect("failed to load pub cert");
297
298        // load cert with private key
299        let priv_cert_bytes = include_bytes!("../test/cms.p12");
300        let priv_cert = Pkcs12::from_der(priv_cert_bytes).expect("failed to load priv cert");
301        let priv_cert = priv_cert
302            .parse2("mypass")
303            .expect("failed to parse priv cert");
304
305        // encrypt cms message using public key cert
306        let input = String::from("My Message");
307        let mut cert_stack = Stack::new().expect("failed to create stack");
308        cert_stack
309            .push(pub_cert)
310            .expect("failed to add pub cert to stack");
311
312        let encrypt = CmsContentInfo::encrypt(
313            &cert_stack,
314            input.as_bytes(),
315            Cipher::des_ede3_cbc(),
316            CMSOptions::empty(),
317        )
318        .expect("failed create encrypted cms");
319
320        // decrypt cms message using private key cert (DER)
321        {
322            let encrypted_der = encrypt.to_der().expect("failed to create der from cms");
323            let decrypt =
324                CmsContentInfo::from_der(&encrypted_der).expect("failed read cms from der");
325
326            let decrypt_with_cert_check = decrypt
327                .decrypt(
328                    priv_cert.pkey.as_ref().unwrap(),
329                    priv_cert.cert.as_ref().unwrap(),
330                )
331                .expect("failed to decrypt cms");
332            let decrypt_with_cert_check = String::from_utf8(decrypt_with_cert_check)
333                .expect("failed to create string from cms content");
334
335            let decrypt_without_cert_check = decrypt
336                .decrypt_without_cert_check(priv_cert.pkey.as_ref().unwrap())
337                .expect("failed to decrypt cms");
338            let decrypt_without_cert_check = String::from_utf8(decrypt_without_cert_check)
339                .expect("failed to create string from cms content");
340
341            assert_eq!(input, decrypt_with_cert_check);
342            assert_eq!(input, decrypt_without_cert_check);
343        }
344
345        // decrypt cms message using private key cert (PEM)
346        {
347            let encrypted_pem = encrypt.to_pem().expect("failed to create pem from cms");
348            let decrypt =
349                CmsContentInfo::from_pem(&encrypted_pem).expect("failed read cms from pem");
350
351            let decrypt_with_cert_check = decrypt
352                .decrypt(
353                    priv_cert.pkey.as_ref().unwrap(),
354                    priv_cert.cert.as_ref().unwrap(),
355                )
356                .expect("failed to decrypt cms");
357            let decrypt_with_cert_check = String::from_utf8(decrypt_with_cert_check)
358                .expect("failed to create string from cms content");
359
360            let decrypt_without_cert_check = decrypt
361                .decrypt_without_cert_check(priv_cert.pkey.as_ref().unwrap())
362                .expect("failed to decrypt cms");
363            let decrypt_without_cert_check = String::from_utf8(decrypt_without_cert_check)
364                .expect("failed to create string from cms content");
365
366            assert_eq!(input, decrypt_with_cert_check);
367            assert_eq!(input, decrypt_without_cert_check);
368        }
369    }
370
371    fn cms_sign_verify_generic_helper(is_detached: bool) {
372        // load cert with private key
373        let cert_bytes = include_bytes!("../test/cert.pem");
374        let cert = X509::from_pem(cert_bytes).expect("failed to load cert.pem");
375
376        let key_bytes = include_bytes!("../test/key.pem");
377        let key = PKey::private_key_from_pem(key_bytes).expect("failed to load key.pem");
378
379        let root_bytes = include_bytes!("../test/root-ca.pem");
380        let root = X509::from_pem(root_bytes).expect("failed to load root-ca.pem");
381
382        // sign cms message using public key cert
383        let data = b"Hello world!";
384
385        let (opt, ext_data): (CMSOptions, Option<&[u8]>) = if is_detached {
386            (CMSOptions::DETACHED | CMSOptions::BINARY, Some(data))
387        } else {
388            (CMSOptions::empty(), None)
389        };
390
391        let mut cms = CmsContentInfo::sign(Some(&cert), Some(&key), None, Some(data), opt)
392            .expect("failed to CMS sign a message");
393
394        // check CMS signature length
395        let pem_cms = cms
396            .to_pem()
397            .expect("failed to pack CmsContentInfo into PEM");
398        assert!(!pem_cms.is_empty());
399
400        // verify CMS signature
401        let mut builder = X509StoreBuilder::new().expect("failed to create X509StoreBuilder");
402        builder
403            .add_cert(root)
404            .expect("failed to add root-ca into X509StoreBuilder");
405        let store: X509Store = builder.build();
406        let mut out_data: Vec<u8> = Vec::new();
407        let res = cms.verify(
408            None,
409            Some(&store),
410            ext_data,
411            Some(&mut out_data),
412            CMSOptions::empty(),
413        );
414
415        // check verification result -  valid signature
416        res.unwrap();
417        assert_eq!(data.to_vec(), out_data);
418    }
419
420    #[test]
421    fn cms_sign_verify_ok() {
422        cms_sign_verify_generic_helper(false);
423    }
424
425    #[test]
426    fn cms_sign_verify_detached_ok() {
427        cms_sign_verify_generic_helper(true);
428    }
429
430    #[test]
431    fn cms_sign_verify_error() {
432        #[cfg(ossl300)]
433        let _provider = crate::provider::Provider::try_load(None, "legacy", true).unwrap();
434
435        // load cert with private key
436        let priv_cert_bytes = include_bytes!("../test/cms.p12");
437        let priv_cert = Pkcs12::from_der(priv_cert_bytes).expect("failed to load priv cert");
438        let priv_cert = priv_cert
439            .parse2("mypass")
440            .expect("failed to parse priv cert");
441
442        // sign cms message using public key cert
443        let data = b"Hello world!";
444        let mut cms = CmsContentInfo::sign(
445            Some(&priv_cert.cert.unwrap()),
446            Some(&priv_cert.pkey.unwrap()),
447            None,
448            Some(data),
449            CMSOptions::empty(),
450        )
451        .expect("failed to CMS sign a message");
452
453        // check CMS signature length
454        let pem_cms = cms
455            .to_pem()
456            .expect("failed to pack CmsContentInfo into PEM");
457        assert!(!pem_cms.is_empty());
458
459        let empty_store = X509StoreBuilder::new()
460            .expect("failed to create X509StoreBuilder")
461            .build();
462
463        // verify CMS signature
464        let res = cms.verify(
465            None,
466            Some(&empty_store),
467            Some(data),
468            None,
469            CMSOptions::empty(),
470        );
471
472        // check verification result - this is an invalid signature
473        // defined in openssl crypto/cms/cms.h
474        const CMS_R_CERTIFICATE_VERIFY_ERROR: i32 = 100;
475        let es = res.unwrap_err();
476        let error_array = es.errors();
477        assert_eq!(1, error_array.len());
478        let code = error_array[0].reason_code();
479        assert_eq!(code, CMS_R_CERTIFICATE_VERIFY_ERROR);
480    }
481}