@@ -4,6 +4,39 @@ use testsuite::cli::dgw_tokio_cmd;
44use testsuite:: dgw_config:: { DgwConfig , DgwConfigHandle } ;
55use 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+
740async 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.
3063async 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
107224mod 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