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 decrypting S/MIME messages #11555

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

nitneuqr
Copy link
Contributor

@nitneuqr nitneuqr commented Sep 6, 2024

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

My essential thoughts for testing were to do the round-trip: encryption using the PKCS7EnvelopeBuilder and decryption using the PKCS7EnvelopeDecryptor (or whatever its name will be). No tests were made against openssl, even though it might interesting.

For the Python API, I've tried to stick as much as possible with the current API design (PKCS7SignatureBuilder, PKCS7EnvelopeBuilder).

I'm new to rust, so please let me know if you see some issues in variable lifetime, or some unnecessary copying between Python & Rust.

During development, I've realized that we could migrate PKCS7 unpadding to rust instead of using types, so I did migrate it. I'll probably open another smaller PR for that matter (if that makes sense?). I still have some code using PKCS7 Unpadding on Python if needed.

cc @alex

@alex
Copy link
Member

alex commented Sep 6, 2024

Thanks for working on this! I haven't had a chance to look in depth, but re: PKCS7Unpadder, it'd be great to have that in a stand-alone PR.

@nitneuqr
Copy link
Contributor Author

nitneuqr commented Sep 6, 2024

Thanks for working on this! I haven't had a chance to look in depth, but re: PKCS7Unpadder, it'd be great to have that in a stand-alone PR.

Sure! Opened #11556 for that matter.

@nitneuqr nitneuqr force-pushed the smime-decryption branch 2 times, most recently from cb082ce to e1c4620 Compare September 8, 2024 09:01
@nitneuqr
Copy link
Contributor Author

nitneuqr commented Sep 8, 2024

Rebased to main after integration of #11556. Should I split it again in smaller PRs? Maybe around symmetric_decrypt or other subfunctions in the code.

@alex
Copy link
Member

alex commented Sep 8, 2024

We're always happy to have smaller PRs split out (though it can sometimes be complicated due to coverage). I'm hoping to have time to review this today, though it might be tomorrow.

@alex
Copy link
Member

alex commented Sep 10, 2024

@facutuesca FYI, if you're interested

@nitneuqr
Copy link
Contributor Author

@alex @reaperhulk took into account the first comments and adapted the tests / coverage accordingly.

Do you have any time for another review? Do you need anything else from me before reviewing? 😊

@alex
Copy link
Member

alex commented Oct 5, 2024

Sorry I got behind, will have a look now. Thanks for your patience!

Copy link
Member

@alex alex left a comment

Choose a reason for hiding this comment

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

Did an initial review of the code -- I haven't really sat down and reviewed the core PKCS#7 logic against hte RFC yet though.

Comment on lines 266 to 285
def pkcs7_decrypt_der(
data: bytes,
certificate: x509.Certificate,
private_key: rsa.RSAPrivateKey,
options: typing.Iterable[PKCS7Options],
) -> bytes:
return _pkcs7_decrypt(
data, certificate, private_key, options, serialization.Encoding.DER
)


def pkcs7_decrypt_pem(
data: bytes,
certificate: x509.Certificate,
private_key: rsa.RSAPrivateKey,
options: typing.Iterable[PKCS7Options],
) -> bytes:
return _pkcs7_decrypt(
data, certificate, private_key, options, serialization.Encoding.PEM
)
Copy link
Member

Choose a reason for hiding this comment

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

I don't love needing to pass an encoding down, ideally we'd be able to PEM decode the data and then simply call pkcs7_decrypt_der with the results. That's probably annoying to do in Python though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A few solutions:

  • Separating the PEM decode logic in another rust function, and calling it separately
  • Finding a library that does PEM to DER in Python
  • Coding it in-house (but probably annoying and / or dirty)

I'm for the 1st one, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've implemented a quick function in python using rust to decode pem to der. Please let me know what you think!

src/cryptography/hazmat/primitives/serialization/pkcs7.py Outdated Show resolved Hide resolved
src/rust/src/pkcs12.rs Outdated Show resolved Hide resolved
src/rust/src/pkcs7.rs Outdated Show resolved Hide resolved
};

// Deserialize the content info
let content_info = asn1::parse_single::<pkcs7::ContentInfo<'_>>(&extracted_data).unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

@reaperhulk This would be our first time using our DER parser for PKCS7. On the one hand: new API, so there's no backwards-compat concerns. On the other hand, surely someone is emitting awful BER PKCS#7. Any concerns?

Copy link
Member

Choose a reason for hiding this comment

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

@reaperhulk just going to keep pinging you on this :-) (Enjoy your vacation)

Copy link
Member

Choose a reason for hiding this comment

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

