diff --git a/crates/starknet_transaction_prover/resources/test_tls/README.md b/crates/starknet_transaction_prover/resources/test_tls/README.md new file mode 100644 index 00000000000..1904a6bf846 --- /dev/null +++ b/crates/starknet_transaction_prover/resources/test_tls/README.md @@ -0,0 +1,25 @@ +# Test TLS Material + +**This directory contains a self-signed certificate and its private key used +exclusively by the unit tests in `src/server/tls_test.rs`.** + +The key is intentionally checked into the repository — it is a test fixture, +not a secret. Do not reuse this material outside of test code. + +## Properties + +- Subject: `CN=localhost` +- SAN: `DNS:localhost,IP:127.0.0.1` +- Validity: 100 years from generation +- Key: 2048-bit RSA, unencrypted + +## Regenerating + +```bash +openssl req -x509 -newkey rsa:2048 \ + -keyout crates/starknet_transaction_prover/resources/test_tls/key.pem \ + -out crates/starknet_transaction_prover/resources/test_tls/cert.pem \ + -sha256 -days 36500 -nodes \ + -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" +``` diff --git a/crates/starknet_transaction_prover/resources/test_tls/cert.pem b/crates/starknet_transaction_prover/resources/test_tls/cert.pem new file mode 100644 index 00000000000..f85267ce6e4 --- /dev/null +++ b/crates/starknet_transaction_prover/resources/test_tls/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJzCCAg+gAwIBAgIUCfCD8/3lfYaThN2hDz8c4CIbTDowDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MDUxNzE1Mjk0N1oYDzIxMjYw +NDIzMTUyOTQ3WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCcDEcAhUbdyBnUkIGykVCvNtXVg+sHYlvTTYnpasxM +iUyY9jyoLYJlBPZjmNPipDd67REL2fGa7q3hkkkHAy4BxdYIqE2l9fPkX2unY/nS +OWDemwPgOXEorcDWJC/kIVwNtVdqfLKG22d88QUvMe4rqUCA6J32dQv1/UAQ9OKw +9kVQ3NAwGTMG2e61A3Ueur0DBmtap5YSn7IlT3HKQXSkTX08V7MCf8W9N3/Bg01h +a7yAtNiQX66Gs4y/8CsM7eTBnLt4e0MVyhSJ9Zj2Ibpatr/YyWAhaW8p7y4sGo50 +3ovQ6isrOO8ayi6d7fYVCI27NDoiSmJ67okfQk0rf3DJAgMBAAGjbzBtMB0GA1Ud +DgQWBBQdz7tV14UJxD6RchjKEDz61wgN/zAfBgNVHSMEGDAWgBQdz7tV14UJxD6R +chjKEDz61wgN/zAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z +dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAIiAb1/7gA/NXMck/k3WGnYCp3Aut +rYDbfq1fwG/GJZ572qd9XREtuC198QPpQ04yjv7v2sspeWFqAS8e+Gr67h1cQ7q/ +mEnGPXQBKQ9r4TSamsFHnrh5x/J9Ec/hBXU9xd8OemZ+o00Itxn1FWwDBvudzfOA +1IJ/7RNxmhUK2/dOOS9Jo5zGhRX/f6s8qmW6ZX2dRpvio3YqV30LpVSBZMsfQiWv +dRHcl5xVoxxtvSkKt1Ou8VE2SEl61c7+Gb3zPBxOdT59QDFGZvX9PC8fPsOq2dNh +ZhYJ4UJfWP5lMa372GATdPjVlZSf575xDpagUgSEKxEyCd7XC2k/p00u+w== +-----END CERTIFICATE----- diff --git a/crates/starknet_transaction_prover/resources/test_tls/key.pem b/crates/starknet_transaction_prover/resources/test_tls/key.pem new file mode 100644 index 00000000000..fcd884b2e52 --- /dev/null +++ b/crates/starknet_transaction_prover/resources/test_tls/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcDEcAhUbdyBnU +kIGykVCvNtXVg+sHYlvTTYnpasxMiUyY9jyoLYJlBPZjmNPipDd67REL2fGa7q3h +kkkHAy4BxdYIqE2l9fPkX2unY/nSOWDemwPgOXEorcDWJC/kIVwNtVdqfLKG22d8 +8QUvMe4rqUCA6J32dQv1/UAQ9OKw9kVQ3NAwGTMG2e61A3Ueur0DBmtap5YSn7Il +T3HKQXSkTX08V7MCf8W9N3/Bg01ha7yAtNiQX66Gs4y/8CsM7eTBnLt4e0MVyhSJ +9Zj2Ibpatr/YyWAhaW8p7y4sGo503ovQ6isrOO8ayi6d7fYVCI27NDoiSmJ67okf +Qk0rf3DJAgMBAAECggEAQmpDSehvifMhc0Pxv4NzmK85AX/85w6o0F0fBlZrD2Qc +UrnyhQ2hgsdC6o7gF4UXC92cNLQUzYEqRmhRZoem7CA8gUDIk4sDu74U/pBhgmTj +YrsNQkCQdeTFvx51t52vJTJ6OxtJjHYTLK0ULMsOeEy35GWc3Ylhhte7jbv8Q55S +wSwmCqxnEj6+5HlAeOIC2LJnrerDEejlTpXH1HCEsTH0dd4EQ/sHixzNVt2MSHDU +fjhsmTl6kTO+RcIPoohuUSMrtrN9j/T4GxcrZqqwdzws/KMg1GYWvRxBJQHuO1yl +5ivhA4sjE8cmN2SbcgrYa3R7aK3XzrCmxx4DKHNYlQKBgQDWeS4BIVEIKwzjjQOC +RPhPg4Zo+0xYzceq4Q7ERn6mBojsLxNF91Z0Yx32rDpQziV1FESe0xmWneRCIjP8 +Ua31MU5FeH1GI7q0RyWD7XoUTI8io8cFdMiQEbRrYqARVrHun6NpBHIUG5Z5az8L +JWRP/P0QN90cgkgLnqwxFicHywKBgQC6Qx7ej5cPBBf/m8Aijmd8cUDqrmk6A4Zc +df8IjsfMdkv7H29qBip4KgNuThREaTq5fuCqgKvVKUgmudEFousfuWjzJsICBE9h +4DGFDUxPBYyUFf70PmlQ8e4avvNg1Cx5VgIt4M2IAsUmiAC/52y14R67u7MfQP7q +UWU3YitPOwKBgQCkq/A9n+YGnn9L68aI7Am3i2XVDzXEbWNj+V8MJpAxS40vwslK +jCjOPhgQgJZZ2p358fDp/W2FLn/Go1pE3jXxr8TIJEYTZ3V/26ybSefU1B+GWjeC +IfOoYl+jn9sE1QrTC7E8/dPVSoVTfpuuJCyMGdP38tyLeiB1A4R0P+0B1wKBgAtZ +79Wsdo5Jt5SyT0FL4G6rEEO9IViRwmx8HHDPEsoZI4RIZCfX/FqaZN8iDwYkS5nm +a5a4hMBW5bjGdkCbryydxhGbeRNaY+QZH6t2JgJi2jBkLsd/zjdKpzImFPr/sz4p +ybQ2ERCK6qzweOs5FVz4PUE/rSjocyCgmUSIzQ7lAoGAE+Vc2EeHPxFRZgVSFS6m +SEl5p5h8Wfr39+HKBSVd1P7QMHAFN11yXrGhIrRnAkI37chSvNtwLbTLicHuY127 +zEVAVXATvTZosErLp8WuNCxnb/1PxBVv1RMxJHm0ibjfRuWpDF+Gstn6ESQYKyPU +Z84XK248kL9fShkmRNRujGI= +-----END PRIVATE KEY----- diff --git a/crates/starknet_transaction_prover/src/server.rs b/crates/starknet_transaction_prover/src/server.rs index b0bad582471..5a48a244640 100644 --- a/crates/starknet_transaction_prover/src/server.rs +++ b/crates/starknet_transaction_prover/src/server.rs @@ -43,6 +43,8 @@ pub use health::{HealthLayer, HEALTH_PATH}; mod cors_test; #[cfg(test)] mod rpc_spec_test; +#[cfg(test)] +mod tls_test; #[cfg(test)] mod request_body_size_test; diff --git a/crates/starknet_transaction_prover/src/server/tls.rs b/crates/starknet_transaction_prover/src/server/tls.rs index d03e93650b0..54a6941c9c4 100644 --- a/crates/starknet_transaction_prover/src/server/tls.rs +++ b/crates/starknet_transaction_prover/src/server/tls.rs @@ -141,7 +141,7 @@ pub async fn start_tls_server( } /// Loads a certificate chain and private key from PEM files and builds a TLS acceptor. -fn load_tls_acceptor(cert_path: &Path, key_path: &Path) -> anyhow::Result { +pub(crate) fn load_tls_acceptor(cert_path: &Path, key_path: &Path) -> anyhow::Result { let cert_pem = std::fs::read(cert_path) .with_context(|| format!("Failed to read TLS certificate file: {}", cert_path.display()))?; let cert_chain: Vec> = diff --git a/crates/starknet_transaction_prover/src/server/tls_test.rs b/crates/starknet_transaction_prover/src/server/tls_test.rs new file mode 100644 index 00000000000..6b9742b3abb --- /dev/null +++ b/crates/starknet_transaction_prover/src/server/tls_test.rs @@ -0,0 +1,148 @@ +//! Integration tests for the TLS server bootstrap. +//! +//! Uses a self-signed certificate checked into `resources/test_tls/` (CN=localhost, valid for +//! 100 years). See `resources/test_tls/README.md` for the openssl regeneration command. + +use std::io::Write; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; + +use rstest::rstest; +use serde_json::Value; +use tempfile::NamedTempFile; + +use crate::server::mock_rpc::MockProvingRpc; +use crate::server::rpc_api::ProvingRpcServer; +use crate::server::rpc_impl::SPEC_VERSION; +use crate::server::tls::{load_tls_acceptor, start_tls_server}; + +fn ensure_crypto_provider() { + let _ = tokio_rustls::rustls::crypto::aws_lc_rs::default_provider().install_default(); +} + +fn test_cert_path() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test_tls/cert.pem") +} + +fn test_key_path() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test_tls/key.pem") +} + +fn write_pem_to_tempfile(pem_bytes: &[u8]) -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + file.write_all(pem_bytes).unwrap(); + file.flush().unwrap(); + file +} + +fn spec_version_request() -> Value { + serde_json::json!({ + "jsonrpc": "2.0", + "id": "1", + "method": "starknet_specVersion" + }) +} + +async fn start_test_tls_server() -> (SocketAddr, jsonrpsee::server::ServerHandle, Vec) { + let methods = MockProvingRpc::from_expected_json().into_rpc(); + let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); + + let (local_addr, handle) = start_tls_server( + addr, + &test_cert_path(), + &test_key_path(), + methods, + 10, // max_connections + 5 * 1024 * 1024, // max_request_body_size + None, // cors_layer + None, // ohttp_layer + ) + .await + .expect("Failed to start TLS server"); + + let cert_pem = std::fs::read(test_cert_path()).unwrap(); + (local_addr, handle, cert_pem) +} + +#[tokio::test] +async fn test_https_spec_version_succeeds() { + ensure_crypto_provider(); + let (addr, handle, cert_pem) = start_test_tls_server().await; + + let cert = reqwest::tls::Certificate::from_pem(&cert_pem).unwrap(); + let client = reqwest::Client::builder().add_root_certificate(cert).build().unwrap(); + + let response = client + .post(format!("https://localhost:{}", addr.port())) + .json(&spec_version_request()) + .send() + .await + .expect("HTTPS request failed"); + + assert_eq!(response.status(), 200); + let json: Value = response.json().await.unwrap(); + assert_eq!(json["result"].as_str().unwrap(), SPEC_VERSION); + + handle.stop().unwrap(); +} + +#[tokio::test] +async fn test_http_to_tls_server_fails() { + ensure_crypto_provider(); + let (addr, handle, _cert_pem) = start_test_tls_server().await; + + // Plain HTTP to a TLS server should fail (connection or protocol error). + let result = reqwest::Client::new() + .post(format!("http://localhost:{}", addr.port())) + .json(&spec_version_request()) + .send() + .await; + assert!(result.is_err(), "Expected HTTP to TLS server to fail, got: {result:?}"); + + handle.stop().unwrap(); +} + +/// How a given path argument is materialised for `load_tls_acceptor`. +enum PathMode { + /// Use the checked-in valid test fixture. + Valid, + /// Path to a file that does not exist. + Missing, + /// Path to a tempfile containing these bytes (returned alongside the path so the + /// `NamedTempFile` is kept alive for the call). + Junk(&'static [u8]), +} + +/// `PathMode::Junk` returns `Some(tempfile)` so the tempfile is dropped after the test, not before. +fn materialise(mode: PathMode, missing: &str, valid: PathBuf) -> (PathBuf, Option) { + match mode { + PathMode::Valid => (valid, None), + PathMode::Missing => (missing.into(), None), + PathMode::Junk(bytes) => { + let file = write_pem_to_tempfile(bytes); + (file.path().into(), Some(file)) + } + } +} + +/// Each case isolates one specific failure path by holding the other input valid, so a green test +/// proves `load_tls_acceptor` actually rejected on the named reason and not on something earlier. +#[rstest] +#[case::missing_cert(PathMode::Missing, PathMode::Valid)] +#[case::missing_key(PathMode::Valid, PathMode::Missing)] +#[case::invalid_cert_pem(PathMode::Junk(b"not a valid PEM cert"), PathMode::Valid)] +#[case::invalid_key_pem(PathMode::Valid, PathMode::Junk(b"not a valid PEM key"))] +fn test_load_tls_acceptor_failure(#[case] cert: PathMode, #[case] key: PathMode) { + let (cert_path, _cert_tmp) = materialise(cert, "/nonexistent/cert.pem", test_cert_path()); + let (key_path, _key_tmp) = materialise(key, "/nonexistent/key.pem", test_key_path()); + + assert!(load_tls_acceptor(&cert_path, &key_path).is_err()); +} + +#[test] +fn test_load_tls_acceptor_succeeds_for_valid_files() { + // `load_tls_acceptor` builds a rustls `ServerConfig`, which requires a process-level crypto + // provider. nextest runs each test in a fresh process, so install the provider here. + ensure_crypto_provider(); + load_tls_acceptor(&test_cert_path(), &test_key_path()).unwrap(); +}