Skip to content

Commit 44827d2

Browse files
committed
starknet_transaction_prover: add TLS integration tests
Adds end-to-end HTTPS tests against a self-signed test certificate checked into resources/test_tls/: a successful HTTPS JSON-RPC roundtrip on starknet_specVersion, a plain-HTTP-against-TLS-server negative test, and unit tests around load_tls_acceptor covering missing cert/key paths, invalid PEM contents, and the happy path. To allow co-located tests in tls_test.rs to call it, load_tls_acceptor is bumped from private to pub(crate); no API surface is exposed outside the crate.
1 parent 3a5608b commit 44827d2

6 files changed

Lines changed: 223 additions & 1 deletion

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Test TLS Material
2+
3+
**This directory contains a self-signed certificate and its private key used
4+
exclusively by the unit tests in `src/server/tls_test.rs`.**
5+
6+
The key is intentionally checked into the repository — it is a test fixture,
7+
not a secret. Do not reuse this material outside of test code.
8+
9+
## Properties
10+
11+
- Subject: `CN=localhost`
12+
- SAN: `DNS:localhost,IP:127.0.0.1`
13+
- Validity: 100 years from generation
14+
- Key: 2048-bit RSA, unencrypted
15+
16+
## Regenerating
17+
18+
```bash
19+
openssl req -x509 -newkey rsa:2048 \
20+
-keyout crates/starknet_transaction_prover/resources/test_tls/key.pem \
21+
-out crates/starknet_transaction_prover/resources/test_tls/cert.pem \
22+
-sha256 -days 36500 -nodes \
23+
-subj "/CN=localhost" \
24+
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
25+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDJzCCAg+gAwIBAgIUCfCD8/3lfYaThN2hDz8c4CIbTDowDQYJKoZIhvcNAQEL
3+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MDUxNzE1Mjk0N1oYDzIxMjYw
4+
NDIzMTUyOTQ3WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
5+
AQUAA4IBDwAwggEKAoIBAQCcDEcAhUbdyBnUkIGykVCvNtXVg+sHYlvTTYnpasxM
6+
iUyY9jyoLYJlBPZjmNPipDd67REL2fGa7q3hkkkHAy4BxdYIqE2l9fPkX2unY/nS
7+
OWDemwPgOXEorcDWJC/kIVwNtVdqfLKG22d88QUvMe4rqUCA6J32dQv1/UAQ9OKw
8+
9kVQ3NAwGTMG2e61A3Ueur0DBmtap5YSn7IlT3HKQXSkTX08V7MCf8W9N3/Bg01h
9+
a7yAtNiQX66Gs4y/8CsM7eTBnLt4e0MVyhSJ9Zj2Ibpatr/YyWAhaW8p7y4sGo50
10+
3ovQ6isrOO8ayi6d7fYVCI27NDoiSmJ67okfQk0rf3DJAgMBAAGjbzBtMB0GA1Ud
11+
DgQWBBQdz7tV14UJxD6RchjKEDz61wgN/zAfBgNVHSMEGDAWgBQdz7tV14UJxD6R
12+
chjKEDz61wgN/zAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z
13+
dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAIiAb1/7gA/NXMck/k3WGnYCp3Aut
14+
rYDbfq1fwG/GJZ572qd9XREtuC198QPpQ04yjv7v2sspeWFqAS8e+Gr67h1cQ7q/
15+
mEnGPXQBKQ9r4TSamsFHnrh5x/J9Ec/hBXU9xd8OemZ+o00Itxn1FWwDBvudzfOA
16+
1IJ/7RNxmhUK2/dOOS9Jo5zGhRX/f6s8qmW6ZX2dRpvio3YqV30LpVSBZMsfQiWv
17+
dRHcl5xVoxxtvSkKt1Ou8VE2SEl61c7+Gb3zPBxOdT59QDFGZvX9PC8fPsOq2dNh
18+
ZhYJ4UJfWP5lMa372GATdPjVlZSf575xDpagUgSEKxEyCd7XC2k/p00u+w==
19+
-----END CERTIFICATE-----
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcDEcAhUbdyBnU
3+
kIGykVCvNtXVg+sHYlvTTYnpasxMiUyY9jyoLYJlBPZjmNPipDd67REL2fGa7q3h
4+
kkkHAy4BxdYIqE2l9fPkX2unY/nSOWDemwPgOXEorcDWJC/kIVwNtVdqfLKG22d8
5+
8QUvMe4rqUCA6J32dQv1/UAQ9OKw9kVQ3NAwGTMG2e61A3Ueur0DBmtap5YSn7Il
6+
T3HKQXSkTX08V7MCf8W9N3/Bg01ha7yAtNiQX66Gs4y/8CsM7eTBnLt4e0MVyhSJ
7+
9Zj2Ibpatr/YyWAhaW8p7y4sGo503ovQ6isrOO8ayi6d7fYVCI27NDoiSmJ67okf
8+
Qk0rf3DJAgMBAAECggEAQmpDSehvifMhc0Pxv4NzmK85AX/85w6o0F0fBlZrD2Qc
9+
UrnyhQ2hgsdC6o7gF4UXC92cNLQUzYEqRmhRZoem7CA8gUDIk4sDu74U/pBhgmTj
10+
YrsNQkCQdeTFvx51t52vJTJ6OxtJjHYTLK0ULMsOeEy35GWc3Ylhhte7jbv8Q55S
11+
wSwmCqxnEj6+5HlAeOIC2LJnrerDEejlTpXH1HCEsTH0dd4EQ/sHixzNVt2MSHDU
12+
fjhsmTl6kTO+RcIPoohuUSMrtrN9j/T4GxcrZqqwdzws/KMg1GYWvRxBJQHuO1yl
13+
5ivhA4sjE8cmN2SbcgrYa3R7aK3XzrCmxx4DKHNYlQKBgQDWeS4BIVEIKwzjjQOC
14+
RPhPg4Zo+0xYzceq4Q7ERn6mBojsLxNF91Z0Yx32rDpQziV1FESe0xmWneRCIjP8
15+
Ua31MU5FeH1GI7q0RyWD7XoUTI8io8cFdMiQEbRrYqARVrHun6NpBHIUG5Z5az8L
16+
JWRP/P0QN90cgkgLnqwxFicHywKBgQC6Qx7ej5cPBBf/m8Aijmd8cUDqrmk6A4Zc
17+
df8IjsfMdkv7H29qBip4KgNuThREaTq5fuCqgKvVKUgmudEFousfuWjzJsICBE9h
18+
4DGFDUxPBYyUFf70PmlQ8e4avvNg1Cx5VgIt4M2IAsUmiAC/52y14R67u7MfQP7q
19+
UWU3YitPOwKBgQCkq/A9n+YGnn9L68aI7Am3i2XVDzXEbWNj+V8MJpAxS40vwslK
20+
jCjOPhgQgJZZ2p358fDp/W2FLn/Go1pE3jXxr8TIJEYTZ3V/26ybSefU1B+GWjeC
21+
IfOoYl+jn9sE1QrTC7E8/dPVSoVTfpuuJCyMGdP38tyLeiB1A4R0P+0B1wKBgAtZ
22+
79Wsdo5Jt5SyT0FL4G6rEEO9IViRwmx8HHDPEsoZI4RIZCfX/FqaZN8iDwYkS5nm
23+
a5a4hMBW5bjGdkCbryydxhGbeRNaY+QZH6t2JgJi2jBkLsd/zjdKpzImFPr/sz4p
24+
ybQ2ERCK6qzweOs5FVz4PUE/rSjocyCgmUSIzQ7lAoGAE+Vc2EeHPxFRZgVSFS6m
25+
SEl5p5h8Wfr39+HKBSVd1P7QMHAFN11yXrGhIrRnAkI37chSvNtwLbTLicHuY127
26+
zEVAVXATvTZosErLp8WuNCxnb/1PxBVv1RMxJHm0ibjfRuWpDF+Gstn6ESQYKyPU
27+
Z84XK248kL9fShkmRNRujGI=
28+
-----END PRIVATE KEY-----

crates/starknet_transaction_prover/src/server.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub use health::{HealthLayer, HEALTH_PATH};
4343
mod cors_test;
4444
#[cfg(test)]
4545
mod rpc_spec_test;
46+
#[cfg(test)]
47+
mod tls_test;
4648

4749
#[cfg(test)]
4850
#[path = "server/ohttp_integration_test.rs"]

crates/starknet_transaction_prover/src/server/tls.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ pub async fn start_tls_server(
141141
}
142142

143143
/// Loads a certificate chain and private key from PEM files and builds a TLS acceptor.
144-
fn load_tls_acceptor(cert_path: &Path, key_path: &Path) -> anyhow::Result<TlsAcceptor> {
144+
pub(crate) fn load_tls_acceptor(cert_path: &Path, key_path: &Path) -> anyhow::Result<TlsAcceptor> {
145145
let cert_pem = std::fs::read(cert_path)
146146
.with_context(|| format!("Failed to read TLS certificate file: {}", cert_path.display()))?;
147147
let cert_chain: Vec<CertificateDer<'static>> =
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//! Integration tests for the TLS server bootstrap.
2+
//!
3+
//! Uses a self-signed certificate checked into `resources/test_tls/` (CN=localhost, valid for
4+
//! 100 years). See `resources/test_tls/README.md` for the openssl regeneration command.
5+
6+
use std::io::Write;
7+
use std::net::SocketAddr;
8+
use std::path::{Path, PathBuf};
9+
10+
use rstest::rstest;
11+
use serde_json::Value;
12+
use tempfile::NamedTempFile;
13+
14+
use crate::server::mock_rpc::MockProvingRpc;
15+
use crate::server::rpc_api::ProvingRpcServer;
16+
use crate::server::rpc_impl::SPEC_VERSION;
17+
use crate::server::tls::{load_tls_acceptor, start_tls_server};
18+
19+
fn ensure_crypto_provider() {
20+
let _ = tokio_rustls::rustls::crypto::aws_lc_rs::default_provider().install_default();
21+
}
22+
23+
fn test_cert_path() -> PathBuf {
24+
Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test_tls/cert.pem")
25+
}
26+
27+
fn test_key_path() -> PathBuf {
28+
Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test_tls/key.pem")
29+
}
30+
31+
fn write_pem_to_tempfile(pem_bytes: &[u8]) -> NamedTempFile {
32+
let mut file = NamedTempFile::new().unwrap();
33+
file.write_all(pem_bytes).unwrap();
34+
file.flush().unwrap();
35+
file
36+
}
37+
38+
fn spec_version_request() -> Value {
39+
serde_json::json!({
40+
"jsonrpc": "2.0",
41+
"id": "1",
42+
"method": "starknet_specVersion"
43+
})
44+
}
45+
46+
async fn start_test_tls_server() -> (SocketAddr, jsonrpsee::server::ServerHandle, Vec<u8>) {
47+
let methods = MockProvingRpc::from_expected_json().into_rpc();
48+
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
49+
50+
let (local_addr, handle) = start_tls_server(
51+
addr,
52+
&test_cert_path(),
53+
&test_key_path(),
54+
methods,
55+
10, // max_connections
56+
5 * 1024 * 1024, // max_request_body_size
57+
None, // cors_layer
58+
None, // ohttp_layer
59+
)
60+
.await
61+
.expect("Failed to start TLS server");
62+
63+
let cert_pem = std::fs::read(test_cert_path()).unwrap();
64+
(local_addr, handle, cert_pem)
65+
}
66+
67+
#[tokio::test]
68+
async fn test_https_spec_version_succeeds() {
69+
ensure_crypto_provider();
70+
let (addr, handle, cert_pem) = start_test_tls_server().await;
71+
72+
let cert = reqwest::tls::Certificate::from_pem(&cert_pem).unwrap();
73+
let client = reqwest::Client::builder().add_root_certificate(cert).build().unwrap();
74+
75+
let response = client
76+
.post(format!("https://localhost:{}", addr.port()))
77+
.json(&spec_version_request())
78+
.send()
79+
.await
80+
.expect("HTTPS request failed");
81+
82+
assert_eq!(response.status(), 200);
83+
let json: Value = response.json().await.unwrap();
84+
assert_eq!(json["result"].as_str().unwrap(), SPEC_VERSION);
85+
86+
handle.stop().unwrap();
87+
}
88+
89+
#[tokio::test]
90+
async fn test_http_to_tls_server_fails() {
91+
ensure_crypto_provider();
92+
let (addr, handle, _cert_pem) = start_test_tls_server().await;
93+
94+
// Plain HTTP to a TLS server should fail (connection or protocol error).
95+
let result = reqwest::Client::new()
96+
.post(format!("http://localhost:{}", addr.port()))
97+
.json(&spec_version_request())
98+
.send()
99+
.await;
100+
assert!(result.is_err(), "Expected HTTP to TLS server to fail, got: {result:?}");
101+
102+
handle.stop().unwrap();
103+
}
104+
105+
/// How a given path argument is materialised for `load_tls_acceptor`.
106+
enum PathMode {
107+
/// Use the checked-in valid test fixture.
108+
Valid,
109+
/// Path to a file that does not exist.
110+
Missing,
111+
/// Path to a tempfile containing these bytes (returned alongside the path so the
112+
/// `NamedTempFile` is kept alive for the call).
113+
Junk(&'static [u8]),
114+
}
115+
116+
/// `PathMode::Junk` returns `Some(tempfile)` so the tempfile is dropped after the test, not before.
117+
fn materialise(mode: PathMode, missing: &str, valid: PathBuf) -> (PathBuf, Option<NamedTempFile>) {
118+
match mode {
119+
PathMode::Valid => (valid, None),
120+
PathMode::Missing => (missing.into(), None),
121+
PathMode::Junk(bytes) => {
122+
let file = write_pem_to_tempfile(bytes);
123+
(file.path().into(), Some(file))
124+
}
125+
}
126+
}
127+
128+
/// Each case isolates one specific failure path by holding the other input valid, so a green test
129+
/// proves `load_tls_acceptor` actually rejected on the named reason and not on something earlier.
130+
#[rstest]
131+
#[case::missing_cert(PathMode::Missing, PathMode::Valid)]
132+
#[case::missing_key(PathMode::Valid, PathMode::Missing)]
133+
#[case::invalid_cert_pem(PathMode::Junk(b"not a valid PEM cert"), PathMode::Valid)]
134+
#[case::invalid_key_pem(PathMode::Valid, PathMode::Junk(b"not a valid PEM key"))]
135+
fn test_load_tls_acceptor_failure(#[case] cert: PathMode, #[case] key: PathMode) {
136+
let (cert_path, _cert_tmp) = materialise(cert, "/nonexistent/cert.pem", test_cert_path());
137+
let (key_path, _key_tmp) = materialise(key, "/nonexistent/key.pem", test_key_path());
138+
139+
assert!(load_tls_acceptor(&cert_path, &key_path).is_err());
140+
}
141+
142+
#[test]
143+
fn test_load_tls_acceptor_succeeds_for_valid_files() {
144+
// `load_tls_acceptor` builds a rustls `ServerConfig`, which requires a process-level crypto
145+
// provider. nextest runs each test in a fresh process, so install the provider here.
146+
ensure_crypto_provider();
147+
load_tls_acceptor(&test_cert_path(), &test_key_path()).unwrap();
148+
}

0 commit comments

Comments
 (0)