summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2020-09-19 18:07:26 -0500
committerGitHub <noreply@github.com>2020-09-19 19:07:26 -0400
commit20c0388086b4eec91fdf1f9fd9535f4c741e4851 (patch)
treee5b212bf218ecdf22e05ca018cb5e352626e6572 /src
parentc61f24bb4df584bd3a2fb640298dad4bd6351ecd (diff)
downloadcryptography-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.py1
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py62
-rw-r--r--src/cryptography/hazmat/primitives/smime.py109
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"