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