feat(client-rust): accept gateway cert by default over TLS (or documented pin-only fallback)
This commit is contained in:
@@ -89,6 +89,26 @@ impl GatewayClient {
|
||||
detail: format!("failed to read CA file {}: {source}", ca_file.display()),
|
||||
})?;
|
||||
tls = tls.ca_certificate(Certificate::from_pem(certificate));
|
||||
} else if !options.require_certificate_validation() {
|
||||
// Lenient-default fallback (Rust pin-only exception): tonic
|
||||
// 0.13's `ClientTlsConfig` builds its rustls verifier inside a
|
||||
// crate-private connector and exposes no hook for a custom
|
||||
// `ServerCertVerifier`, so — unlike the other clients — the
|
||||
// Rust client cannot accept an arbitrary self-signed cert. Pin
|
||||
// the gateway's CA instead, or opt into strict verification
|
||||
// against the system trust roots. We reject here rather than
|
||||
// silently verifying against system roots (which would fail a
|
||||
// self-signed gateway with a confusing handshake error).
|
||||
return Err(Error::InvalidEndpoint {
|
||||
endpoint: options.endpoint().to_owned(),
|
||||
detail: "TLS requested without a pinned CA. The Rust client cannot accept an \
|
||||
arbitrary self-signed certificate (tonic 0.13 exposes no custom \
|
||||
rustls verifier). Pin the gateway certificate with \
|
||||
ClientOptions::with_ca_file, or call \
|
||||
ClientOptions::with_require_certificate_validation(true) to verify \
|
||||
against the system trust roots."
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
endpoint = endpoint.tls_config(tls)?;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct ClientOptions {
|
||||
api_key: Option<ApiKey>,
|
||||
plaintext: bool,
|
||||
ca_file: Option<PathBuf>,
|
||||
require_certificate_validation: bool,
|
||||
server_name_override: Option<String>,
|
||||
connect_timeout: Duration,
|
||||
call_timeout: Duration,
|
||||
@@ -38,6 +39,7 @@ impl ClientOptions {
|
||||
api_key: None,
|
||||
plaintext: true,
|
||||
ca_file: None,
|
||||
require_certificate_validation: false,
|
||||
server_name_override: None,
|
||||
connect_timeout: Duration::from_secs(10),
|
||||
call_timeout: Duration::from_secs(30),
|
||||
@@ -67,6 +69,22 @@ impl ClientOptions {
|
||||
self
|
||||
}
|
||||
|
||||
/// Require TLS certificate verification even without a pinned CA. Default
|
||||
/// false: the gateway's self-signed certificate is accepted (internal-tool
|
||||
/// posture). Setting a CA file always verifies.
|
||||
///
|
||||
/// Note for Rust: tonic 0.13's `ClientTlsConfig` exposes no hook for a
|
||||
/// custom rustls verifier, so the Rust client cannot accept an arbitrary
|
||||
/// self-signed certificate the way the other clients do. With the default
|
||||
/// (false) and no pinned CA, [`crate::client::GatewayClient::connect`]
|
||||
/// rejects the TLS connection and asks for a CA file. Either pin a CA via
|
||||
/// [`ClientOptions::with_ca_file`] (the supported lenient path on Rust) or
|
||||
/// set this `true` to verify against the system trust roots.
|
||||
pub fn with_require_certificate_validation(mut self, require: bool) -> Self {
|
||||
self.require_certificate_validation = require;
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the SNI/server name used during the TLS handshake. Useful
|
||||
/// when the dial-target host name does not match the certificate.
|
||||
pub fn with_server_name_override(mut self, server_name_override: impl Into<String>) -> Self {
|
||||
@@ -121,6 +139,12 @@ impl ClientOptions {
|
||||
self.ca_file.as_ref()
|
||||
}
|
||||
|
||||
/// Whether TLS certificate verification is required even without a pinned
|
||||
/// CA. See [`ClientOptions::with_require_certificate_validation`].
|
||||
pub fn require_certificate_validation(&self) -> bool {
|
||||
self.require_certificate_validation
|
||||
}
|
||||
|
||||
/// Optional SNI / server-name override for TLS handshakes.
|
||||
pub fn server_name_override(&self) -> Option<&str> {
|
||||
self.server_name_override.as_deref()
|
||||
@@ -161,6 +185,10 @@ impl fmt::Debug for ClientOptions {
|
||||
.field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
|
||||
.field("plaintext", &self.plaintext)
|
||||
.field("ca_file", &self.ca_file)
|
||||
.field(
|
||||
"require_certificate_validation",
|
||||
&self.require_certificate_validation,
|
||||
)
|
||||
.field("server_name_override", &self.server_name_override)
|
||||
.field("connect_timeout", &self.connect_timeout)
|
||||
.field("call_timeout", &self.call_timeout)
|
||||
|
||||
Reference in New Issue
Block a user