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 support for encrypting S/MIME messages #10889

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

facutuesca
Copy link
Contributor

@facutuesca facutuesca commented Apr 25, 2024

I'm opening this PR with an initial implementation of S/MIME encryption, in order to better discuss the API design, the algorithms we want to support, and how we want to approach testing.

The target is a subset of S/MIME v3.2 (RFC5751):

  • Content encryption is done using AES-128-CBC
  • Key management is done with key transport: the symmetric encryption key used for the message is included and encrypted using the recipients' public keys.
  • The other two key management methods (previously-distributed keys and key agreement) are not supported.
  • The symmetric encryption key is encrypted using RSA (PKCS1 v1.5). That is, we only support recipients with RSA public keys, and we use PKCS1v15 padding.

I've been checking the output produced against the output of the openssl-cms command, since openssl-smime is now legacy. For example:

# encrypt
openssl cms -encrypt -recip cryptography/vectors/cryptography_vectors/x509/custom/ca/rsa_ca.pem -aes-128-cbc -in message.txt -out out.txt
# decrypt
openssl cms -decrypt -recip cryptography/vectors/cryptography_vectors/x509/custom/ca/rsa_ca.pem -inkey cryptography/vectors/cryptography_vectors/x509/custom/ca/rsa_key.pem -in out.txt

I added some tests for the unencrypted parts of the message, but complete testing would require that we parse and decrypt the messages. We could follow a similar approach as with testing S/MIME signing, where we call OpenSSL directly to parse and check our output during the tests:

def _pkcs7_verify(encoding, sig, msg, certs, options, backend):

cc @alex @reaperhulk @woodruffw

(the issue tracking this feature is #5488)

//
// AES-IV ::= OCTET STRING (SIZE(16))
#[defined_by(oid::AES_128_CBC_OID)]
AesCbc(&'a [u8]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity: does rust-asn1 know to turn this &[u8] into the inner value of the OCTET STRING, or does this end up containing the raw TLV for the OCTET STRING? I suspect it's the former, but we should confirm.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&[u8] in rust-asn1 are OCTET STRINGs, not raw TLVs.

Comment on lines +77 to +79
pub const AES_256_CBC_OID: asn1::ObjectIdentifier = asn1::oid!(2, 16, 840, 1, 101, 3, 4, 1, 42);
pub const AES_192_CBC_OID: asn1::ObjectIdentifier = asn1::oid!(2, 16, 840, 1, 101, 3, 4, 1, 22);
pub const AES_128_CBC_OID: asn1::ObjectIdentifier = asn1::oid!(2, 16, 840, 1, 101, 3, 4, 1, 2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for cross-checking: confirmed these against their parent arc: https://oidref.com/2.16.840.1.101.3.4.1

Comment on lines 316 to 324
fp = io.BytesIO()
g = email.generator.BytesGenerator(
fp,
maxheaderlen=0,
mangle_from_=False,
policy=m.policy.clone(linesep="\n"),
)
g.flatten(m)
return fp.getvalue()
Copy link
Contributor

@woodruffw woodruffw Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: you might be able to get away with just m.as_bytes(...) rather than jumping through a BytesIO + BytesGenerator here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

@@ -225,6 +307,26 @@ def _smime_encode(
return fp.getvalue()


def _smime_enveloped_encode(data: bytes) -> bytes:
# This function works pretty hard to replicate what OpenSSL does
# precisely. For good and for ill.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: might be good to include a URL or code reference for OpenSSL's construction of the encoding here, just in case this ever needs to be re-evaluated 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually a comment from the other encoder function that I used as a base for this one (_smime_signed_encode).

I think it doesn't make much sense here since we just add the headers, so I removed it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants