Skip to content
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

Add S/MIME encryption #5488

Closed
frennkie opened this issue Oct 16, 2020 · 11 comments
Closed

Add S/MIME encryption #5488

frennkie opened this issue Oct 16, 2020 · 11 comments

Comments

@frennkie
Copy link
Contributor

frennkie commented Oct 16, 2020

Following up on the comment in #1621 I would like to suggest to add the possibility to use pyca/cryptography for encrypting ("envelope") S/MIME messages.

I have been using this in past projects:

def encrypt_message(message, certs_recipients,
                    content_enc_alg="aes256_cbc", key_enc_alg="rsaes_pkcs1v15", prefix=""):

As far as I see it most parts needed in order to encrypt a message are already present in cryptography:

  • Symmetric encryption of the content using e.g. AES CBC with a 256 bit key
  • Asymmetric encryption of the symmetric encryption key using PKCS1v15 for each of the recipients

What I have not found/solved yet is that an equivalent to the cms.RecipientInfo structure from asn1crypto would be required.

Similar to the recent addition of S/MIME signing an API could look like this:

>>> smime.SMIMEEnvelopeBuilder().set_data(
...     b"data to encrypt"
... ).add_recepients(
...     [certs], asymmetric.padding.PKCS1v15()
... ).encrypt(
...     algorithms.AES(), modes.CBC()
... )
b'...'

In the end a combination of a first signed and then enveloped message would also be great.

@reaperhulk
Copy link
Member

Okay, looking at this... OpenSSL has PKCS7_encrypt, which is probably what we want to use here (if only because whatever weird things it does will be what people are already expecting). That function looks like this:

 PKCS7 *PKCS7_encrypt(STACK_OF(X509) *certs, BIO *in, const EVP_CIPHER *cipher, int flags);

So your API then looks more like:

>>> smime.PKCS7EnvelopeBuilder().set_data(
...     b"data to encrypt"
... ).add_recipient(
...     cert1
... ).add_recipient(
...     cert2
... ).encrypt(
...     <not sure>
... )
b'...'

Where the above API would reject any non-RSA PKCS1v15 X.509 certificates with a TypeError.

We need to be able to derive an EVP_CIPHER * as well, but I'm not sure how to do that. We can't easily reuse the algorithm or mode objects in this case. Our APIs all expect those to be instantiated and we determine properties from those objects (e.g. for AES we determine 128/192/256 from key length provided), as well as requiring the IVs/nonces to be provided upon initialization for modes.

We've discussed whether we should establish an enum of ciphersuites for algorithm selection in some of our serialization APIs (right now we only allow BestAvailable or NoEncryption). Maybe that same enum makes sense here? A straw man:

class EncryptionAlgorithm(Enum):
    AES_128_CBC = "AES-128 with CBC mode"
    AES_256_CBC = "AES-256 with CBC mode"
    TripleDES_CBC = "3DES with CBC mode"

Unfortunately, much like our issues with general key serialization, we're limited in the set of algorithms we can actually use...and the most usefully interoperable ones are by far the worst.

@frennkie
Copy link
Contributor Author

Yes, I agree.. interoperability is a huge problem here.

S/MIME 4.0 (RFC 8551) defines two modern content encryption algorithms (AES-GCM, ChaCha20-Poly1305) that are far better. Obviously these aren't yet broadly available/implemented.. but somebody has to start.. right?! :-D

PKCS7 *PKCS7_encrypt(STACK_OF(X509) *certs, BIO *in, const EVP_CIPHER *cipher, int flags);

I'm not entirely sure/convinced (yet) that using this would be the ideal way to go here.

@alex
Copy link
Member

alex commented Oct 25, 2020 via email

@reaperhulk
Copy link
Member

If there is an alternative to building these structures (and supporting modern encryption) I'd much rather go down that path than PKCS7_encrypt. That probably means we'd be getting in the business of constructing some ASN.1 ourselves, which is fine, but should be considered blocked on landing #5357 (Rust ASN.1!) for now.

@frennkie
Copy link
Contributor Author

frennkie commented Oct 25, 2020

As I indicated in the initial description of this issue I have already managed to encrypt an S/MIME message using pure python and cryptography. The missing part was the ASN.1 stuff for which I had to use asn1crypto.

Thanks for pointing out the Rust ASN.1 issue. I hadn't seen that yet (as I'm pretty new to this project).

@T-256
Copy link

T-256 commented Nov 26, 2023