Let's assume we release with DER only parsing. In that scenario we're attempting to empirically determine whether or not there's enough BER out there to matter. If no one comes to complain then great, job done. However, if we discover we're wrong and BER is relevant, what is our plan? Are we going to add BER support in rust?

Copy link
Member

Choose a reason for hiding this comment

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

If we do determine that this API is basically low utility for many users because their S/MIME messages aren't valid DER, it seems we have a few options:

  1. Declare that's how it goes. This is a net new API, so no users are worse off than they were before. May result in greater timelines for adoption of this, and migration away from other libraries.

  2. Re-implement in terms of OpenSSL S/MIME APIs. Probably straightforward, although as-ever, assessing the compatibility surface will be a PITA.

  3. Add BER support to rust-asn1. No fucking chance, not worth discussing.

  4. We force ourselves to do a survey of the ecosystem before landing this so we can contact producers of non-DER S/MIMEs.

I have a soft spot for 1, I could be persuaded of 4, and if we really need to do it, I guess 2 is the way to go.

WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

Does Go support this? They're DER only for whatever set of PKCS7 they support right?

Copy link
Member

Choose a reason for hiding this comment

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

src/rust/src/pkcs7.rs Outdated Show resolved Hide resolved
src/rust/src/test_support.rs Outdated Show resolved Hide resolved
Copy link
Member

@alex alex left a comment

Choose a reason for hiding this comment

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

@reaperhulk would still like your views on deserialization for PKCS#7 in Rust.

src/rust/src/pkcs7.rs Outdated Show resolved Hide resolved
Comment on lines +267 to +277
// Return content in different form, based on options
let text_mode = options.contains(types::PKCS7_TEXT.get(py)?)?;
let plain_data = if options.contains(types::PKCS7_BINARY.get(py)?)? {
plain_content
} else {
let decanonicalized = smime_decanonicalize(plain_content.as_bytes(), text_mode);
pyo3::types::PyBytes::new_bound(py, decanonicalized.into_owned().as_slice())
};
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: Think about this more.

src/rust/src/test_support.rs Outdated Show resolved Hide resolved
vectors/cryptography_vectors/pkcs7/rsa_ca.pem Outdated Show resolved Hide resolved
@alex
Copy link
Member

alex commented Oct 28, 2024

(Thanks for updating, I'm going to pause on reviewing until #11843 is merged, hopefully that'll be quick.)

first round-trip tests

feat: made asn1 structures readable

refacto: adapted existing functions accordingly

feat/pkcs12: added symmetric_decrypt

feat: deserialize 3 possible encodings

feat: handling AES-128

feat: raise error when no recipient is found

feat/pkcs7: added decanonicalize function

feat/asn1: added decode_der_data

feat/pkcs7: added smime_enveloped_decode

tests are the round-trip (encrypt & decrypt)

more tests for 100% python coverage

test support pkcs7_encrypt with openssl

added algorithm to pkcs7_encrypt signature

refacto: decrypt function is clearer

flow is more natural

refacto: added all rust error tests

refacto: added another CA chain for checking

fix: const handling

Refactor PKCS7Decryptor to pkcs7_decrypt

refacto: removed SMIME_ENVELOPED_DECODE from rust code

refacto: removed decode_der_data

adapted tests accordingly

removed the PEM tag check

added tests for smime_decnonicalize

one more test case

Update src/rust/src/pkcs7.rs

Co-authored-by: Alex Gaynor <[email protected]>

took comments into account

pem to der is now outside of decrypt

fix: removed test_support pkcs7_encrypt

added vector for aes_256_cbc encrypted pkcs7

feat: not using test_support decrypt anymore

added new vectors for PKCS7 tests

feat: using pkcs7 vectors

removed previous ones

fix: changed wrong function

feat: added certificate issuer check

test: generating the RSA chain

removed the vectors accordingly
@nitneuqr
Copy link
Contributor Author

nitneuqr commented Nov 6, 2024

#11843 is merged, I've rebased the branch onto main to take the new changes into account. It should b ready to review again!

@alex
Copy link
Member

alex commented Nov 6, 2024 via email

pub struct SignedData<'a> {
pub version: u8,
pub digest_algorithms: asn1::SetOfWriter<'a, common::AlgorithmIdentifier<'a>>,
pub digest_algorithms: common::Asn1ReadableOrWritable<
Copy link
Member

Choose a reason for hiding this comment

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

Would you be up for pulling the Asn1ReadableOrWritable pieces out into a separate PR? I think this is very very close, and I'm looking for ways to keep it moving as I review the last pieces.

Copy link
Contributor Author

@nitneuqr nitneuqr Nov 8, 2024

Choose a reason for hiding this comment

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

Done, #11922 is opened!

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.

3 participants