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
This commit is contained in:
Paul Kehrer 2020-09-19 18:07:26 -05:00 committed by GitHub
parent c61f24bb4d
commit 20c0388086
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 820 additions and 1 deletions

View file

@ -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:

View file

@ -14,5 +14,6 @@ Primitives
mac/index
cryptographic-hashes
symmetric-encryption
smime
padding
twofactor

View file

@ -0,0 +1,128 @@
.. 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`.
:return bytes: The signed S/MIME message.
: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 is disallowed 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.

View file

@ -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 *);

View file

@ -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):

View file

@ -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"

View file

@ -0,0 +1,518 @@
# 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
import os
import pytest
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization, smime
from cryptography.hazmat.primitives.asymmetric import ed25519
from .utils import load_vectors_from_file
# We have no public verification API and won't be adding one until we get
# some requirements from users so this function exists to give us basic
# verification for the signing tests.
def _smime_verify(encoding, sig, msg, certs, options, backend):
sig_bio = backend._bytes_to_bio(sig)
if encoding is serialization.Encoding.DER:
p7 = backend._lib.d2i_PKCS7_bio(sig_bio.bio, backend._ffi.NULL)
else:
p7 = backend._lib.SMIME_read_PKCS7(sig_bio.bio, backend._ffi.NULL)
backend.openssl_assert(p7 != backend._ffi.NULL)
p7 = backend._ffi.gc(p7, backend._lib.PKCS7_free)
flags = 0
for option in options:
if option is smime.SMIMEOptions.Text:
flags |= backend._lib.PKCS7_TEXT
store = backend._lib.X509_STORE_new()
backend.openssl_assert(store != backend._ffi.NULL)
store = backend._ffi.gc(store, backend._lib.X509_STORE_free)
for cert in certs:
res = backend._lib.X509_STORE_add_cert(store, cert._x509)
backend.openssl_assert(res == 1)
if msg is None:
res = backend._lib.PKCS7_verify(
p7,
backend._ffi.NULL,
store,
backend._ffi.NULL,
backend._ffi.NULL,
flags,
)
else:
msg_bio = backend._bytes_to_bio(msg)
res = backend._lib.PKCS7_verify(
p7, backend._ffi.NULL, store, msg_bio.bio, backend._ffi.NULL, flags
)
backend.openssl_assert(res == 1)
def _load_cert_key():
key = load_vectors_from_file(
os.path.join("x509", "custom", "ca", "ca_key.pem"),
lambda pemfile: serialization.load_pem_private_key(
pemfile.read(), None
),
mode="rb",
)
cert = load_vectors_from_file(
os.path.join("x509", "custom", "ca", "ca.pem"),
loader=lambda pemfile: x509.load_pem_x509_certificate(pemfile.read()),
mode="rb",
)
return cert, key
class TestSMIMEBuilder(object):
def test_invalid_data(self):
builder = smime.SMIMESignatureBuilder()
with pytest.raises(TypeError):
builder.set_data(u"not bytes")
def test_set_data_twice(self):
builder = smime.SMIMESignatureBuilder().set_data(b"test")
with pytest.raises(ValueError):
builder.set_data(b"test")
def test_sign_no_signer(self):
builder = smime.SMIMESignatureBuilder().set_data(b"test")
with pytest.raises(ValueError):
builder.sign(serialization.Encoding.PEM, [])
def test_sign_no_data(self):
cert, key = _load_cert_key()
builder = smime.SMIMESignatureBuilder().add_signer(
cert, key, hashes.SHA256()
)
with pytest.raises(ValueError):
builder.sign(serialization.Encoding.PEM, [])
def test_unsupported_hash_alg(self):
cert, key = _load_cert_key()
with pytest.raises(TypeError):
smime.SMIMESignatureBuilder().add_signer(
cert, key, hashes.SHA512_256()
)
def test_not_a_cert(self):
cert, key = _load_cert_key()
with pytest.raises(TypeError):
smime.SMIMESignatureBuilder().add_signer(
b"notacert", key, hashes.SHA256()
)
@pytest.mark.supported(
only_if=lambda backend: backend.ed25519_supported(),
skip_message="Does not support ed25519.",
)
def test_unsupported_key_type(self, backend):
cert, _ = _load_cert_key()
key = ed25519.Ed25519PrivateKey.generate()
with pytest.raises(TypeError):
smime.SMIMESignatureBuilder().add_signer(
cert, key, hashes.SHA256()
)
def test_sign_invalid_options(self):
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(b"test")
.add_signer(cert, key, hashes.SHA256())
)
with pytest.raises(ValueError):
builder.sign(serialization.Encoding.PEM, [b"invalid"])
def test_sign_invalid_encoding(self):
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(b"test")
.add_signer(cert, key, hashes.SHA256())
)
with pytest.raises(ValueError):
builder.sign(serialization.Encoding.Raw, [])
def test_sign_invalid_options_text_no_detached(self):
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(b"test")
.add_signer(cert, key, hashes.SHA256())
)
options = [smime.SMIMEOptions.Text]
with pytest.raises(ValueError):
builder.sign(serialization.Encoding.PEM, options)
def test_sign_invalid_options_text_der_encoding(self):
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(b"test")
.add_signer(cert, key, hashes.SHA256())
)
options = [
smime.SMIMEOptions.Text,
smime.SMIMEOptions.DetachedSignature,
]
with pytest.raises(ValueError):
builder.sign(serialization.Encoding.DER, options)
def test_sign_invalid_options_no_attrs_and_no_caps(self):
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(b"test")
.add_signer(cert, key, hashes.SHA256())
)
options = [
smime.SMIMEOptions.NoAttributes,
smime.SMIMEOptions.NoCapabilities,
]
with pytest.raises(ValueError):
builder.sign(serialization.Encoding.PEM, options)
def test_smime_sign_detached(self, backend):
data = b"hello world"
cert, key = _load_cert_key()
options = [smime.SMIMEOptions.DetachedSignature]
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
sig = builder.sign(serialization.Encoding.PEM, options)
sig_binary = builder.sign(serialization.Encoding.DER, options)
# We don't have a generic ASN.1 parser available to us so we instead
# will assert on specific byte sequences being present based on the
# parameters chosen above.
assert b"sha-256" in sig
# Detached signature means that the signed data is *not* embedded into
# the PKCS7 structure itself, but is present in the PEM serialization
# as a separate section before the PKCS7 data. So we should expect to
# have data in sig but not in sig_binary
assert data in sig
_smime_verify(
serialization.Encoding.PEM, sig, data, [cert], options, backend
)
assert data not in sig_binary
_smime_verify(
serialization.Encoding.DER,
sig_binary,
data,
[cert],
options,
backend,
)
def test_sign_byteslike(self):
data = bytearray(b"hello world")
cert, key = _load_cert_key()
options = [smime.SMIMEOptions.DetachedSignature]
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
sig = builder.sign(serialization.Encoding.PEM, options)
assert bytes(data) in sig
@pytest.mark.parametrize(
("hash_alg", "expected_value"),
[
(hashes.SHA1(), b"\x06\x05+\x0e\x03\x02\x1a"),
(hashes.SHA256(), b"\x06\t`\x86H\x01e\x03\x04\x02\x01"),
(hashes.SHA384(), b"\x06\t`\x86H\x01e\x03\x04\x02\x02"),
(hashes.SHA512(), b"\x06\t`\x86H\x01e\x03\x04\x02\x03"),
],
)
def test_smime_sign_alternate_digests_der(
self, hash_alg, expected_value, backend
):
data = b"hello world"
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hash_alg)
)
options = []
sig = builder.sign(serialization.Encoding.DER, options)
assert expected_value in sig
_smime_verify(
serialization.Encoding.DER, sig, None, [cert], options, backend
)
@pytest.mark.parametrize(
("hash_alg", "expected_value"),
[
(hashes.SHA1(), b"sha1"),
(hashes.SHA256(), b"sha-256"),
(hashes.SHA384(), b"sha-384"),
(hashes.SHA512(), b"sha-512"),
],
)
def test_smime_sign_alternate_digests_detached_pem(
self, hash_alg, expected_value
):
data = b"hello world"
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hash_alg)
)
options = [smime.SMIMEOptions.DetachedSignature]
sig = builder.sign(serialization.Encoding.PEM, options)
# When in detached signature mode the hash algorithm is stored as a
# byte string like "sha-384".
assert expected_value in sig
def test_smime_sign_attached(self, backend):
data = b"hello world"
cert, key = _load_cert_key()
options = []
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
sig_binary = builder.sign(serialization.Encoding.DER, options)
# When not passing detached signature the signed data is embedded into
# the PKCS7 structure itself
assert data in sig_binary
_smime_verify(
serialization.Encoding.DER,
sig_binary,
None,
[cert],
options,
backend,
)
def test_smime_sign_binary(self, backend):
data = b"hello\nworld"
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
options = []
sig_no_binary = builder.sign(serialization.Encoding.DER, options)
sig_binary = builder.sign(
serialization.Encoding.DER, [smime.SMIMEOptions.Binary]
)
# Binary prevents translation of LF to CR+LF (SMIME canonical form)
# so data should not be present in sig_no_binary, but should be present
# in sig_binary
assert data not in sig_no_binary
_smime_verify(
serialization.Encoding.DER,
sig_no_binary,
None,
[cert],
options,
backend,
)
assert data in sig_binary
_smime_verify(
serialization.Encoding.DER,
sig_binary,
None,
[cert],
options,
backend,
)
def test_smime_sign_smime_canonicalization(self, backend):
data = b"hello\nworld"
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
options = []
sig_binary = builder.sign(serialization.Encoding.DER, options)
# LF gets converted to CR+LF (SMIME canonical form)
# so data should not be present in the sig
assert data not in sig_binary
assert b"hello\r\nworld" in sig_binary
_smime_verify(
serialization.Encoding.DER,
sig_binary,
None,
[cert],
options,
backend,
)
def test_smime_sign_text(self, backend):
data = b"hello world"
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
options = [
smime.SMIMEOptions.Text,
smime.SMIMEOptions.DetachedSignature,
]
sig_pem = builder.sign(serialization.Encoding.PEM, options)
# The text option adds text/plain headers to the S/MIME message
# These headers are only relevant in PEM mode, not binary, which is
# just the PKCS7 structure itself.
assert b"text/plain" in sig_pem
# When passing the Text option the header is prepended so the actual
# signed data is this.
signed_data = b"Content-Type: text/plain\r\n\r\nhello world"
_smime_verify(
serialization.Encoding.PEM,
sig_pem,
signed_data,
[cert],
options,
backend,
)
def test_smime_sign_no_capabilities(self, backend):
data = b"hello world"
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
options = [smime.SMIMEOptions.NoCapabilities]
sig_binary = builder.sign(serialization.Encoding.DER, options)
# NoCapabilities removes the SMIMECapabilities attribute from the
# PKCS7 structure. This is an ASN.1 sequence with the
# OID 1.2.840.113549.1.9.15. It does NOT remove all authenticated
# attributes, so we verify that by looking for the signingTime OID.
# 1.2.840.113549.1.9.15 SMIMECapabilities as an ASN.1 DER encoded OID
assert b"\x06\t*\x86H\x86\xf7\r\x01\t\x0f" not in sig_binary
# 1.2.840.113549.1.9.5 signingTime as an ASN.1 DER encoded OID
assert b"\x06\t*\x86H\x86\xf7\r\x01\t\x05" in sig_binary
_smime_verify(
serialization.Encoding.DER,
sig_binary,
None,
[cert],
options,
backend,
)
def test_smime_sign_no_attributes(self, backend):
data = b"hello world"
cert, key = _load_cert_key()
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA256())
)
options = [smime.SMIMEOptions.NoAttributes]
sig_binary = builder.sign(serialization.Encoding.DER, options)
# NoAttributes removes all authenticated attributes, so we shouldn't
# find SMIMECapabilities or signingTime.
# 1.2.840.113549.1.9.15 SMIMECapabilities as an ASN.1 DER encoded OID
assert b"\x06\t*\x86H\x86\xf7\r\x01\t\x0f" not in sig_binary
# 1.2.840.113549.1.9.5 signingTime as an ASN.1 DER encoded OID
assert b"\x06\t*\x86H\x86\xf7\r\x01\t\x05" not in sig_binary
_smime_verify(
serialization.Encoding.DER,
sig_binary,
None,
[cert],
options,
backend,
)
def test_multiple_signers(self, backend):
data = b"hello world"
cert, key = _load_cert_key()
rsa_key = load_vectors_from_file(
os.path.join("x509", "custom", "ca", "rsa_key.pem"),
lambda pemfile: serialization.load_pem_private_key(
pemfile.read(), None
),
mode="rb",
)
rsa_cert = load_vectors_from_file(
os.path.join("x509", "custom", "ca", "rsa_ca.pem"),
loader=lambda pemfile: x509.load_pem_x509_certificate(
pemfile.read()
),
mode="rb",
)
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA512())
.add_signer(rsa_cert, rsa_key, hashes.SHA512())
)
options = []
sig = builder.sign(serialization.Encoding.DER, options)
# There should be three SHA512 OIDs in this structure
assert sig.count(b"\x06\t`\x86H\x01e\x03\x04\x02\x03") == 3
_smime_verify(
serialization.Encoding.DER,
sig,
None,
[cert, rsa_cert],
options,
backend,
)
def test_multiple_signers_different_hash_algs(self, backend):
data = b"hello world"
cert, key = _load_cert_key()
rsa_key = load_vectors_from_file(
os.path.join("x509", "custom", "ca", "rsa_key.pem"),
lambda pemfile: serialization.load_pem_private_key(
pemfile.read(), None
),
mode="rb",
)
rsa_cert = load_vectors_from_file(
os.path.join("x509", "custom", "ca", "rsa_ca.pem"),
loader=lambda pemfile: x509.load_pem_x509_certificate(
pemfile.read()
),
mode="rb",
)
builder = (
smime.SMIMESignatureBuilder()
.set_data(data)
.add_signer(cert, key, hashes.SHA384())
.add_signer(rsa_cert, rsa_key, hashes.SHA512())
)
options = []
sig = builder.sign(serialization.Encoding.DER, options)
# There should be two SHA384 and two SHA512 OIDs in this structure
assert sig.count(b"\x06\t`\x86H\x01e\x03\x04\x02\x02") == 2
assert sig.count(b"\x06\t`\x86H\x01e\x03\x04\x02\x03") == 2
_smime_verify(
serialization.Encoding.DER,
sig,
None,
[cert, rsa_cert],
options,
backend,
)