Skip to content

Commit f4c49bc

Browse files
committed
test(dgw): use local TLS server for thumbprint anchoring tests
Replace external badssl.com dependencies with a local dummy TLS server using rustls. This improves test reliability by eliminating network dependencies and makes the tests fully reproducible.
1 parent 72130bd commit f4c49bc

File tree

3 files changed

+174
-32
lines changed

3 files changed

+174
-32
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testsuite/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ typed-builder = "0.21"
3030
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots"] }
3131

3232
[dev-dependencies]
33+
base64 = "0.22"
3334
mcp-proxy.path = "../crates/mcp-proxy"
3435
rstest = "0.25"
3536
serde_json = "1"
3637
sysevent.path = "../crates/sysevent"
3738
tempfile = "3"
3839
test-utils.path = "../crates/test-utils"
40+
tokio-rustls = { version = "0.26", features = ["ring"] }
3941

4042
[target.'cfg(unix)'.dev-dependencies]
4143
sysevent-syslog.path = "../crates/sysevent-syslog"

testsuite/tests/cli/dgw/tls_anchoring.rs

Lines changed: 170 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,39 @@ use testsuite::cli::dgw_tokio_cmd;
44
use testsuite::dgw_config::{DgwConfig, DgwConfigHandle};
55
use tokio::process::Child;
66

7+
#[rstest]
8+
#[case::self_signed_correct_thumb(true, true, TlsOutcome::Succeeded)]
9+
#[case::self_signed_wrong_thumb(true, false, TlsOutcome::Failed)]
10+
#[case::self_signed_no_thumb(false, false, TlsOutcome::Failed)]
11+
#[tokio::test]
12+
async fn test(
13+
#[case] include_thumbprint: bool,
14+
#[case] correct_thumbprint: bool,
15+
#[case] expected_outcome: TlsOutcome,
16+
) -> anyhow::Result<()> {
17+
let tls_port = start_dummy_tls_server().await?;
18+
let (config_handle, mut process) = start_gateway().await?;
19+
20+
let token = token::build(tls_port, include_thumbprint, correct_thumbprint);
21+
let stdout = process.stdout.take().unwrap();
22+
23+
let connect_fut = websocket_connect(config_handle.http_port(), &token, token::SESSION_ID);
24+
let read_fut = read_until_tls_done(stdout);
25+
26+
tokio::select! {
27+
res = connect_fut => {
28+
res.context("websocket connect")?;
29+
anyhow::bail!("expected read future to terminate before connect future");
30+
}
31+
res = read_fut => {
32+
let outcome = res.context("read")?;
33+
assert_eq!(outcome, expected_outcome);
34+
}
35+
}
36+
37+
Ok(())
38+
}
39+
740
async fn start_gateway() -> anyhow::Result<(DgwConfigHandle, Child)> {
841
let config_handle = DgwConfig::builder()
942
.disable_token_validation(true)
@@ -26,11 +59,11 @@ async fn start_gateway() -> anyhow::Result<(DgwConfigHandle, Child)> {
2659
Ok((config_handle, process))
2760
}
2861

29-
/// Perform a WebSocket connection on the /jet/fwd/tcp endpoint.
62+
/// Perform a WebSocket connection on the /jet/fwd/tls endpoint.
3063
async fn websocket_connect(port: u16, token: &str, session_id: &str) -> anyhow::Result<()> {
3164
let url = format!("ws://127.0.0.1:{port}/jet/fwd/tls/{session_id}?token={token}");
3265

33-
// Try to connect with a timeout
66+
// Try to connect with a timeout.
3467
let (_ws_stream, response) =
3568
tokio::time::timeout(std::time::Duration::from_secs(5), tokio_tungstenite::connect_async(url))
3669
.await
@@ -74,48 +107,153 @@ async fn read_until_tls_done(mut logs: impl tokio::io::AsyncRead + Unpin) -> any
74107
}
75108
}
76109

