diff options
| author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2020-09-19 18:07:26 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-09-19 19:07:26 -0400 |
| commit | 20c0388086b4eec91fdf1f9fd9535f4c741e4851 (patch) | |
| tree | e5b212bf218ecdf22e05ca018cb5e352626e6572 /src | |
| parent | c61f24bb4df584bd3a2fb640298dad4bd6351ecd (diff) | |
| download | cryptography-20c0388086b4eec91fdf1f9fd9535f4c741e4851.tar.gz | |
smime signer support (#5465)
* smime signer support
* fix ed25519 check
* change some wording
* python 2.7...
* review feedback
* s/secure/signed
* do some verification in the tests
* review feedback
* doc return value
Diffstat (limited to 'src')
| -rw-r--r-- | src/_cffi_src/openssl/pkcs7.py | 1 | ||||
| -rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 62 | ||||
| -rw-r--r-- | src/cryptography/hazmat/primitives/smime.py | 109 |
3 files changed, 171 insertions, 1 deletions
diff --git a/src/_cffi_src/openssl/pkcs7.py b/src/_cffi_src/openssl/pkcs7.py index 72f9c2130..1878ec59d 100644 --- a/src/_cffi_src/openssl/pkcs7.py +++ b/src/_cffi_src/openssl/pkcs7.py @@ -67,6 +67,7 @@ int PKCS7_final(PKCS7 *, BIO *, int); https://github.com/pyca/cryptography/issues/5433 */ int PKCS7_verify(PKCS7 *, Cryptography_STACK_OF_X509 *, X509_STORE *, BIO *, BIO *, int); +PKCS7 *SMIME_read_PKCS7(BIO *, BIO **); int PKCS7_type_is_signed(PKCS7 *); int PKCS7_type_is_enveloped(PKCS7 *); diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 97c7fd054..f7d6a47c7 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -115,7 +115,7 @@ from cryptography.hazmat.backends.openssl.x509 import ( _RevokedCertificate, ) from cryptography.hazmat.bindings.openssl import binding -from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives import hashes, serialization, smime from cryptography.hazmat.primitives.asymmetric import ( dsa, ec, @@ -2690,6 +2690,66 @@ class Backend(object): return certs + def smime_sign(self, builder, encoding, options): + bio = self._bytes_to_bio(builder._data) + init_flags = self._lib.PKCS7_PARTIAL + final_flags = 0 + + if smime.SMIMEOptions.DetachedSignature in options: + # Don't embed the data in the PKCS7 structure + init_flags |= self._lib.PKCS7_DETACHED + final_flags |= self._lib.PKCS7_DETACHED + + # This just inits a structure for us. However, there + # are flags we need to set, joy. + p7 = self._lib.PKCS7_sign( + self._ffi.NULL, + self._ffi.NULL, + self._ffi.NULL, + self._ffi.NULL, + init_flags, + ) + self.openssl_assert(p7 != self._ffi.NULL) + p7 = self._ffi.gc(p7, self._lib.PKCS7_free) + signer_flags = 0 + # These flags are configurable on a per-signature basis + # but we've deliberately chosen to make the API only allow + # setting it across all signatures for now. + if smime.SMIMEOptions.NoCapabilities in options: + signer_flags |= self._lib.PKCS7_NOSMIMECAP + elif smime.SMIMEOptions.NoAttributes in options: + signer_flags |= self._lib.PKCS7_NOATTR + for certificate, private_key, hash_algorithm in builder._signers: + md = self._evp_md_non_null_from_algorithm(hash_algorithm) + p7signerinfo = self._lib.PKCS7_sign_add_signer( + p7, certificate._x509, private_key._evp_pkey, md, signer_flags + ) + self.openssl_assert(p7signerinfo != self._ffi.NULL) + + for option in options: + # DetachedSignature, NoCapabilities, and NoAttributes are already + # handled so we just need to check these last two options. + if option is smime.SMIMEOptions.Text: + final_flags |= self._lib.PKCS7_TEXT + elif option is smime.SMIMEOptions.Binary: + final_flags |= self._lib.PKCS7_BINARY + + bio_out = self._create_mem_bio_gc() + if encoding is serialization.Encoding.PEM: + # This finalizes the structure + res = self._lib.SMIME_write_PKCS7( + bio_out, p7, bio.bio, final_flags + ) + else: + assert encoding is serialization.Encoding.DER + # We need to call finalize here becauase i2d_PKCS7_bio does not + # finalize. + res = self._lib.PKCS7_final(p7, bio.bio, final_flags) + self.openssl_assert(res == 1) + res = self._lib.i2d_PKCS7_bio(bio_out, p7) + self.openssl_assert(res == 1) + return self._read_mem_bio(bio_out) + class GetCipherByName(object): def __init__(self, fmt): diff --git a/src/cryptography/hazmat/primitives/smime.py b/src/cryptography/hazmat/primitives/smime.py new file mode 100644 index 000000000..538ba6a00 --- /dev/null +++ b/src/cryptography/hazmat/primitives/smime.py @@ -0,0 +1,109 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +from enum import Enum + +from cryptography import x509 +from cryptography.hazmat.backends import _get_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.utils import _check_byteslike + + +class SMIMESignatureBuilder(object): + def __init__(self, data=None, signers=[]): + self._data = data + self._signers = signers + + def set_data(self, data): + _check_byteslike("data", data) + if self._data is not None: + raise ValueError("data may only be set once") + + return SMIMESignatureBuilder(data, self._signers) + + def add_signer(self, certificate, private_key, hash_algorithm): + if not isinstance( + hash_algorithm, + ( + hashes.SHA1, + hashes.SHA224, + hashes.SHA256, + hashes.SHA384, + hashes.SHA512, + ), + ): + raise TypeError( + "hash_algorithm must be one of hashes.SHA1, SHA224, " + "SHA256, SHA384, or SHA512" + ) + if not isinstance(certificate, x509.Certificate): + raise TypeError("certificate must be a x509.Certificate") + + if not isinstance( + private_key, (rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey) + ): + raise TypeError("Only RSA & EC keys are supported at this time.") + + return SMIMESignatureBuilder( + self._data, + self._signers + [(certificate, private_key, hash_algorithm)], + ) + + def sign(self, encoding, options, backend=None): + if len(self._signers) == 0: + raise ValueError("Must have at least one signer") + if self._data is None: + raise ValueError("You must add data to sign") + options = list(options) + if not all(isinstance(x, SMIMEOptions) for x in options): + raise ValueError("options must be from the SMIMEOptions enum") + if ( + encoding is not serialization.Encoding.PEM + and encoding is not serialization.Encoding.DER + ): + raise ValueError("Must be PEM or DER from the Encoding enum") + + # Text is a meaningless option unless it is accompanied by + # DetachedSignature + if ( + SMIMEOptions.Text in options + and SMIMEOptions.DetachedSignature not in options + ): + raise ValueError( + "When passing the Text option you must also pass " + "DetachedSignature" + ) + + if ( + SMIMEOptions.Text in options + and encoding is serialization.Encoding.DER + ): + raise ValueError( + "The Text option does nothing when serializing to DER" + ) + + # No attributes implies no capabilities so we'll error if you try to + # pass both. + if ( + SMIMEOptions.NoAttributes in options + and SMIMEOptions.NoCapabilities in options + ): + raise ValueError( + "NoAttributes is a superset of NoCapabilities. Do not pass " + "both values." + ) + + backend = _get_backend(backend) + return backend.smime_sign(self, encoding, options) + + +class SMIMEOptions(Enum): + Text = "Add text/plain MIME type" + Binary = "Don't translate input data into canonical MIME format" + DetachedSignature = "Don't embed data in the PKCS7 structure" + NoCapabilities = "Don't embed SMIME capabilities" + NoAttributes = "Don't embed authenticatedAttributes" |
