Skip to content

smime signer support #5465

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 19, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Changelog

* Support for OpenSSL 1.0.2 has been removed. Users on older version of OpenSSL
will need to upgrade.
* Added basic support for SMIME signing via
:class:`~cryptography.hazmat.primitives.smime.SMIMESignatureBuilder`.

.. _v3-1:

Expand Down
1 change: 1 addition & 0 deletions docs/hazmat/primitives/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ Primitives
mac/index
cryptographic-hashes
symmetric-encryption
smime
padding
twofactor
126 changes: 126 additions & 0 deletions docs/hazmat/primitives/smime.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
.. hazmat::

S/MIME
======

.. module:: cryptography.hazmat.primitives.smime

.. testsetup::

ca_key = b"""
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgA8Zqz5vLeR0ePZUe
jBfdyMmnnI4U5uAJApWTsMn/RuWhRANCAAQY/8+7+Tm49d3D7sBAiwZ1BqtPzdgs
UiROH+AQRme1XxW5Yr07zwxvvhr3tKEPtLnLboazUPlsUb/Bgte+xfkF
-----END PRIVATE KEY-----
""".strip()

ca_cert = b"""
-----BEGIN CERTIFICATE-----
MIIBUTCB96ADAgECAgIDCTAKBggqhkjOPQQDAjAnMQswCQYDVQQGEwJVUzEYMBYG
A1UEAwwPY3J5cHRvZ3JhcGh5IENBMB4XDTE3MDEwMTEyMDEwMFoXDTM4MTIzMTA4
MzAwMFowJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBDQTBZ
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABBj/z7v5Obj13cPuwECLBnUGq0/N2CxS
JE4f4BBGZ7VfFblivTvPDG++Gve0oQ+0uctuhrNQ+WxRv8GC177F+QWjEzARMA8G
A1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhANES742XWm64tkGnz8Dn
pG6u2lHkZFQr3oaVvPcemvlbAiEA0WGGzmYx5C9UvfXIK7NEziT4pQtyESE0uRVK
Xw4nMqk=
-----END CERTIFICATE-----
""".strip()


S/MIME provides a method to send and receive signed MIME messages. It is
commonly used in email. S/MIME has multiple versions, but this
module implements a subset of :rfc:`2632`, also known as S/MIME Version 3.


.. class:: SMIMESignatureBuilder

.. versionadded:: 3.2

.. doctest::

>>> from cryptography.hazmat.primitives import hashes, serialization, smime
>>> from cryptography import x509
>>> cert = x509.load_pem_x509_certificate(ca_cert)
>>> key = serialization.load_pem_private_key(ca_key, None)
>>> options = [smime.SMIMEOptions.DetachedSignature]
>>> smime.SMIMESignatureBuilder().set_data(
... b"data to sign"
... ).add_signer(
... cert, key, hashes.SHA256()
... ).sign(
... serialization.Encoding.PEM, options
... )
b'...'

.. method:: set_data(data)

:param data: The data to be hashed and signed.
:type data: :term:`bytes-like`

.. method:: add_signer(certificate, private_key, hash_algorithm)

:param certificate: The :class:`~cryptography.x509.Certificate`.

:param private_key: The
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` or
:class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey`
associated with the certificate provided.

:param hash_algorithm: The
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that
will be used to generate the signature. This must be an instance of
:class:`~cryptography.hazmat.primitives.hashes.SHA1`,
:class:`~cryptography.hazmat.primitives.hashes.SHA224`,
:class:`~cryptography.hazmat.primitives.hashes.SHA256`,
:class:`~cryptography.hazmat.primitives.hashes.SHA384`, or
:class:`~cryptography.hazmat.primitives.hashes.SHA512`.

.. method:: sign(encoding, options, backend=None)

:param encoding: :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`
or :attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`.

:param options: A list of :class:`~cryptography.hazmat.primitives.smime.SMIMEOptions`.

:param backend: An optional backend.


.. class:: SMIMEOptions

.. versionadded:: 3.2

An enumeration of options for S/MIME signature creation.

.. attribute:: Text

The text option adds ``text/plain`` headers to the S/MIME message when
serializing to
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM`.
This option has no effect with ``DER`` serialization.

.. attribute:: Binary

S/MIME signing normally converts line endings (LF to CRLF). When
passing this option the data will not be converted.

.. attribute:: DetachedSignature

Don't embed the signed data within the ASN.1. When signing with
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM` this
also results in the data being added as clear text before the
PEM encoded structure.

.. attribute:: NoCapabilities

S/MIME structures contain a ``MIMECapabilities`` section inside the
``authenticatedAttributes``. Passing this as an option removes
``MIMECapabilities``.

.. attribute:: NoAttributes

S/MIME structures contain an ``authenticatedAttributes`` section.
Passing this as an option removes that section. Note that if you
pass ``NoAttributes`` you can't pass ``NoCapabilities`` since
``NoAttributes`` removes ``MIMECapabilities`` and more.
1 change: 1 addition & 0 deletions src/_cffi_src/openssl/pkcs7.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
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 *);
Expand Down
63 changes: 62 additions & 1 deletion src/cryptography/hazmat/backends/openssl/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
_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,
Expand Down Expand Up @@ -2690,6 +2690,67 @@ def _load_pkcs7_certificates(self, p7):

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)
for certificate, private_key, hash_algorithm in builder._signers:
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

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):
Expand Down
109 changes: 109 additions & 0 deletions src/cryptography/hazmat/primitives/smime.py
Original file line number Diff line number Diff line change
@@ -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"
Loading