rustls / rustls-ffi

Use Rustls from any language
Other
128 stars 30 forks source link

CryptoProvider support #366

Closed jsha closed 3 weeks ago

jsha commented 10 months ago

Rustls' upcoming 0.22 release has support for a CryptoProvider trait that can be used to provide alternate crypto backends. We should support it in rustls-ffi. We can think of this as two stages:

Stage 1

Allow choosing between rustls' first party supported providers, ring, and aws-lc-rs. Requirements:

Stage 2

Allow implementation of third-party CryptoProviders through rustls-ffi.

The path to do this would be to expose each of the 15 traits I mentioned in https://github.com/rustls/rustls/issues/1603. The pattern to implement traits in FFI is to effectively do the same thing as trait objects in Rust: define a vtable and do dynamic dispatch.

So, there's a path to exposing all the traits of the CryptoProvider API. But I actually think we shouldn't do this. I think the FFI should go the other way. If there's an existing C library that provides crypto implementations (e.g. boringssl), someone should write Rust code that calls out to that library with FFI in order to provide the traits that CryptoProvider wants. This is how the demo boring-rustls-provider does it.

That leaves the question: say a rustls-ffi users wants to use neither ring nor aws-lc-rs, but some third thing like boring-rustls-provider. How do they do that?

I think it should work like this:

cpu commented 10 months ago

(Was this meant to be filed on rustls/rustls-ffi instead of this repo?)

jsha commented 10 months ago

(Was this meant to be filed on rustls/rustls-ffi instead of this repo?)

Yep, thanks!

cpu commented 10 months ago

@jsha I started working on the "Stage I" plan and have a prototype branch that I think gets us most of the way there: cpu-choose-your-own-crypto-adventure.

The rustls_client_config_builder_new and rustls_server_config_builder_new fns use the default provider based on the build args. In addition there's two new fns, rustls_client_config_builder_new_with_provider and rustls_server_config_builder_new_with_provider, that can be used to explicitly chose a provider.

If built with *ring* support (e.g. no CRYPTO_PROVIDER make arg or CRYPTO_PROVIDER=all) then there is a rustls_crypto_provider_ring_new function to use with the _with_provider fns. If built with aws_lc_rs support (e.g. CRYPTO_PROVIDER=aws_lc_rs or CRYPTO_PROVIDER=all) then there is a rustls_crypto_provider_aws_lc_rs_new function for choosing that provider explicitly.

I've been using nm to look at the symbols in the built client/server binaries with different build configs and it looks to be doing what I'd hope w.r.t not statically linking a provider that isn't specified by build args. Integration tests pass with both providers (but I haven't updated CI for this).

Some places that I could use feedback:

I'll keep poking at this in the meantime and update here if I get any further along. Thanks!

jsha commented 10 months ago

Is this CRYPTO_PROVIDER Makefile arg roughly what you had in mind for selecting the crate features + provider options at build time?

Yes, this is good.

rustls_connection_get_negotiated_ciphersuite uses ALL_CIPHER_SUITES, but I think we need to associate a specific crypto provider with the rustls_connection to make sure we're matching the SupportedCipherSuite correctly instead of always using ALL_CIPHER_SUITES from the default provider.

Right now the only thing you can do with a *rustls_supported_ciphersuite is get its IANA registered number, or its name. I think we could cut out the *rustls_supported_ciphersuite type entirely and offer direct accessors to get the name and/or number. That is, rustls_connection_get_negotiated_ciphersuite_name and rustls_connection_get_negotiated_ciphersuite_id.

To put another way: SupportedCipherSuite is actually about implementation rather than configuration; the only item of value to the end user is the configuration info buried deep inside (the cipher suite id).

Ditto for RUSTLS_ALL_CIPHER_SUITES and RUSTLS_DEFAULT_CIPHER_SUITES - I think we'll want fns on the rustls_crypto_provider that return this information per-provider. The existing items are being populated based on which default provider is used.

Maybe we don't need these at all? If people want the defaults, they can just rely on them being default. We should do some brief spelunking to see if there was more motivation to offer these.

rustls_certified_key_build is using the default crypto provider for any_supported_signing_key - should there be a second rustls_certified_key_build_with_provider fn for this that accepts an explciit rustls_crypto_provider?

Yeah, this seems like the best way forward, though I don't love it. It means asking users to pick a crypto provider once to build their config, and a second time to build their certified key.

cpu commented 10 months ago

Thanks for the feedback!

Right now the only thing you can do with a rustls_supported_ciphersuite is get its IANA registered number, or its name. I think we could cut out the rustls_supported_ciphersuite type entirely and offer direct accessors to get the name and/or number.

