Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions tonic-xds/src/client/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ use crate::client::route::{Router, XdsRoutingLayer};
use crate::xds::bootstrap::{BootstrapConfig, BootstrapError};
use crate::xds::cache::XdsCache;
#[cfg(feature = "_tls-any")]
use crate::xds::cert_provider::{CertProviderError, CertProviderRegistry};
use crate::xds::cert_provider::{CertProviderError, CertProviderRegistry, CertificateProvider};
use crate::xds::cluster_discovery::XdsClusterDiscovery;
use crate::xds::resource_manager::XdsResourceManager;
use crate::xds::routing::XdsRouter;
use crate::{TonicCallCredentials, XdsUri};
use http::Request;
#[cfg(feature = "_tls-any")]
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use std::task::{Context, Poll};
Expand Down Expand Up @@ -168,20 +170,26 @@ const _: fn() = || {
pub struct XdsChannelBuilder {
config: Arc<XdsChannelConfig>,
recorder: Option<Arc<dyn MetricsRecorder>>,
#[cfg(feature = "_tls-any")]
cert_providers: HashMap<String, Arc<dyn CertificateProvider>>,
}

impl Debug for XdsChannelBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("XdsChannelBuilder")
.field("config", &self.config)
.field(
"recorder",
&self
.recorder
.as_deref()
.map_or("None", |r| std::any::type_name_of_val(r)),
)
.finish()
let mut s = f.debug_struct("XdsChannelBuilder");
s.field("config", &self.config).field(
"recorder",
&self
.recorder
.as_deref()
.map_or("None", |r| std::any::type_name_of_val(r)),
);
#[cfg(feature = "_tls-any")]
s.field(
"cert_providers",
&self.cert_providers.keys().collect::<Vec<_>>(),
);
s.finish()
}
}

Expand All @@ -192,6 +200,8 @@ impl XdsChannelBuilder {
Self {
config: Arc::new(config),
recorder: None,
#[cfg(feature = "_tls-any")]
cert_providers: HashMap::new(),
}
}

Expand All @@ -207,6 +217,22 @@ impl XdsChannelBuilder {
self
}

/// Registers a custom [`CertificateProvider`] under an xDS certificate
/// provider instance name, resolved by CDS `UpstreamTlsContext` references.
/// Shadows a bootstrap `file_watcher` instance of the same name.
///
/// [`CertificateProvider`]: crate::CertificateProvider
#[cfg(feature = "_tls-any")]
#[must_use]
pub fn with_certificate_provider(
mut self,
instance_name: impl Into<String>,
provider: Arc<dyn CertificateProvider>,
) -> Self {
self.cert_providers.insert(instance_name.into(), provider);
self
}

