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
2 changes: 1 addition & 1 deletion spiffe-rustls-grpc-examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ rust-version = "1.83"
publish = false

[dependencies]
spiffe = "0.7.4"
spiffe = "0.8.0"
spiffe-rustls = { path = "../spiffe-rustls", features = ["ring"] }

# gRPC stack
Expand Down
4 changes: 1 addition & 3 deletions spiffe-rustls-grpc-examples/src/bin/grpc_client_mtls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
};

// Build rustls client config backed by SPIFFE X509Source.
let mut client_cfg = ClientConfigBuilder::new(source.clone(), opts)
.build()
.await?;
let mut client_cfg = ClientConfigBuilder::new(source.clone(), opts).build()?;

// gRPC requires HTTP/2 via ALPN.
client_cfg.alpn_protocols = vec![b"h2".to_vec()];
Expand Down
3 changes: 1 addition & 2 deletions spiffe-rustls-grpc-examples/src/bin/grpc_server_mtls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
source.clone(),
ServerConfigOptions::allow_any("example.org".try_into()?),
)
.build()
.await?;
.build()?;

// gRPC requires HTTP/2 via ALPN.
server_cfg.alpn_protocols = vec![b"h2".to_vec()];
Expand Down
27 changes: 14 additions & 13 deletions spiffe-rustls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,8 @@ readme = "README.md"
keywords = ["spiffe", "spire", "rustls", "mtls", "tls"]
categories = ["network-programming", "cryptography", "authentication"]

[features]
default = ["ring"]

# Crypto backend selection (rustls feature passthrough)
ring = ["rustls/ring"]
aws-lc-rs = ["rustls/aws_lc_rs"]

tcp-examples = ["dep:tokio-rustls"]

# Internal: enables deps used by integration tests
integration-tests = ["dep:tokio-rustls"]

[dependencies]
spiffe = "0.7.4"
spiffe = "0.8.0"
rustls = { version = "0.23", default-features = false, features = ["std"] }
tokio = { version = "1", default-features = false, features = ["rt", "sync"] }
tokio-util = "0.7"
Expand All @@ -48,6 +36,19 @@ tokio = { version = "1", default-features = false, features = [
"sync",
] }

[features]
default = ["ring"]

# Crypto backend selection (rustls feature passthrough)
ring = ["rustls/ring"]
aws-lc-rs = ["rustls/aws_lc_rs"]

tcp-examples = ["dep:tokio-rustls"]

# Internal: enables deps used by integration tests
integration-tests = ["dep:tokio-rustls"]


[[example]]
name = "mtls_tcp_server"
path = "examples/mtls_tcp_server.rs"
Expand Down
6 changes: 2 additions & 4 deletions spiffe-rustls/examples/mtls_tcp_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ async fn main() -> anyhow::Result<()> {
}),
};

let client_cfg = ClientConfigBuilder::new(source.clone(), opts)
.build()
.await?;
let client_cfg = ClientConfigBuilder::new(source.clone(), opts).build()?;
let connector = TlsConnector::from(Arc::new(client_cfg));

let tcp = TcpStream::connect("127.0.0.1:8443").await?;
Expand All @@ -42,6 +40,6 @@ async fn main() -> anyhow::Result<()> {

let _ = tls.shutdown().await;

source.shutdown().await?;
source.shutdown().await;
Ok(())
}
6 changes: 2 additions & 4 deletions spiffe-rustls/examples/mtls_tcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ async fn main() -> anyhow::Result<()> {

let opts = ServerConfigOptions::allow_any("example.org".try_into()?);

let server_cfg = ServerConfigBuilder::new(source.clone(), opts)
.build()
.await?;
let server_cfg = ServerConfigBuilder::new(source.clone(), opts).build()?;
let acceptor = TlsAcceptor::from(Arc::new(server_cfg));

let addr = "127.0.0.1:8443";
Expand Down Expand Up @@ -66,6 +64,6 @@ async fn main() -> anyhow::Result<()> {
}
}

source.shutdown().await?;
source.shutdown().await;
Ok(())
}
39 changes: 34 additions & 5 deletions spiffe-rustls/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ pub struct ClientConfigOptions {
pub authorize_server: AuthorizeSpiffeId,
}

impl std::fmt::Debug for ClientConfigOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientConfigOptions")
.field("trust_domain", &self.trust_domain)
.field("authorize_server", &"<authorize_fn>")
.finish()
}
}

