cryptography
cryptography copied to clipboard
Add S/MIME encryption
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.
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.
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.
The idea of support extremely modern encryption in order to nudge people towards modern things appeals to me :-)
On Sun, Oct 25, 2020 at 12:13 PM frennkie [email protected] wrote:
Yes, I agree.. interoperability is a huge problem here.
S/MIME 4.0 (RFC 8551 https://tools.ietf.org/html/rfc8551#section-2.7) 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.
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/pyca/cryptography/issues/5488#issuecomment-716172218, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAAGBDEBYX34QVIRSPZED3SMRFBRANCNFSM4ST2PKGA .
-- All that is necessary for evil to succeed is for good people to do nothing.
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.
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).
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.