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 
8 use bitflags::bitflags;
9 use foreign_types::{ForeignType, ForeignTypeRef};
10 use libc::c_uint;
11 use std::ptr;
12 
13 use crate::bio::{MemBio, MemBioSlice};
14 use crate::error::ErrorStack;
15 use crate::pkey::{HasPrivate, PKeyRef};
16 use crate::stack::StackRef;
17 use crate::symm::Cipher;
18 use crate::x509::{store::X509StoreRef, X509Ref, X509};
19 use crate::{cvt, cvt_p};
20 use openssl_macros::corresponds;
21 
22 bitflags! {
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 
53 foreign_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 
72 impl 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)]
decrypt<T>(&self, pkey: &PKeyRef<T>, cert: &X509) -> Result<Vec<u8>, ErrorStack> where T: HasPrivate,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
decrypt_without_cert_check<T>(&self, pkey: &PKeyRef<T>) -> Result<Vec<u8>, ErrorStack> where T: HasPrivate,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 
140 impl CmsContentInfo {
141     /// Parses a smime formatted `vec` of bytes into a `CmsContentInfo`.
142     #[corresponds(SMIME_read_CMS)]
smime_read_cms(smime: &[u8]) -> Result<CmsContentInfo, ErrorStack>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)]
sign<T>( signcert: Option<&X509Ref>, pkey: Option<&PKeyRef<T>>, certs: Option<&StackRef<X509>>, data: Option<&[u8]>, flags: CMSOptions, ) -> Result<CmsContentInfo, ErrorStack> where T: HasPrivate,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)]
encrypt( certs: &StackRef<X509>, data: &[u8], cipher: Cipher, flags: CMSOptions, ) -> Result<CmsContentInfo, ErrorStack>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)]
verify( &mut self, certs: Option<&StackRef<X509>>, store: Option<&X509StoreRef>, detached_data: Option<&[u8]>, output_data: Option<&mut Vec<u8>>, flags: CMSOptions, ) -> Result<(), ErrorStack>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)]
281 mod 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]
cms_encrypt_decrypt()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 
cms_sign_verify_generic_helper(is_detached: bool)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]
cms_sign_verify_ok()424     fn cms_sign_verify_ok() {
425         cms_sign_verify_generic_helper(false);
426     }
427 
428     #[test]
cms_sign_verify_detached_ok()429     fn cms_sign_verify_detached_ok() {
430         cms_sign_verify_generic_helper(true);
431     }
432 
433     #[test]
cms_sign_verify_error()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 }
485