impl ClientConfigOptions {
/// Creates options that authenticate the server but allow any SPIFFE ID.
///
Expand Down Expand Up @@ -55,6 +64,7 @@ impl ClientConfigOptions {
///
/// Use [`ClientConfigOptions::allow_any`] to disable authorization while
/// retaining full TLS authentication.
#[derive(Debug)]
pub struct ClientConfigBuilder {
source: Arc<X509Source>,
opts: ClientConfigOptions,
Expand All @@ -67,20 +77,39 @@ impl ClientConfigBuilder {
}

/// Builds the `rustls::ClientConfig`.
pub async fn build(self) -> Result<ClientConfig> {
///
/// The returned configuration:
///
/// * presents the current SPIFFE X.509 SVID as the client certificate
/// * validates the server certificate chain against the configured trust domain
/// * authorizes the server by SPIFFE ID (URI SAN)
///
/// The configuration is backed by a live [`X509Source`]. When the underlying
/// SVID or trust bundle is rotated by the SPIRE agent, **new TLS handshakes
/// automatically use the updated material**.
///
/// # Errors
///
/// Returns an error if:
///
/// * the Rustls crypto provider is not installed
/// * no current X.509 SVID is available from the `X509Source`
/// * the trust bundle for the configured trust domain is missing
/// * building the underlying Rustls certificate verifier fails
pub fn build(self) -> Result<ClientConfig> {
crate::crypto::ensure_crypto_provider_installed();

let watcher = MaterialWatcher::new(self.source, self.opts.trust_domain).await?;
let watcher = MaterialWatcher::new(self.source, self.opts.trust_domain)?;

let resolver: Arc<dyn ResolvesClientCert> =
Arc::new(resolve_client::SpiffeClientCertResolver {
watcher: watcher.clone(),
});

let verifier = Arc::new(SpiffeServerCertVerifier::new(
Arc::new(watcher.clone()),
let verifier = Arc::new(SpiffeServerCertVerifier::from_watcher(
watcher.clone(),
self.opts.authorize_server,
)?);
));

let cfg = ClientConfig::builder()
.dangerous()
Expand Down
12 changes: 9 additions & 3 deletions spiffe-rustls/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#![deny(missing_docs)]
#![deny(unsafe_code)]
#![warn(missing_debug_implementations)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::must_use_candidate)]

//! # spiffe-rustls
//!
//! `spiffe-rustls` integrates [`rustls`] with SPIFFE/SPIRE using a live
Expand Down Expand Up @@ -28,9 +36,7 @@
//! }),
//! };
//!
//! let client_config = ClientConfigBuilder::new(source, opts)
//! .build()
//! .await?;
//! let client_config = ClientConfigBuilder::new(source, opts).build()?;
//! # Ok(())
//! # }
//! ```
Expand Down
63 changes: 42 additions & 21 deletions spiffe-rustls/src/material.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
use crate::error::{Error, Result};
use log::debug;
use rustls::pki_types::CertificateDer;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use rustls::RootCertStore;
use std::sync::Arc;

#[derive(Clone, Debug)]
pub(crate) struct MaterialSnapshot {
pub generation: u64,
pub certified_key: Arc<rustls::sign::CertifiedKey>,
pub roots: Arc<RootCertStore>,
}

pub(crate) fn roots_from_bundle_der(bundle_authorities: &[Vec<u8>]) -> Result<Arc<RootCertStore>> {
/// Build a `RootCertStore` from DER-encoded certificate authorities.
///
/// ## Errors
///
/// Returns [`Error::Internal`] if no certificates are accepted into the store.
pub(crate) fn roots_from_certs(certs: &[CertificateDer<'static>]) -> Result<Arc<RootCertStore>> {
let mut store = RootCertStore::empty();

let ders: Vec<CertificateDer<'static>> = bundle_authorities
.iter()
.cloned()
.map(CertificateDer::from)
.collect();
let added = store.add_parsable_certificates(certs.iter().cloned());

let added = store.add_parsable_certificates(ders);

debug!("loaded root cert(s): {:?}", added);
debug!("loaded root cert(s): {added:?}");

if store.is_empty() {
return Err(Error::Internal(
Expand All @@ -32,18 +32,17 @@ pub(crate) fn roots_from_bundle_der(bundle_authorities: &[Vec<u8>]) -> Result<Ar
Ok(Arc::new(store))
}

pub(crate) fn certified_key_from_der(
cert_chain_der: &[Vec<u8>],
/// Build a rustls `CertifiedKey` from a cert chain and a PKCS#8 private key.
///
/// ## Errors
///
/// Returns [`Error::CertifiedKey`] if the crypto provider is not installed
/// or the key can't be loaded.
pub(crate) fn certified_key_from_chain_and_key(
cert_chain: Vec<CertificateDer<'static>>,
private_key_pkcs8_der: &[u8],
) -> Result<Arc<rustls::sign::CertifiedKey>> {
let certs: Vec<rustls::pki_types::CertificateDer<'static>> = cert_chain_der
.iter()
.map(|c| rustls::pki_types::CertificateDer::from(c.clone()))
.collect();

let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(
rustls::pki_types::PrivatePkcs8KeyDer::from(private_key_pkcs8_der.to_vec()),
);
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(private_key_pkcs8_der.to_vec()));

let provider = rustls::crypto::CryptoProvider::get_default()
.ok_or_else(|| Error::CertifiedKey("rustls crypto provider is not installed".into()))?;
Expand All @@ -54,7 +53,29 @@ pub(crate) fn certified_key_from_der(
.map_err(|e| Error::CertifiedKey(format!("{e:?}")))?;

Ok(Arc::new(rustls::sign::CertifiedKey::new(
certs,
cert_chain,
signing_key,
)))
}

/// Helper: build an owned cert chain from an iterator of DER bytes.
///
/// This prevents higher layers from passing around `Vec<Vec<u8>>`.
pub(crate) fn cert_chain_from_der_bytes<'a, I>(ders: I) -> Vec<CertificateDer<'static>>
where
I: IntoIterator<Item = &'a [u8]>,
{
ders.into_iter()
.map(|b| CertificateDer::from(b.to_vec()))
.collect()
}

/// Helper: build owned root certs from an iterator of DER bytes.
pub(crate) fn certs_from_der_bytes<'a, I>(ders: I) -> Vec<CertificateDer<'static>>
where
I: IntoIterator<Item = &'a [u8]>,
{
ders.into_iter()
.map(|b| CertificateDer::from(b.to_vec()))
.collect()
}
Loading
Loading