-
Notifications
You must be signed in to change notification settings - Fork 717
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
feat(bindings): enable application owned certs #4937
Conversation
* rustfmt * clippy
* better variable naming * remove useless pub on sibling struct * correct typo in doc comment
To address the memory leak, we manually track ownership using a boolean toggle. We only _actually_ free the memory when the cert is "owned".
bindings/rust/s2n-tls/src/config.rs
Outdated
.application_owned_certs | ||
.extend(chains.clone()); | ||
|
||
let raw_certs: Vec<*mut s2n_cert_chain_and_key> = chains |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bummer that we have to allocate here... Is there not a way to append defaults?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately there isn't a way to append defaults. Each call to set_defaults
removes the previous defaults, and there is no separate "append" API.
But we can avoid the allocation as long as we are okay with duplicating a constant/some safety checks. This lets us just use an array on the stack instead of a vector. I'll go ahead and implement that in the next revision.
bindings/rust/s2n-tls/src/config.rs
Outdated
/// Corresponds to [s2n_config_set_cert_chain_and_key_defaults]. | ||
pub fn set_default_chains( | ||
&mut self, | ||
chains: Vec<CertificateChain<'static>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be a Vec? Wonder if it would be more flexible as an IntoIterator<Item = CertificateChain>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It works, but does make things a tiny bit messier. Will make the change in the next revision.
pub struct CertificateChain<'a> { | ||
ptr: NonNull<s2n_cert_chain_and_key>, | ||
is_owned: bool, | ||
ptr: Arc<CertificateChainHandle>, | ||
_lifetime: PhantomData<&'a s2n_cert_chain_and_key>, | ||
} | ||
|
||
impl CertificateChain<'_> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the plan on adding mutating APIs to CertificateChain
? i.e s2n_cert_chain_and_key_set_ocsp_data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the callout! I switched to using a builder pattern. Initial mutation is done on a single thread, with the rust type system enforcing a single owner.
The returned CertificateChain is immutable, and therefore can't be modified by any threads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, although with fully immutable chain and config updating OCSP staple would require rebuilding the whole config, which is not ideal, but I don't see a better option without introducing thread-safety to s2n-tls methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to maintain the flexibility to modify things whenever, we could just make the application to handle thread safety.
Instead of making CertificateChain
internally reference counted, it could be externally reference counted, Arc<CertificateChain>
. So if customer wanted to mutate it from multiple threads they would need to use an Arc<Mutex<CertificateChain>>
or something along those lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the problem is that we'd have to give non-mutex version to s2n_config internally, at which point you can't safely mutate it again even if there is mutex on rust representation. The only way this will work is if C implementation has locks and is thread-safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya, I was thinking about this further and I don't think there is any safe way to let you mutate an OCSP on an already created certificate. Because there isn't any way for us to guarantee that e.g. s2n-tls isn't currently reading that OCSP data.
* use IntoIterator trait bound for set defaults * avoid heap allocation in set defaults * avoid useless increment/decrement of ref count
/// | ||
/// This can be used with [crate::config::Builder::load_chain] to share a | ||
/// single cert across multiple configs. | ||
pub fn load_pem(&mut self, chain: &[u8], key: &[u8]) -> Result<&mut Self, Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm following our convention of builders returning &mut Self
, but personally I like builders returning Self
by value. Thoughts on whether it would be worth breaking from convention?
Also, I switched the API naming back to load_
because that felt more appropriate for a builder 🤷
// SAFETY: manual audit of load_public_pem_bytes shows that `chain_pem` | ||
// is not modified | ||
// https://github.com/aws/s2n-tls/issues/4140 | ||
s2n_cert_chain_and_key_load_public_pem_bytes( | ||
self.cert.as_mut_ptr(), | ||
chain.as_ptr() as *mut _, | ||
chain.len() as u32, | ||
) | ||
.into_result() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if I call load_public, then load_public_pem? Should you keep your "from" methods, but they return builders instead of direct chains?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same thing that happens if you call config::Builder::load_pem
then config::Builder::load_public_pem
. 😅
I think the consistency is valuable, and this method preserves a bit more future flexibility (although I can't think of a scenario where you'd have a single certificate chain with both ...)
If we just want strong validation, what do you think about adding validation/errors on the builder? We could return an explicit error such as
CertificateChain can only load one certificate chain. You already called {method}, so it is invalid to call {method}`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consistency is a good argument :( You can't by definition have a chain that both has a private key and doesn't have a private key tho.
Not blocking, but it might actually be nice as a follow-up to add that validation error. It could also catch the case where we don't add any pem at all to the chain.
* add intermediate to avoid "undo" of cert chain refs * add comment for as_mut_ptr
bindings/rust/s2n-tls/src/config.rs
Outdated
const CHAINS_MAX_COUNT: usize = 5; | ||
|
||
let mut chain_arrays: [Option<CertificateChain<'static>>; CHAINS_MAX_COUNT] = | ||
[None, None, None, None, None]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is admittedly not pretty, but there aren't any good solutions because Option<CertificateChain<'static>>
is not Copy, and we don't want to clone placeholder certs.
https://www.joshmcguigan.com/blog/array-initialization-rust/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say still better than removing chains from the list on failure, so still a win?
* fix typos in comment * adjust limit to match the C code
Merging as this doesn't interact with the failing CRT test at all |
Release Summary:
Customers can now use application owned certs from the rust bindings. This allows rust consumers of s2n-tls to load certificates for many domains on a single config, and also allows certificates to be shared across a config.
Description of changes:
This PR builds on work in #4902
There are two main design point
CertificateChain reference counting: The CertificateChain struct is now internally reference counted. This allows applications to cheaply clone it and share it across configs.
Config->Context holds references: Instead of trying to retrieve reference counts from their storage on the C side, we store a handle to each reference counted certificate in the config->context. Then when the config and context are dropped, the container is as well which ends up decrementing all of the appropriate reference counts.
Call-outs:
API Naming
This PR adds three new public APIs
config::Builder::load_chain
s2n_config_add_cert_chain_and_key_to_store
config::Builder::set_default_chains
s2n_config_set_cert_chain_and_key_defaults
CertificateChain::from_pem
s2n_cert_chain_and_key_load_pem_bytes
CertificateChain::from_public_pem
s2n_cert_chain_and_key_load_public_pem_bytes
The CertificateChain apis were named to mirror the similar config apis -
load_pem
andload_public_pem
.Then the config APIs are hopefully noticeably different from the "direct pem" apis.
load_chain
vsload_pem
.set_default_chains
is a bit of an ugly name, but didn't happen upon anything betterCertificateChain builder?
There is a notable departure from the C api with
CertificateChain::load_pem
.In the C API, applications must first create the certificate chain and then call
load_pem
with it. That felt rather silly to do in rust. The choices wereConsolidation of Cert Loading
Note that we could switch
config::Builder::load_pem
to use "application" owned certs, by just creating the appropriate CertificateChain for the application and storing it on the config. This would consolidate out cert loading, and prevent the edge case where a customer tried to use bothload_pem
andadd_to_store
and receives an error.We don't need to make this decision now, which is why I didn't do this in the PR.
Testing:
Unit tests were added covering reference counting behavior, and sanity checking some end to end workflows with default/non-default certs.
I am also working on porting the
test_sni_matches
integration test to rust so that we could benefit from the integration test coverage.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.