feat(client-rust): accept gateway cert by default over TLS (or documented pin-only fallback)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
//! TLS posture coverage for the Rust client.
|
||||
//!
|
||||
//! tonic 0.13.1's `ClientTlsConfig` exposes no hook for a custom rustls
|
||||
//! `ServerCertVerifier` (the verifier is built internally inside the
|
||||
//! crate-private `TlsConnector`), so the Rust client cannot implement the
|
||||
//! "accept any server certificate" lenient default the other clients use.
|
||||
//! Rust is therefore the documented **pin-only exception**: TLS without a
|
||||
//! pinned CA is rejected up front with a clear, actionable error, and
|
||||
//! supplying a CA file is the supported path. These tests pin that contract.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use zb_mom_ww_mxgateway_client::{ClientOptions, Error, GatewayClient};
|
||||
|
||||
/// Drive `connect` to its error without requiring `GatewayClient: Debug`
|
||||
/// (the success arm is dropped explicitly so `unwrap_err` is unnecessary).
|
||||
async fn connect_err(options: ClientOptions) -> Error {
|
||||
match GatewayClient::connect(options).await {
|
||||
Ok(_client) => panic!("connect unexpectedly succeeded against a dead TLS address"),
|
||||
Err(error) => error,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_without_ca_is_rejected_with_actionable_error_by_default() {
|
||||
let options = ClientOptions::new("https://127.0.0.1:1")
|
||||
.with_plaintext(false)
|
||||
.with_connect_timeout(Duration::from_millis(200));
|
||||
|
||||
let error = connect_err(options).await;
|
||||
|
||||
let Error::InvalidEndpoint { detail, .. } = error else {
|
||||
panic!("expected InvalidEndpoint, got {error:?}");
|
||||
};
|
||||
// The message must point the caller at the supported remedy (pin a CA)
|
||||
// and name the opt-in escape hatch.
|
||||
assert!(
|
||||
detail.contains("ca_file") || detail.contains("CA"),
|
||||
"error should instruct the user to pass a CA file: {detail}"
|
||||
);
|
||||
assert!(
|
||||
detail.contains("require_certificate_validation"),
|
||||
"error should mention the require_certificate_validation opt-in: {detail}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_with_require_certificate_validation_does_not_short_circuit() {
|
||||
// With strict verification opted in, the no-CA guard must not fire; the
|
||||
// connect attempt instead proceeds to the transport (and fails to reach
|
||||
// the dead address) rather than returning the "CA required" guard error.
|
||||
let options = ClientOptions::new("https://127.0.0.1:1")
|
||||
.with_plaintext(false)
|
||||
.with_require_certificate_validation(true)
|
||||
.with_connect_timeout(Duration::from_millis(200));
|
||||
|
||||
let error = connect_err(options).await;
|
||||
|
||||
assert!(
|
||||
!matches!(&error, Error::InvalidEndpoint { detail, .. }
|
||||
if detail.contains("require_certificate_validation")),
|
||||
"strict verification must bypass the no-CA guard, got {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_with_ca_file_is_permitted_and_proceeds_past_the_guard() {
|
||||
// Pinning a CA is the supported TLS path: the no-CA guard must not fire.
|
||||
// We hand it a readable PEM file; construction proceeds past the guard
|
||||
// and only fails later at the transport (dead address / handshake).
|
||||
let ca_path = std::env::temp_dir().join("mxgw-rust-tls-ca-fixture.pem");
|
||||
std::fs::write(&ca_path, SELF_SIGNED_CA_PEM).unwrap();
|
||||
|
||||
let options = ClientOptions::new("https://127.0.0.1:1")
|
||||
.with_plaintext(false)
|
||||
.with_ca_file(&ca_path)
|
||||
.with_connect_timeout(Duration::from_millis(200));
|
||||
|
||||
let error = connect_err(options).await;
|
||||
|
||||
let _ = std::fs::remove_file(&ca_path);
|
||||
|
||||
assert!(
|
||||
!matches!(&error, Error::InvalidEndpoint { detail, .. }
|
||||
if detail.contains("require_certificate_validation")),
|
||||
"pinning a CA must bypass the no-CA guard, got {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A throwaway self-signed CA certificate (PEM). Only needs to parse as a
|
||||
/// PEM trust root so the CA-pinning path is exercised past the guard.
|
||||
const SELF_SIGNED_CA_PEM: &str = "-----BEGIN CERTIFICATE-----
|
||||
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
|
||||
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
|
||||
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
|
||||
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
|
||||
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
|
||||
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
|
||||
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
|
||||
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
|
||||
6MF9+Yw1Yy0t
|
||||
-----END CERTIFICATE-----
|
||||
";
|
||||
Reference in New Issue
Block a user