mirror of
https://github.com/saymrwulf/cryptography.git
synced 2026-05-14 20:37:55 +00:00
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:
parent
c61f24bb4d
commit
20c0388086
7 changed files with 820 additions and 1 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -14,5 +14,6 @@ Primitives
|
|||
mac/index
|
||||
cryptographic-hashes
|
||||
symmetric-encryption
|
||||
smime
|
||||
padding
|
||||
twofactor
|
||||
|
|
|
|||
128
docs/hazmat/primitives/smime.rst
Normal file
128
docs/hazmat/primitives/smime.rst
Normal 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.
|
||||
|
|
@ -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 *);
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
109
src/cryptography/hazmat/primitives/smime.py
Normal file
109
src/cryptography/hazmat/primitives/smime.py
Normal 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"
|
||||
518
tests/hazmat/primitives/test_smime.py
Normal file
518
tests/hazmat/primitives/test_smime.py
Normal 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,
|
||||
)
|
||||
Loading…
Reference in a new issue