Discussion is not enabled for this repo, so I ask here.
Are we able to encrypt with SMIME that mentioned in cryptography.hazmat.primitives.serialization.Encoding?

Currently we use some patches on https://github.com/balena/python-smime which is unmaintained:

from smime.block import get_cipher, AES
from smime.cert import certs_from_pem
from asn1crypto import cms

# some patch
def _encrypt(self, data):
    padded_data = self._pad(data, self.block_size)
    if not isinstance(padded_data, bytes):
        padded_data = padded_data.encode('utf-8')
    encrypted_content = self._encryptor.update(padded_data) + self._encryptor.finalize()
    return {
        'content_type': 'data',
        'content_encryption_algorithm': {
            'algorithm': self.algorithm,
            'parameters': self._iv
        },
        'encrypted_content': encrypted_content
    }
@staticmethod
def _pad(s, block_size):
    n = block_size - len(s) % block_size
    return s + n * (chr(n) if isinstance(s, str) else bytes([n]))
AES._pad = _pad
AES.encrypt = _encrypt


def encrypt(message, certs, algorithm='aes256_cbc'):
    block_cipher = get_cipher(algorithm)
    if block_cipher == None:
        raise ValueError('Unknown block algorithm')

    recipient_infos = []
    for cert_file in certs:
        for cert in certs_from_pem(cert_file):
            recipient_info = cert.recipient_info(block_cipher.session_key)
            if recipient_info == None:
                raise ValueError('Unknown public-key algorithm')
            recipient_infos.append(recipient_info)

    return cms.ContentInfo({
        'content_type': 'enveloped_data',
        'content': {
            'version': 'v0',
            'recipient_infos': recipient_infos,
            'encrypted_content_info': block_cipher.encrypt(message)
        }
    }).dump()

encrypt(<DATA>, [<BASE64 CERT>])

It does the job but recently we noticed it is so slow. Is there alternative to do this with cryptography?
Also, we can drop other dependencies and use only one package for crypto stuffs.

@facutuesca
Copy link
Contributor

@alex @reaperhulk If we wanted to support for S/MIME encryption, which algorithms do we want to support?

In S/MIME v3 (the same version we support for signing), the RFC states:

Sending and receiving agents MUST support encryption and decryption
with DES EDE3 CBC, hereinafter called "tripleDES" [3DES] [DES].
Receiving agents SHOULD support encryption and decryption using the
RC2 [RC2] or a compatible algorithm at a key size of 40 bits,
hereinafter called "RC2/40".

Sending and receiving agents MUST support Diffie-Hellman defined in
[DH].
Receiving agents SHOULD support rsaEncryption. Incoming encrypted
messages contain symmetric keys which are to be decrypted with a
user's private key. The size of the private key is determined during
key generation.
Sending agents SHOULD support rsaEncryption.

cc @woodruffw

@reaperhulk
Copy link
Member

I believe there's a later RFC that adds AES and I'd be inclined to support only that as a first pass if we're going to implement this at all. I'm strongly against RC2 (barring some very large reason why we need compatibility but absolutely no security) and weakly against 3DES.

@facutuesca
Copy link
Contributor

True, looking at the S/MIME v3.2 RFCs:

Sending and receiving agents:
- MUST support encryption and decryption with AES-128 CBC [CMSAES].
- SHOULD+ support encryption and decryption with AES-192 CBC and AES-256 CBC [CMSAES].
- SHOULD- support encryption and decryption with DES EDE3 CBC hereinafter called "tripleDES" [CMSALG].

Receiving and sending agents:
- MUST support RSA Encryption, as specified in [CMSALG].
- SHOULD+ support RSAES-OAEP, as specified in [RSAOAEP].
- SHOULD- support DH ephemeral-static mode, as specified in [CMSALG] and [SP800-57].
(....)
Note that S/MIME v3.1 clients might only implement key encryption and
decryption using the rsaEncryption algorithm. Note that S/MIME v3
clients might only implement key encryption and decryption using the
Diffie-Hellman algorithm. Also note that S/MIME v2 clients are only
capable of decrypting content-encryption keys using the rsaEncryption
algorithm.

So for the first pass, we could support AES-128 CBC for content encryption, and RSA (PKCS #1 v1.5) for key encryption. Does that sound good?

@reaperhulk
Copy link
Member

I think that’s a reasonable path, yes.

@reaperhulk
Copy link
Member

This was completed in #10889

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

No branches or pull requests

5 participants