/// Emits the gRFC A78 xDS client metrics through an OpenTelemetry `Meter`.
///
/// Convenience wrapper over
Expand Down Expand Up @@ -254,6 +280,7 @@ impl XdsChannelBuilder {
#[cfg(feature = "_tls-any")]
let cert_provider_registry = Arc::new(CertProviderRegistry::from_bootstrap(
&bootstrap.certificate_providers,
self.cert_providers.clone(),
)?);

let node = Node::try_from(bootstrap.node)?;
Expand Down Expand Up @@ -688,8 +715,10 @@ mod tests {
dyn ClusterDiscovery<EndpointAddress, EndpointChannel<Channel>>,
> = {
use crate::xds::cert_provider::CertProviderRegistry;
let registry =
Arc::new(CertProviderRegistry::from_bootstrap(&Default::default()).unwrap());
let registry = Arc::new(
CertProviderRegistry::from_bootstrap(&Default::default(), Default::default())
.unwrap(),
);
Arc::new(XdsClusterDiscovery::new(cache, registry))
};
#[cfg(not(feature = "_tls-any"))]
Expand Down Expand Up @@ -821,8 +850,10 @@ mod tests {
#[cfg(feature = "_tls-any")]
let _channel = {
use crate::xds::cert_provider::CertProviderRegistry;
let registry =
Arc::new(CertProviderRegistry::from_bootstrap(&Default::default()).unwrap());
let registry = Arc::new(
CertProviderRegistry::from_bootstrap(&Default::default(), Default::default())
.unwrap(),
);
builder.build_from_cache(cache, registry, xds_client, resource_manager)
};
#[cfg(not(feature = "_tls-any"))]
Expand Down
3 changes: 3 additions & 0 deletions tonic-xds/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ pub use xds::bootstrap::{BootstrapConfig, BootstrapError};
pub use xds::uri::{XdsUri, XdsUriError};
pub use xds_client::TonicCallCredentials;

#[cfg(feature = "_tls-any")]
pub use xds::cert_provider::{CertProviderError, CertificateData, CertificateProvider, Identity};

pub use xds_client::{Instrument, InstrumentKind, KeyValue, MetricsRecorder, StringValue, Value};

#[cfg(any(test, feature = "testutil"))]
Expand Down
67 changes: 21 additions & 46 deletions tonic-xds/src/xds/cert_provider/file_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ use std::sync::Arc;
use std::time::Duration;

use arc_swap::ArcSwap;
use rustls::RootCertStore;
use rustls::pki_types::CertificateDer;
use serde::Deserialize;

use crate::common::async_util::AbortOnDrop;
Expand Down Expand Up @@ -135,28 +133,23 @@ impl CertificateProvider for FileWatcherProvider {

/// Read certificate data from the files specified in the config.
///
/// CA roots are parsed into [`Arc<RootCertStore>`] in this function — once per
/// refresh — so the verifier can use them directly on every TLS handshake
/// without re-parsing. Identity bytes are kept as PEM because
/// [`tonic::transport::Identity::from_pem`] is bytes-only on the consumer side.
///
/// This function is the single validation boundary between the permissive
/// JSON-parsed [`FileWatcherConfig`] and the invariant-enforcing
/// CA roots and identity material are read as raw PEM bytes; parsing is left to
/// the consumer. This function is the single validation boundary between the
/// permissive JSON-parsed [`FileWatcherConfig`] and the invariant-enforcing
/// [`CertificateData`]. It checks both A65 rules:
/// - cert/key pairing (first match)
/// - at least one of identity/roots is set (second match)
fn read_certificate_data(config: &FileWatcherConfig) -> Result<CertificateData, CertProviderError> {
let roots = config
.ca_certificate_file
.as_deref()
.map(read_and_parse_roots)
.map(read_file)
.transpose()?;

let identity = match (&config.certificate_file, &config.private_key_file) {
(Some(cert_path), Some(key_path)) => Some(Identity {
cert_chain: read_file(cert_path)?,
key: read_file(key_path)?,
}),
(Some(cert_path), Some(key_path)) => {
Some(Identity::new(read_file(cert_path)?, read_file(key_path)?))
}
(None, None) => None,
(Some(_), None) | (None, Some(_)) => return Err(CertProviderError::UnpairedCertKey),
};
Expand All @@ -176,26 +169,6 @@ fn read_file(path: &Path) -> Result<Vec<u8>, CertProviderError> {
})
}

fn read_and_parse_roots(path: &Path) -> Result<Arc<RootCertStore>, CertProviderError> {
let pem = read_file(path)?;
let mut reader = std::io::Cursor::new(&pem);
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
.collect::<Result<_, _>>()
.map_err(|e| CertProviderError::PemParse {
path: path.display().to_string(),
reason: e.to_string(),
})?;
let mut store = RootCertStore::empty();
let (added, _) = store.add_parsable_certificates(certs);
if added == 0 {
return Err(CertProviderError::PemParse {
path: path.display().to_string(),
reason: "no usable certificates in PEM".into(),
});
}
Ok(Arc::new(store))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -229,15 +202,15 @@ mod tests {

#[tokio::test]
async fn reads_ca_certificate() {
let ca_file = write_temp_file(&gen_ca_pem());
let ca_pem = gen_ca_pem();
let ca_file = write_temp_file(&ca_pem);

let provider =
FileWatcherProvider::new(make_config(ca_file.path().to_str(), None, None)).unwrap();
let data = provider.fetch().unwrap();

assert!(matches!(*data, CertificateData::RootsOnly { .. }));
let roots = data.roots().unwrap();
assert_eq!(roots.len(), 1);
assert_eq!(data.roots().unwrap(), ca_pem.as_slice());
assert!(data.identity().is_none());
}

Expand All @@ -256,14 +229,15 @@ mod tests {

assert!(matches!(*data, CertificateData::IdentityOnly { .. }));
let identity = data.identity().unwrap();
assert_eq!(identity.cert_chain.as_slice(), b"cert-chain-pem");
assert_eq!(identity.key.as_slice(), b"private-key-pem");
assert_eq!(identity.cert_chain(), b"cert-chain-pem");
assert_eq!(identity.key(), b"private-key-pem");
assert!(data.roots().is_none());
}

#[tokio::test]
async fn reads_all_files() {
let ca_file = write_temp_file(&gen_ca_pem());
let ca_pem = gen_ca_pem();
let ca_file = write_temp_file(&ca_pem);
let cert_file = write_temp_file(b"cert-pem");
let key_file = write_temp_file(b"key-pem");

Expand All @@ -276,10 +250,10 @@ mod tests {
let data = provider.fetch().unwrap();

assert!(matches!(*data, CertificateData::Both { .. }));
assert_eq!(data.roots().unwrap().len(), 1);
assert_eq!(data.roots().unwrap(), ca_pem.as_slice());
let identity = data.identity().unwrap();
assert_eq!(identity.cert_chain.as_slice(), b"cert-pem");
assert_eq!(identity.key.as_slice(), b"key-pem");
assert_eq!(identity.cert_chain(), b"cert-pem");
assert_eq!(identity.key(), b"key-pem");
}

#[test]
Expand Down Expand Up @@ -417,7 +391,8 @@ mod tests {
use crate::xds::cert_provider::CertProviderRegistry;
use std::collections::HashMap;

let ca_file = write_temp_file(&gen_ca_pem());
let ca_pem = gen_ca_pem();
let ca_file = write_temp_file(&ca_pem);

let mut configs = HashMap::new();
configs.insert(
Expand All @@ -430,12 +405,12 @@ mod tests {
},
);

let registry = CertProviderRegistry::from_bootstrap(&configs).unwrap();
let registry = CertProviderRegistry::from_bootstrap(&configs, HashMap::new()).unwrap();
assert!(registry.get("my_certs").is_some());
assert!(registry.get("other").is_none());

let provider = registry.get("my_certs").unwrap();
let data = provider.fetch().unwrap();
assert_eq!(data.roots().unwrap().len(), 1);
assert_eq!(data.roots().unwrap(), ca_pem.as_slice());
}
}
Loading
Loading