77-
#[rstest]
78-
#[case::self_signed_correct_thumb(token::SELF_SIGNED_WITH_CORRECT_THUMB, TlsOutcome::Succeeded)]
79-
#[case::self_signed_wrong_thumb(token::SELF_SIGNED_WITH_WRONG_THUMB, TlsOutcome::Failed)]
80-
#[case::self_signed_no_thumb(token::SELF_SIGNED_NO_THUMB, TlsOutcome::Failed)]
81-
#[case::valid_cert_no_thumb(token::VALID_CERT_NO_THUMB, TlsOutcome::Succeeded)]
82-
#[tokio::test]
83-
async fn test(#[case] token: &str, #[case] expected_outcome: TlsOutcome) -> anyhow::Result<()> {
84-
let (config_handle, mut process) = start_gateway().await?;
110+
/// Starts a dummy TLS server and returns its port.
111+
async fn start_dummy_tls_server() -> anyhow::Result<u16> {
112+
use std::sync::Arc;
113+
use tokio::io::AsyncWriteExt as _;
114+
use tokio::net::TcpListener;
115+
use tokio_rustls::TlsAcceptor;
116+
use tokio_rustls::rustls::ServerConfig;
117+
use tokio_rustls::rustls::crypto::ring::default_provider;
118+
use tokio_rustls::rustls::pki_types::pem::PemObject as _;
119+
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
85120

86-
let stdout = process.stdout.take().unwrap();
121+
// Install the ring crypto provider if not already installed.
122+
let _ = default_provider().install_default();
87123

88-
let connect_fut = websocket_connect(config_handle.http_port(), token, token::SESSION_ID);
89-
let read_fut = read_until_tls_done(stdout);
124+
let cert_pem = tls::CERT_PEM;
125+
let key_pem = tls::KEY_PEM;
90126

91-
tokio::select! {
92-
res = connect_fut => {
93-
res.context("websocket connect")?;
94-
anyhow::bail!("expected read future to terminate before connect future");
95-
}
96-
res = read_fut => {
97-
let outcome = res.context("read")?;
98-
assert_eq!(outcome, expected_outcome);
127+
// Parse certificate.
128+
let cert = CertificateDer::from_pem_slice(cert_pem.as_bytes()).context("parse certificate")?;
129+
130+
// Parse private key.
131+
let key = PrivateKeyDer::from_pem_slice(key_pem.as_bytes()).context("parse private key DER")?;
132+
133+
// Build TLS config.
134+
let tls_config = ServerConfig::builder()
135+
.with_no_client_auth()
136+
.with_single_cert(vec![cert], key)
137+
.context("build TLS config")?;
138+
139+
let acceptor = TlsAcceptor::from(Arc::new(tls_config));
140+
141+
// Bind to an ephemeral port.
142+
let listener = TcpListener::bind("127.0.0.1:0").await.context("bind")?;
143+
let port = listener.local_addr().context("local_addr")?.port();
144+
145+
// We spawn-and-forget the task; the async runtime is dropped at the end of
146+
// the test, including all the spawned futures.
147+
tokio::spawn(async move {
148+
loop {
149+
let Ok((stream, _)) = listener.accept().await else {
150+
break;
151+
};
152+
153+
let acceptor = acceptor.clone();
154+
155+
tokio::spawn(async move {
156+
if let Ok(mut tls_stream) = acceptor.accept(stream).await {
157+
// Send a simple response and close.
158+
let _ = tls_stream.write_all(b"Hello from dummy TLS server\n").await;
159+
let _ = tls_stream.shutdown().await;
160+
}
161+
});
99162
}
100-
}
163+
});
101164

102-
Ok(())
165+
Ok(port)
103166
}
104167

105-
// FIXME: Spawn a dummy TLS server using rustls for better reproducibility.
168+
mod tls {
169+
/// Self-signed certificate for localhost (valid for 100 years).
170+
pub(super) const CERT_PEM: &str = r#"-----BEGIN CERTIFICATE-----
171+
MIIDCzCCAfOgAwIBAgIUPRJa8i280unV3/kW6TE2fSUw8PwwDQYJKoZIhvcNAQEL
172+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI1MTEyNTA5NDAzMFoYDzIxMjUx
173+
MTAxMDk0MDMwWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
174+
AQUAA4IBDwAwggEKAoIBAQDHpBlyRgUx/V9cQGw/eqDFc6odxB2hvnbudi67LvEj
175+
cNIWOU79R1e/NswME4oecqT9W05n4UyxkABfm2qjODO0nDf47W0DsgbEA87qE715
176+
RWg8AtC529CZAazqTV3gqYyRMsCuVKzPVxgWa8rhPc7E6In1uDRak0lWKQPQSBbc
177+
34nxMOVIusZNlkAEar8/aYPr/YWvdEqkobEvXp+g9WsuMaU913ecacWDjyWDkf80
178+
pPPtf+uet7WMysKMhzGQtpbgilT8XCo8uTsgUbK+TMWvkF9bcxAQDnJsrZRL7Jfh
179+
ofsFfQbTIvbvpn+4J4kmHN36BTohlNL8TX1jrU3cPA7dAgMBAAGjUzBRMB0GA1Ud
180+
DgQWBBTT+m6dyc/c3mXF3JAsZr9OqUwgWTAfBgNVHSMEGDAWgBTT+m6dyc/c3mXF
181+
3JAsZr9OqUwgWTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBB
182+
i/yonZY3ztaeGElzD8xkI+rJ+daJ5WzdfKnzudJllg/Ht8m7wO5SdQnMt2T44gbH
183+
05uekc1zXnXb7fJKqs3R6DacctG0nQ3acuI+IMtTaBbbAcf3PJJlo0Pap0ypVC0R
184+
IUiUhJGFNi4cCBOvJqsly0d3T5xqOXU1Q5j3mIwRBY68+m9btwwuZWvASRADtCyZ
185+
RpisBzS4a6jSeHXa4iG/VhskbiZkcnfHNTw7yNJJdv125y2zQkWWF9wlLbYwWr40
186+
x9Ba6YbssOz6epATKhvt80yclO34AzUyimssvViIUpgFEyaPhZZTw46Q/6X3ixK4
187+
/v4eYM0cCHN0h+rynSor
188+
-----END CERTIFICATE-----"#;
189+
190+
/// Private key for the self-signed certificate.
191+
pub(super) const KEY_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
192+
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHpBlyRgUx/V9c
193+
QGw/eqDFc6odxB2hvnbudi67LvEjcNIWOU79R1e/NswME4oecqT9W05n4UyxkABf
194+
m2qjODO0nDf47W0DsgbEA87qE715RWg8AtC529CZAazqTV3gqYyRMsCuVKzPVxgW
195+
a8rhPc7E6In1uDRak0lWKQPQSBbc34nxMOVIusZNlkAEar8/aYPr/YWvdEqkobEv
196+
Xp+g9WsuMaU913ecacWDjyWDkf80pPPtf+uet7WMysKMhzGQtpbgilT8XCo8uTsg
197+
UbK+TMWvkF9bcxAQDnJsrZRL7JfhofsFfQbTIvbvpn+4J4kmHN36BTohlNL8TX1j
198+
rU3cPA7dAgMBAAECggEAKh7KK5zwTaq6atlAvWfe8anEk4EkC1MG/qq6k02FHMgZ
199+
2wx+SNu7fKFQDaA1vNTNUJLqCOq05qWOHp3IsuURq6JmAMP/Aw+Vc9el2ScPC74E
200+
Dt09MmlZKl77H3fxPYwoFx5RHrbIuvoSH/DgHgOPU2YIbWpOyWlXyLDgmBoNkM3N
201+
fXYLXJONpStPHeQLhh7LcHO3CZgn6kycJyByEO2NtcchS5zITiJuwL+qR5/QIlvD
202+
Yo7jdCjelJat38MZ9dE1us8xlIjQtsYF/acZZtcpYho+7ZpDCNcb+xF8KStKei+B
203+
MMpWISsa+Zh9g7lPYTnG/i1dSMMT100XCEw8o4rBoQKBgQDnptz8acp7DB2wJH4L
204+
c0xuw8IlrSl3BGUEj8H+RyFlpH3+//i6/fE9MrtF8b4FSYUp5AG4NVFGcRbwJVGW
205+
jeL13YwIKMdXjmx8fDIylCgBB1tzBS9T/0ws3HS8avxhKvjgoXIZm6D3XDcBslrH
206+
c9/LojT8YGI1wx7jWI2qKj8yeQKBgQDcn+kQ1QjzgIz6bAVWY3t1jr5uHHyaS+5G
207+
ihY/mx4Mn3DURgPXZHz/HrN9rZkax0zuq9wuIlqgZ2KI37iCF49M4aZxC788LyDo
208+
Hp0Cak3wt3g0Tj6J7SJiQe8h/6VBS4R5dRD2vhEc3xPAOf7WIFdlLYBOOvE/LmOt
209+
N6ChkfgGhQKBgQDSiDqLRPJ7BjXtIh1T9sPeXxeR+mCXBG1yydx7ZtYZdHf2S1kZ
210+
STX4cqT1GpGiaIEX41sUuZBWPu2j76bI98bvwRxFRhp1nsFGGfHdOf1pgfBBBtNO
211+
udXXZ7zIiUs6XD24mcIDOAgBB9QOPLR4VP1uKsuRG1/mkKD/6jlGEANDsQKBgQDC
212+
AoEygxQnBVFz2c/rwvnLS+Zb8AMGsGTtdPrRnjeThBX1JUi1fbGJq1bN2v27Fa2q
213+
aEjr7NvjGGcG1C1tgQhL5Fa4LEtTwmHenSUW/aJiXwR+gpvuMDC/VRnTvPp2a9En
214+
+XEcedGUoPq+XIGjjLctyxB8Osrw83tF1JgV3MXN/QKBgQC83B54rYDd4QmVH5nL
215+
WLw834fgr+Z1hA6UqJIaahlD/bDwzbbJEv0pHCBxe01ywQFivqWBdVbuoy9YSeLS
216+
KKEklzh+L0SorrYoBA5F63qx0zy05bba0ASplgDUEUNZn7oIFi7x5pVsNNaNxZpR
217+
bQGM8UrNQvWQ+tutRmp7PM6VuQ==
218+
-----END PRIVATE KEY-----"#;
219+
220+
/// SHA-256 thumbprint of the certificate.
221+
pub(super) const CERT_THUMBPRINT: &str = "bce13f257b9d856404c51b46f2420eff6d01b3a4c99fe3d0e11e4517c2291b70";
222+
}
106223

107224
mod token {
225+
use base64::prelude::*;
226+
108227
pub(super) const SESSION_ID: &str = "897fd399-540c-4be3-84a1-47c73f68c7a4";
109228

110-
/// Token with correct thumbprint for self-signed.badssl.com
111-
pub(super) const SELF_SIGNED_WITH_CORRECT_THUMB: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkFTU09DSUFUSU9OIn0.eyJjZXJ0X3RodW1iMjU2IjoiYmRjYWYxYzY1ZTg2MTAwOGUwMTFjZmVhNGM2YmM1N2I3YjVkOTAwOGY2YTE4N2JiYzM1Nzk3YWIyNWRiYWFmZSIsImRzdF9oc3QiOiJzZWxmLXNpZ25lZC5iYWRzc2wuY29tOjQ0MyIsImV4cCI6MTc2MjkzNzI5OCwiamV0X2FpZCI6Ijg5N2ZkMzk5LTU0MGMtNGJlMy04NGExLTQ3YzczZjY4YzdhNCIsImpldF9hcCI6InVua25vd24iLCJqZXRfY20iOiJmd2QiLCJqZXRfcmVjIjoibm9uZSIsImp0aSI6IjgwYTcxN2JmLTZlMzItNGEyMi05Yjk3LTVlYzFkNzk1YjVlMSIsIm5iZiI6MTc2MjkzNjM5OH0.ZHVtbXlfc2lnbmF0dXJl";
229+
/// Build a JWT token for TLS anchoring tests.
230+
pub(super) fn build(port: u16, include_thumbprint: bool, correct_thumbprint: bool) -> String {
231+
/// Static JWT header: {"alg":"RS256","typ":"JWT","cty":"ASSOCIATION"}
232+
const HEADER: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkFTU09DSUFUSU9OIn0";
233+
234+
/// Static dummy signature.
235+
const SIGNATURE: &str = "ZHVtbXlfc2lnbmF0dXJl";
112236

113-
/// Token with wrong thumbprint for self-signed.badssl.com
114-
pub(super) const SELF_SIGNED_WITH_WRONG_THUMB: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkFTU09DSUFUSU9OIn0.eyJjZXJ0X3RodW1iMjU2IjoiYTkxYTIyODIyZjQ4NjA2NDQwNTkyNjU1ODExMTExNThmNTUyMTNkODc0YzVmYmY1NzFjZThiZTYzYmZlY2Y1NCIsImRzdF9oc3QiOiJzZWxmLXNpZ25lZC5iYWRzc2wuY29tOjQ0MyIsImV4cCI6MTc2MjkzODI5MywiamV0X2FpZCI6Ijg5N2ZkMzk5LTU0MGMtNGJlMy04NGExLTQ3YzczZjY4YzdhNCIsImpldF9hcCI6InVua25vd24iLCJqZXRfY20iOiJmd2QiLCJqZXRfcmVjIjoibm9uZSIsImp0aSI6IjRlMjZhNjM2LTA0MjUtNDNlMy1iMGZmLWYzZDk1ODhjZWY4YSIsIm5iZiI6MTc2MjkzNzM5M30.ZHVtbXlfc2lnbmF0dXJl";
237+
/// A wrong thumbprint for testing.
238+
const WRONG_THUMBPRINT: &str = "0000000000000000000000000000000000000000000000000000000000000000";
115239

116-
/// Token without thumbprint for self-signed.badssl.com
117-
pub(super) const SELF_SIGNED_NO_THUMB: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkFTU09DSUFUSU9OIn0.eyJkc3RfaHN0Ijoic2VsZi1zaWduZWQuYmFkc3NsLmNvbTo0NDMiLCJleHAiOjE3NjI5Mzc0ODAsImpldF9haWQiOiI4OTdmZDM5OS01NDBjLTRiZTMtODRhMS00N2M3M2Y2OGM3YTQiLCJqZXRfYXAiOiJ1bmtub3duIiwiamV0X2NtIjoiZndkIiwiamV0X3JlYyI6Im5vbmUiLCJqdGkiOiI0ODdjZThiNS1lY2ZmLTRlY2QtYWE3ZC0wNTJkNThlM2U2YjEiLCJuYmYiOjE3NjI5MzY1ODB9.ZHVtbXlfc2lnbmF0dXJl";
240+
let thumbprint_field = if include_thumbprint {
241+
let thumb = if correct_thumbprint {
242+
super::tls::CERT_THUMBPRINT
243+
} else {
244+
WRONG_THUMBPRINT
245+
};
246+
format!(r#""cert_thumb256":"{thumb}","#)
247+
} else {
248+
String::new()
249+
};
118250

119-
/// Token without thumbprint for badssl.com (valid cert)
120-
pub(super) const VALID_CERT_NO_THUMB: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkFTU09DSUFUSU9OIn0.eyJkc3RfaHN0IjoiYmFkc3NsLmNvbTo0NDMiLCJleHAiOjE3NjI5Mzc1MjEsImpldF9haWQiOiI4OTdmZDM5OS01NDBjLTRiZTMtODRhMS00N2M3M2Y2OGM3YTQiLCJqZXRfYXAiOiJ1bmtub3duIiwiamV0X2NtIjoiZndkIiwiamV0X3JlYyI6Im5vbmUiLCJqdGkiOiI4YWUzMzkxNS00ZDNlLTQyYmItODBkNi0yYjQzYjIyN2QzYTQiLCJuYmYiOjE3NjI5MzY2MjF9.ZHVtbXlfc2lnbmF0dXJl";
251+
let body_json = format!(
252+
r#"{{{thumbprint_field}"dst_hst":"127.0.0.1:{port}","exp":9999999999,"jet_aid":"{SESSION_ID}","jet_ap":"unknown","jet_cm":"fwd","jet_rec":"none","jti":"00000000-0000-0000-0000-000000000000","nbf":0}}"#
253+
);
254+
255+
let body = BASE64_URL_SAFE_NO_PAD.encode(body_json.as_bytes());
256+
257+
format!("{HEADER}.{body}.{SIGNATURE}")
258+
}
121259
}

0 commit comments

Comments
 (0)