That type is also used for the rustls_{client|server}_config_builder_new_custom cipher_suites argument. Whether or not we keep the type, I think there needs to be a way to get the provider's cipher_suites's across the FFI boundary to be able to select a subset for the custom configuration (either as rustls_supported_ciphersuite entries, or u16s).

I'm imagining something like:

  #[no_mangle]
  pub extern "C" fn rustls_crypto_provider_cipher_suites(
      provider: *const rustls_crypto_provider,
      cipher_suites: *mut *const *const rustls_supported_ciphersuite,
      cipher_suites_len: *mut size_t,
  ) -> rustls_result {
     ...
  }

I tried a quick initial implementation without introducing a new Castable and I think I'm running into issues because I'm converting the frame-local Vec from the provider into a pointer passed over the FFI boundary and so break the invariant that the Vec lives longer than the as_ptr result - probably I need to make a new type and Castable with Ownership = OwnershipArc and RustType = Vec<SupportedCipherSuite> :thinking: I'll revisit tomorrow when I have more mental energy.

jsha commented 10 months ago

That type [rustls_supported_ciphersuite] is also used for the rustls{client|server}_config_builder_new_custom cipher_suites argument.

Oops, you are correct. Okay, an amendment:

When calling get_negotiated_ciphersuite, the user only cares about the configuration aspect of SupportedCipherSuite; the name and ID. They don't care about the implementation aspect, which is most of what SupportedCipherSuite carries.

So for get_negotiated_ciphersuite we could return something different; just the ID, or just the name, or and object from which you can get the ID and name. Basically rustls::CipherSuite.

However, for configuring a connection, you would still need to pass *rustls_supported_ciphersuite because that carries the implementation. And we need to pass implementation when configuring any non-default ciphersuites, because that's how we accomplish link-time removal of unused ciphersuites.

So *rustls_supported_ciphersuite as a type would stay, we just wouldn't return it from get_negotiated_ciphersuite.

cpu commented 10 months ago

Here's a checkpoint branch that's a bit further along (but still missing lots of polish).

I also tried something new and added a unit tests module in crypto.rs that uses the FFI interfaces with unsafe blocks to try and test the more interesting bits of the crypto provider without needing to figure out how to write portable C unit tests. Another option I considered was writing tests in a separate crate that doesn't take a rustls-ffi dependency, but uses the .a and rust-bindgen to test the C API from Rust in a way that can't "cheat" using rustls-ffi Rust code/helpers. I'm not sure how you feel about these ideas, maybe you've avoided this on purpose? My thinking here is that it would be nice to have something in-between pure Rust tests and continuing to hack more and more awkward C code into server.c and client.c.

Some places for improvement:

What do you think about this general direction for the API?

jsha commented 10 months ago

Generally looks right. rustls_certified_key_build_with_provider isn't a match for the Rust API of CertifiedKey. The C function takes a whole crypto provider, when CertifiedKey::new just takes an Arc<dyn SigningKey>. To match the Rust API I think we need to expose a type for Arc<dyn SigningKey> and a function that parses PEM. That function should collapse some of the complications of rustls-pemfile being a different crate, and the intermediate enums in rustls-pki-types.

I also tried something new and added a unit tests module in crypto.rs that uses the FFI interfaces with unsafe blocks to try and test the more interesting bits of the crypto provider without needing to figure out how to write portable C unit tests. Another option I considered was writing tests in a separate crate that doesn't take a rustls-ffi dependency, but uses the .a and rust-bindgen to test the C API from Rust in a way that can't "cheat" using rustls-ffi Rust code/helpers. I'm not sure how you feel about these ideas, maybe you've avoided this on purpose?

Yep, this is excellent and the way forward. We discussed this a bit here: https://github.com/rustls/rustls-ffi/issues/209. One of the benefits of Rust tests is that we get the benefit of Miri, at least in code paths that don't hit FFI (which Miri doesn't like). The reason you don't see a lot of these is just that we don't have a lot of unittest coverage. My initial testing priority was to get a sort of working example and see how the API felt in C. That's of course a downside of writing unittests in Rust; since we're not exercising the code in C we won't see right away if we've made an awkward API. But I think that's an acceptable tradeoff. I haven't found any unittesting frameworks in C that looked great.

cpu commented 10 months ago

To match the Rust API I think we need to expose a type for Arc and a function that parses PEM. That function should collapse some of the complications of rustls-pemfile being a different crate, and the intermediate enums in rustls-pki-types.

That makes sense. I'll take a look at fixing that up.

Yep, this is excellent and the way forward

Awesome. What you're saying about the trade-offs makes sense to me too.

cpu commented 2 months ago

I was able to find time to come back around at this. PTAL: https://github.com/rustls/rustls-ffi/pull/441