feat(grpc): TLS channel credentials#2508
Conversation
|
|
||
| // Test-only constructor that enables injecting a custom crypto provider. | ||
| #[cfg(test)] | ||
| pub fn new_for_test( |
There was a problem hiding this comment.
nit: test only code is usually considered an anti pattern(despite being widely used) , Can we manipulate the default provider in the unit test body instead of chaning the construction path?
There was a problem hiding this comment.
This constructor was simply calling Self::new_impl. Since new_impl is a private method, it is not accessible outside this module.
Can we manipulate the default provider in the unit test body...
This would either require a test-only setter or make the tests rely on the internal structure of RustlsClientTlsCredentials, making them brittle.
The common way to mock dependencies is to have them injected into the constructor. In this case, however, the crypto provider is fetched from a process-level singleton, and we don't expect users to fetch and pass it to the constructor themselves. To solve this, I introduced an internal constructor that supports dependency injection. The public constructor is now a thin wrapper that delegates to the internal one. This pattern ensures maximum test coverage without compromising the public API.
|
|
||
| let (_, connection) = tls_stream.get_ref(); | ||
| if let Some(negotiated) = connection.alpn_protocol() { | ||
| if negotiated != b"h2" { |
There was a problem hiding this comment.
Maybe we should avoid magic strings at multiple places and use const strings?
There was a problem hiding this comment.
Introduced a constant.
There was a problem hiding this comment.
Quick question: Why do we need a separate tests file instead of keeping it in the module itself? Is this a standard practice?
There was a problem hiding this comment.
The tests must be in a separate module so they stay out of the production binary. Usually, we keep the test module in the same file, but the test code was getting so long that it made scrolling through the actual logic difficult. I decided to move the tests to their own file to keep things organized, which is also a common practice.
| } | ||
|
|
||
| impl KeyLogFileInner { | ||
| fn new(path: &PathBuf) -> Self { |
There was a problem hiding this comment.
I don't think new is appropriate here.
https://rust-lang.github.io/api-guidelines/predictability.html#constructors-are-static-inherent-methods-c-ctor mentions that IO like constructors should use domain spacific names . Aditionally, file::open retuns a Result, maybe we should consider doing the same.
Additionally, since I couldn't find any style guide around fallibility of constructors in rust . I'll be a heretic and point towards cpp style which requires constructors to be "small and infallible".
Another note, should we be supporting AsyncIo here , in case we need to support remote fs etc? I am guessing no, because the trait we want to implement is sync?
There was a problem hiding this comment.
This file is copied from the rustls repository which uses the same constructor: https://github.com/rustls/rustls/blob/b07b8419b786f05a95d08868643193465449a136/rustls-util/src/key_log_file.rs#L20
I would prefer not to make too many modifications to this file. Please let me know if you think we need to apply readability fixes here.
| return Ok(()); | ||
| }; | ||
|
|
||
| self.buf.truncate(0); |
There was a problem hiding this comment.
Is there a utility for it? This seems like a bunch of magic strings, how do we know this is correct?
There was a problem hiding this comment.
This file is copied from the rustls repository, source here: https://github.com/rustls/rustls/blob/main/rustls-util/src/key_log_file.rs
The original implementation doesn't export the constructor to set an arbitrary file path, and relies on the SSLKEYLOGFILE env variable which is commonly used by openssl. Since gRPC C++ supports specifying the file path, I copied over the file and exposed the internal constructor.
| /// [`KeyLog`] implementation that opens a file whose name is | ||
| /// given to the constructor, and writes keys into it. | ||
| /// | ||
| /// If such a file cannot be opened, or cannot be written then |
There was a problem hiding this comment.
Is silently failing the correct option? What are the implications of silently failing?
There was a problem hiding this comment.
Key logging is meant for debugging. This feature must not be used in production. This file is copied from the rsutls repo.
| } | ||
| } | ||
|
|
||
| impl KeyLog for KeyLogFile { |
There was a problem hiding this comment.
I am guessing rustls depends on implementing this trait which happens to be infallible and hence requires us to also silently fail?
There was a problem hiding this comment.
Yes, this file is copied from the rustls repo.
| /// | ||
| /// The provided PEM should include at least one PEM encoded certificate. | ||
| pub fn from_pem(pem: impl AsRef<[u8]>) -> Self { | ||
| let pem = pem.as_ref().into(); |
There was a problem hiding this comment.
Is this always a copy? Same for below.
There was a problem hiding this comment.
This results in a single copy, during the creation of a gRPC channel. I don't think the overhead is significant. This is a similar API to the one in tonic:
tonic/tonic/src/transport/tls.rs
Lines 15 to 21 in e6318b7
| } | ||
|
|
||
| /// Get a immutable reference to underlying certificate | ||
| fn get_ref(&self) -> &[u8] { |
There was a problem hiding this comment.
These are private, right? Why do we need all of them
There was a problem hiding this comment.
Only get_ref is required. The other methods are an artifact from copying the tonic implementation, removed them now.
| } | ||
| } | ||
|
|
||
| impl AsRef<[u8]> for RootCertificates { |
There was a problem hiding this comment.
I am assuming we'll also need AsMut for consistency?
There was a problem hiding this comment.
Removed this for now. Using AsRead exposes the method publicly, while using the get_ref() method in the impl block above avoids this. We can add this in the future, when required.
| } | ||
| } | ||
|
|
||
| mod provider { |
There was a problem hiding this comment.
nit: I think the provider stuff should probably be moved into its own file maybe?
There was a problem hiding this comment.
This section is roughly 50 lines, consisting mostly of documentation comments. Even with this addition, the mod.rs file remains a manageable size. I’d prefer not to split the file prematurely, but I’m happy to do so if you feel strongly about it.
| security_protocol: "tls", | ||
| }; | ||
|
|
||
| fn sanitize_crypto_provider(mut crypto_provider: CryptoProvider) -> Result<CryptoProvider, String> { |
There was a problem hiding this comment.
The placement of the utility functions seem slightly odd . They are used in another file but are implemented as private here. Wouldn't it be better to move them closer to their point of use unless we want to share them ?
There was a problem hiding this comment.
This will get used in the TLS server credentials implementation.
| use crate::rt::{endpoint, GrpcEndpoint}; | ||
|
|
||
| pub struct TlsStream<T> { | ||
| pub(crate) inner: RustlsStream<T>, |
There was a problem hiding this comment.
Wouldn't it be better to make it private and provide and inner and/or inner_ref method instead?
There was a problem hiding this comment.
Changed. Added a a constructor and an inner method.
| /// This should be used **only for debugging purposes**. It should never be | ||
| /// used in a production environment due to security concerns. |
There was a problem hiding this comment.
Should we give it a scarier name, then? insecure_with_key_log_path e.g.?
There was a problem hiding this comment.
Renamed to include an insecure_ prefix. Some rustls APIs use the word "dangerous" for this purpose.
There was a problem hiding this comment.
Should we put this in a rustls directory instead of tls since we know we'll eventually have a second TLS creds implementation?
There was a problem hiding this comment.
Renamed the directory and module to rustls. I suspect the configuration objects could be reused for the future BoringSSL implementation, so having them in a separate tls module might make sense down the line. However, I've kept everything in the rustls module for now. We can always move the symbols to a new module and use aliases in the rustls module later.
There was a problem hiding this comment.
Yeah, I think we'll want some things in tls that are common to the boring implementation, but we can wait until later.
| return Err( | ||
| "No crypto provider installed. Enable `tls-aws-lc` feature or install one manually." | ||
| .to_string(), | ||
| ); |
There was a problem hiding this comment.
Formatting looks wrong here?
| let provider = if let Some(p) = CryptoProvider::get_default() { | ||
| p.as_ref().clone() | ||
| } else { | ||
| return Err( | ||
| "No crypto provider installed. Enable `tls-aws-lc` feature or install one manually." | ||
| .to_string(), | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Does this work instead?
| let provider = if let Some(p) = CryptoProvider::get_default() { | |
| p.as_ref().clone() | |
| } else { | |
| return Err( | |
| "No crypto provider installed. Enable `tls-aws-lc` feature or install one manually." | |
| .to_string(), | |
| ); | |
| }; | |
| let Some(provider) = CryptoProvider::get_default().cloned() else { | |
| return Err( | |
| "No crypto provider installed. Enable `tls-aws-lc` feature or install one manually." | |
| .to_string(), | |
| ); | |
| }; |
There was a problem hiding this comment.
Applied the change manually. I was relying on rustfmt to auto-format it, but it doesn't seem to make any changes with the default configuration.
| .await | ||
| .map_err(|e| e.to_string())?; | ||
|
|
||
| let (_, connection) = tls_stream.get_ref(); |
There was a problem hiding this comment.
I would like us to try to name the _ variables in situations like these so that we know what it is that we're ignoring, unless it's super obvious. _io? (What's the meaning of this API? There's two different ways to access the underlying stream?)
There was a problem hiding this comment.
Renamed the ignored symbol to _io prefix.
What's the meaning of this API? There's two different ways to access the underlying stream?
The returned IO represents the plaintext IO stream that was passed to the connector.connect() method. The ClientConnection struct represents the TLS state machine. The TlsStream glues both these parts. It keeps reading data from the IO and passing it into to the ClientConnection.
| assert!(creds.is_err()); | ||
| assert_eq!( | ||
| creds.err().unwrap(), | ||
| "Crypto provider has no cipher suites matching the security policy (TLS1.3 or TLS1.2+ECDHE)" |
There was a problem hiding this comment.
This might be brittle? Maybe contains("no cipher suites matching") or something would be less so?
There was a problem hiding this comment.
Relaxed the assertion.
| let key_log_path = key_log_dir.join("grpc_rust_key_log.txt"); | ||
|
|
||
| // Ensure file doesn't exist | ||
| if key_log_path.exists() { | ||
| std::fs::remove_file(&key_log_path).unwrap(); | ||
| } |
There was a problem hiding this comment.
This pattern is problematic -- if you are running tests in different branches concurrently, e.g. We need something like mktemp where we don't have to see if the directory/file is already in use.
There was a problem hiding this comment.
Nice catch! The rustls tests are running the keylog tests serially and using the CWD to create a temp file: permalink
The recommended way to create temporary directories seems to be using the tempfile crate, which tonic also seems to use:
tonic/tests/default_stubs/Cargo.toml
Lines 13 to 14 in fe0393c
I've added a dev dependency and switched to using the tempfile crate.
| // Note: This file has been modified by gRPC authors on 11th Feb 2026. | ||
| // - Add ability to write logs to arbitrary path. |
There was a problem hiding this comment.
Is it a requirement to list all modifications here? Because I'd rather not since I'm sure we'll make a change on some future date and forget to update it. I would prefer something like "Note: this file contains modifications by the gRPC authors; see revision history for details" if that is acceptable.
There was a problem hiding this comment.
Based on section 4.b of the liscence, the revision details are not required: https://www.apache.org/licenses/LICENSE-2.0
You must cause any modified files to carry prominent notices stating that You changed the files
I've updated the note as suggested.
| @@ -0,0 +1,117 @@ | |||
| // Copyright (c) 2016 Joseph Birr-Pixton <jpixton@gmail.com> | |||
| // | |||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||
There was a problem hiding this comment.
I think we will want to put any third-party copyrighted files into a separate "vendored" directory, per guidance from legal. We can talk about the details later; this is probably OK for now, pre-1.0.
There was a problem hiding this comment.
Tracking down all the files may be a problem later. I've moved this file to the src/vendored directory now.
There was a problem hiding this comment.
After reading the CNCF guidelines for attribution, my understanding is that the keylog file qualifies as Incorporated Code instead of Vendored component since it's modified.
- Incorporated code: portions of code that are taken from a third-party software component and incorporated directly into a CNCF project’s own source code
- Vendored component: a component whose code is incorporated, unmodified, into a folder specifically intended for third-party dependencies, often with a name such as vendor/, third_party/, node_modules/ (for NPM dependencies), etc.
The guidelines for Incorporated Code only mention copying the license into source file for sources with an Apache 2 license.
Based on this, I've moved the keylog file back to the rustls directory.
| /// | ||
| /// This allows the consumer to observe the current value and await | ||
| /// future updates. | ||
| fn get_receiver(self) -> Receiver<T>; |
There was a problem hiding this comment.
With names so generic (Sender, Receiver) and reused so frequently, maybe we want to say they should always be prefixed by the module name? So, use tokio::sync::watch and watch::Receiver?
There was a problem hiding this comment.
Changed to watch::Receiver.
61cbb74 to
2a1771e
Compare
This change adds a
rustls-based implementation ofChannelCredentials.Notes: