diff --git a/.gitignore b/.gitignore index e48792b4..2434366e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ targets.json .idea/ logs .vscode/ +certs/ diff --git a/Cargo.lock b/Cargo.lock index 3e224fc4..22400244 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,7 +356,7 @@ dependencies = [ "proptest-derive", "rand 0.8.5", "ruint", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "sha3", "tiny-keccak", @@ -890,6 +890,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "archery" version = "0.4.0" @@ -1105,6 +1111,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-lc-rs" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.7.9" @@ -1240,6 +1269,28 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "axum-server" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" +dependencies = [ + "arc-swap", + "bytes", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -1288,6 +1339,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.100", + "which", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1506,6 +1580,7 @@ dependencies = [ "docker-compose-types", "eyre", "indexmap 2.8.0", + "rcgen", "serde_yaml", ] @@ -1535,6 +1610,7 @@ dependencies = [ "pbkdf2 0.12.2", "rand 0.9.0", "rayon", + "rcgen", "reqwest", "serde", "serde_json", @@ -1599,6 +1675,7 @@ dependencies = [ "alloy", "axum 0.8.1", "axum-extra", + "axum-server", "bimap", "blsful", "cb-common", @@ -1612,6 +1689,7 @@ dependencies = [ "prometheus", "prost", "rand 0.9.0", + "rustls", "thiserror 2.0.12", "tokio", "tonic", @@ -1632,6 +1710,7 @@ dependencies = [ "cb-signer", "eyre", "jsonwebtoken", + "rcgen", "reqwest", "serde_json", "tempfile", @@ -1648,15 +1727,32 @@ version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.40" @@ -1689,6 +1785,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.32" @@ -1729,6 +1836,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -2690,6 +2806,22 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2831,8 +2963,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -2999,6 +3133,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.3.1" @@ -3081,6 +3224,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -3425,6 +3569,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -3509,12 +3662,28 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.11" @@ -3694,6 +3863,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -3737,6 +3912,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4016,6 +4201,16 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4287,6 +4482,60 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +dependencies = [ + "bytes", + "getrandom 0.3.1", + "rand 0.9.0", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -4392,6 +4641,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -4478,7 +4740,10 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -4486,6 +4751,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower 0.5.2", "tower-service", @@ -4494,6 +4760,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", "windows-registry", ] @@ -4607,6 +4874,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4669,6 +4942,7 @@ version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -4692,6 +4966,9 @@ name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -4699,6 +4976,7 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -6211,6 +6489,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.8" @@ -6220,6 +6508,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "widestring" version = "1.2.0" @@ -6496,6 +6796,15 @@ dependencies = [ "tap", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 82ef3919..7559cca7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ alloy = { version = "0.12", features = [ async-trait = "0.1.80" axum = { version = "0.8.1", features = ["macros"] } axum-extra = { version = "0.10.0", features = ["typed-header"] } +axum-server = { version = "0.7.2", features = ["tls-rustls"] } base64 = "0.22.1" bimap = { version = "0.6.3", features = ["serde"] } blsful = "2.5" @@ -55,7 +56,9 @@ prometheus = "0.13.4" prost = "0.13.4" rand = { version = "0.9", features = ["os_rng"] } rayon = "1.10.0" -reqwest = { version = "0.12.4", features = ["json", "stream"] } +rcgen = "0.13.2" +reqwest = { version = "0.12.4", features = ["json", "rustls-tls", "stream"] } +rustls = "0.23.23" serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.117" serde_yaml = "0.9.33" diff --git a/config.example.toml b/config.example.toml index 4aaf62d9..67085409 100644 --- a/config.example.toml +++ b/config.example.toml @@ -169,6 +169,14 @@ jwt_auth_fail_limit = 3 # OPTIONAL, DEFAULT: 300 jwt_auth_fail_timeout_seconds = 300 +# [signer.tls_mode] +# How to use TLS for the Signer's HTTP server; two modes are supported: +# - type = "insecure": disable TLS, so the server runs in HTTP mode (not recommended for production). +# - type = "certificate": Use TLS. Include a property named "path" below this with the provided path; `path` should be a directory containing `cert.pem` and `key.pem` files to use. If they don't exist, they'll be automatically generated in self-signed mode. +# OPTIONAL, DEFAULT: +# type = "certificate" +# path = "./certs" + # For Remote signer: # [signer.remote] # URL of the Web3Signer instance diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 2acc6a7b..ac07c46c 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,4 +11,5 @@ clap.workspace = true docker-compose-types.workspace = true eyre.workspace = true indexmap.workspace = true +rcgen.workspace = true serde_yaml.workspace = true diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 0e1193e7..d06231ae 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -6,15 +6,18 @@ use std::{ use cb_common::{ config::{ - CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, ADMIN_JWT_ENV, - CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, - DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, - DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, - MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, - PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, - PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, - SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_ENDPOINT_ENV, SIGNER_KEYS_ENV, - SIGNER_MODULE_NAME, SIGNER_PORT_DEFAULT, SIGNER_URL_ENV, + CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, TlsMode, + ADMIN_JWT_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, + DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, + DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, + LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, + PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, + PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, + SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, + SIGNER_DIR_SECRETS_ENV, SIGNER_ENDPOINT_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, + SIGNER_PORT_DEFAULT, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, SIGNER_TLS_KEY_NAME, + SIGNER_URL_ENV, }, pbs::{BUILDER_V1_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, @@ -71,13 +74,19 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // targets to pass to prometheus let mut targets = Vec::new(); + let using_tls = cb_config + .signer + .as_ref() + .is_some_and(|signer_config| matches!(signer_config.tls_mode, TlsMode::Certificate(_))); + let signer_http_prefix = if using_tls { "https" } else { "http" }; + // address for signer API communication let signer_port = cb_config.signer.as_ref().map(|s| s.port).unwrap_or(SIGNER_PORT_DEFAULT); let signer_server = if let Some(SignerConfig { inner: SignerType::Remote { url }, .. }) = &cb_config.signer { url.to_string() } else { - format!("http://cb_signer:{signer_port}") + format!("{signer_http_prefix}://cb_signer:{signer_port}") }; let mut warnings = Vec::new(); @@ -87,6 +96,19 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re modules.iter().any(|module| matches!(module.kind, ModuleKind::Commit)) }); + // If signer config is not set, certs_path doesn't really matter + let certs_path = cb_config + .signer + .as_ref() + .map(|config| match &config.tls_mode { + TlsMode::Insecure => { + warnings.push("Signer TLS mode is set to Insecure, using HTTP instead of HTTPS for signer communication".to_string()); + None + }, + TlsMode::Certificate(path) => Some(path), + }) + .unwrap_or_default(); + // setup modules if let Some(modules_config) = cb_config.modules { for module in modules_config { @@ -107,6 +129,13 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re get_env_interp(MODULE_JWT_ENV, &jwt_name), get_env_val(SIGNER_URL_ENV, &signer_server), ]); + if using_tls { + let env_val = get_env_val( + SIGNER_TLS_CERTIFICATES_PATH_ENV, + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + ); + module_envs.insert(env_val.0, env_val.1); + } // Pass on the env variables if let Some(envs) = module.env { @@ -154,6 +183,9 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut module_volumes = vec![config_volume.clone()]; module_volumes.extend(chain_spec_volume.clone()); module_volumes.extend(get_log_volume(&cb_config.logs, &module.id)); + if let Some(certs_path) = certs_path { + module_volumes.push(create_cert_binding(certs_path)); + } // depends_on let mut module_dependencies = IndexMap::new(); @@ -237,6 +269,14 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // volumes pbs_volumes.extend(chain_spec_volume.clone()); pbs_volumes.extend(get_log_volume(&cb_config.logs, PBS_MODULE_NAME)); + if needs_signer_module { + if let Some(certs_path) = certs_path { + pbs_volumes.push(create_cert_binding(certs_path)); + let (key, val) = + get_env_val(SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT); + pbs_envs.insert(key, val); + } + } let pbs_service = Service { container_name: Some("cb_pbs".to_owned()), @@ -263,7 +303,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // setup signer service if needs_signer_module { - let Some(signer_config) = cb_config.signer else { + let Some(signer_config) = cb_config.signer.clone() else { panic!("Signer module required but no signer config provided"); }; @@ -273,6 +313,10 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_same(JWTS_ENV), get_env_same(ADMIN_JWT_ENV), + get_env_val( + SIGNER_TLS_CERTIFICATES_PATH_ENV, + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + ), ]); // Bind the signer API to 0.0.0.0 @@ -376,6 +420,11 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re volumes.extend(get_log_volume(&cb_config.logs, SIGNER_MODULE_NAME)); + // Add TLS support if needed + if let Some(certs_path) = certs_path { + add_tls_certs_volume(&mut volumes, certs_path)? + } + // networks let signer_networks = vec![SIGNER_NETWORK.to_owned()]; @@ -388,7 +437,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re environment: Environment::KvPair(signer_envs), healthcheck: Some(Healthcheck { test: Some(HealthcheckTest::Single(format!( - "curl -f http://localhost:{signer_port}/status" + "curl -k -f {signer_server}/status" ))), interval: Some("30s".into()), timeout: Some("5s".into()), @@ -483,6 +532,11 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re None => {} } + // Add TLS support if needed + if let Some(certs_path) = certs_path { + add_tls_certs_volume(&mut volumes, certs_path)? + } + // networks let signer_networks = vec![SIGNER_NETWORK.to_owned()]; @@ -495,7 +549,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re environment: Environment::KvPair(signer_envs), healthcheck: Some(Healthcheck { test: Some(HealthcheckTest::Single(format!( - "curl -f http://localhost:{signer_port}/status" + "curl -k -f {signer_server}/status" ))), interval: Some("30s".into()), timeout: Some("5s".into()), @@ -626,3 +680,38 @@ fn get_log_volume(config: &LogsSettings, module_id: &str) -> Option { fn format_comma_separated(map: &IndexMap) -> String { map.iter().map(|(k, v)| format!("{}={}", k, v)).collect::>().join(",") } + +fn create_cert_binding(certs_path: &Path) -> Volumes { + Volumes::Simple(format!( + "{}:{}/{}:ro", + certs_path.join(SIGNER_TLS_CERTIFICATE_NAME).display(), + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + SIGNER_TLS_CERTIFICATE_NAME + )) +} + +/// Adds the TLS cert and key bindings to the provided volumes list +fn add_tls_certs_volume(volumes: &mut Vec, certs_path: &Path) -> Result<()> { + if !certs_path.try_exists()? { + std::fs::create_dir(certs_path)?; + } + + if !certs_path.join(SIGNER_TLS_CERTIFICATE_NAME).try_exists()? || + !certs_path.join(SIGNER_TLS_KEY_NAME).try_exists()? + { + return Err(eyre::eyre!( + "Signer TLS certificate or key not found at {}, please provide a valid certificate and key or create them", + certs_path.display() + )); + } + + volumes.push(create_cert_binding(certs_path)); + volumes.push(Volumes::Simple(format!( + "{}:{}/{}:ro", + certs_path.join(SIGNER_TLS_KEY_NAME).display(), + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + SIGNER_TLS_KEY_NAME + ))); + + Ok(()) +} diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 3cf20988..a2fa792a 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -29,6 +29,7 @@ lh_types.workspace = true pbkdf2.workspace = true rand.workspace = true rayon.workspace = true +rcgen.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index e7466d6c..9d6b87ae 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -1,8 +1,11 @@ -use std::time::{Duration, Instant}; +use std::path::PathBuf; use alloy::primitives::Address; use eyre::WrapErr; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use reqwest::{ + header::{HeaderMap, HeaderValue, AUTHORIZATION}, + Certificate, +}; use serde::{Deserialize, Serialize}; use url::Url; @@ -22,7 +25,6 @@ use crate::{ }, response::{BlsSignResponse, EcdsaSignResponse}, }, - constants::SIGNER_JWT_EXPIRATION, types::{BlsPublicKey, Jwt, ModuleId}, utils::create_jwt, DEFAULT_REQUEST_TIMEOUT, @@ -34,15 +36,32 @@ pub struct SignerClient { /// Url endpoint of the Signer Module url: Url, client: reqwest::Client, - last_jwt_refresh: Instant, module_id: ModuleId, jwt_secret: Jwt, } impl SignerClient { /// Create a new SignerClient - pub fn new(signer_server_url: Url, jwt_secret: Jwt, module_id: ModuleId) -> eyre::Result { - let jwt = create_jwt(&module_id, &jwt_secret, None)?; + pub fn new( + signer_server_url: Url, + cert_path: Option, + jwt_secret: Jwt, + module_id: ModuleId, + ) -> eyre::Result { + let mut builder = reqwest::Client::builder().timeout(DEFAULT_REQUEST_TIMEOUT); + + // If a certificate path is provided, use it + if let Some(cert_path) = cert_path { + builder = builder + .use_rustls_tls() + .add_root_certificate(Certificate::from_pem(&std::fs::read(cert_path)?)?); + } + + Ok(Self { url: signer_server_url, client: builder.build()?, module_id, jwt_secret }) + } + + fn refresh_jwt(&mut self) -> Result<(), SignerClientError> { + let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?; let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; @@ -51,37 +70,11 @@ impl SignerClient { let mut headers = HeaderMap::new(); headers.insert(AUTHORIZATION, auth_value); - let client = reqwest::Client::builder() + self.client = reqwest::Client::builder() .timeout(DEFAULT_REQUEST_TIMEOUT) .default_headers(headers) .build()?; - Ok(Self { - url: signer_server_url, - client, - last_jwt_refresh: Instant::now(), - module_id, - jwt_secret, - }) - } - - fn refresh_jwt(&mut self) -> Result<(), SignerClientError> { - if self.last_jwt_refresh.elapsed() > Duration::from_secs(SIGNER_JWT_EXPIRATION) { - let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?; - - let mut auth_value = - HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; - auth_value.set_sensitive(true); - - let mut headers = HeaderMap::new(); - headers.insert(AUTHORIZATION, auth_value); - - self.client = reqwest::Client::builder() - .timeout(DEFAULT_REQUEST_TIMEOUT) - .default_headers(headers) - .build()?; - } - Ok(()) } diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index b3ba63e9..a4c54f8c 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -46,6 +46,12 @@ pub const SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT: u32 = 5 * 60; pub const JWTS_ENV: &str = "CB_JWTS"; pub const ADMIN_JWT_ENV: &str = "CB_SIGNER_ADMIN_JWT"; +/// Path to the certificates folder where the cert.pem and key.pem files are +/// stored/generated +pub const SIGNER_TLS_CERTIFICATES_PATH_ENV: &str = "CB_SIGNER_TLS_CERTIFICATES"; +pub const SIGNER_TLS_CERTIFICATES_PATH_DEFAULT: &str = "/certs"; +pub const SIGNER_TLS_CERTIFICATE_NAME: &str = "cert.pem"; +pub const SIGNER_TLS_KEY_NAME: &str = "key.pem"; /// Path to json file with plaintext keys (testing only) pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_LOADER_FILE"; pub const SIGNER_DEFAULT: &str = "/keys.json"; diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index ff4f1dc0..b2f30ae8 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, path::PathBuf}; use alloy::primitives::B256; use eyre::{ContextCompat, Result}; @@ -11,6 +11,7 @@ use crate::{ constants::{CONFIG_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, SIGNER_URL_ENV}, load_env_var, utils::load_file_from_env, + SignerConfig, TlsMode, SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, }, types::{Chain, Jwt, ModuleId}, }; @@ -82,6 +83,7 @@ pub fn load_commit_module_config() -> Result { chain: Chain, modules: Vec>, + signer: SignerConfig, } // load module config including the extra data (if any) @@ -104,7 +106,16 @@ pub fn load_commit_module_config() -> Result None, + TlsMode::Certificate(path) => Some( + load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV) + .map(PathBuf::from) + .unwrap_or(path) + .join(SIGNER_TLS_CERTIFICATE_NAME), + ), + }; + let signer_client = SignerClient::new(signer_server_url, certs_path, module_jwt, module_id)?; Ok(StartCommitModuleConfig { id: module_config.static_config.id, diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index 1889223a..b7187cec 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -3,6 +3,7 @@ use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddr}, + path::PathBuf, sync::Arc, }; @@ -21,8 +22,9 @@ use super::{ use crate::{ commit::client::SignerClient, config::{ - load_env_var, load_file_from_env, PbsMuxes, CONFIG_ENV, MODULE_JWT_ENV, PBS_MODULE_NAME, - SIGNER_URL_ENV, + load_env_var, load_file_from_env, PbsMuxes, SignerConfig, TlsMode, CONFIG_ENV, + MODULE_JWT_ENV, PBS_MODULE_NAME, SIGNER_TLS_CERTIFICATES_PATH_ENV, + SIGNER_TLS_CERTIFICATE_NAME, SIGNER_URL_ENV, }, pbs::{ DefaultTimeout, RelayClient, RelayEntry, DEFAULT_PBS_PORT, LATE_IN_SLOT_TIME_MS, @@ -298,6 +300,7 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC chain: Chain, relays: Vec, pbs: CustomPbsConfig, + signer: SignerConfig, muxes: Option, } @@ -350,8 +353,18 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC // if custom pbs requires a signer client, load jwt let module_jwt = Jwt(load_env_var(MODULE_JWT_ENV)?); let signer_server_url = load_env_var(SIGNER_URL_ENV)?.parse()?; + let certs_path = match cb_config.signer.tls_mode { + TlsMode::Insecure => None, + TlsMode::Certificate(path) => Some( + load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV) + .map(PathBuf::from) + .unwrap_or(path) + .join(SIGNER_TLS_CERTIFICATE_NAME), + ), + }; Some(SignerClient::new( signer_server_url, + certs_path, module_jwt, ModuleId(PBS_MODULE_NAME.to_string()), )?) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index a397d696..77eb425a 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -15,7 +15,8 @@ use super::{ load_optional_env_var, utils::load_env_var, CommitBoostConfig, SIGNER_ENDPOINT_ENV, SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_ENV, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV, - SIGNER_PORT_DEFAULT, + SIGNER_PORT_DEFAULT, SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, + SIGNER_TLS_KEY_NAME, }; use crate::{ config::{ @@ -57,6 +58,17 @@ impl ModuleSigningConfig { } } +/// Mode to use for TLS support when starting the signer service +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "type", content = "path", rename_all = "snake_case")] +pub enum TlsMode { + /// Don't use TLS (regular HTTP) + Insecure, + + /// Use TLS with a certificate and key file in the provided directory + Certificate(PathBuf), +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub struct SignerConfig { @@ -80,6 +92,12 @@ pub struct SignerConfig { #[serde(default = "default_u32::")] pub jwt_auth_fail_timeout_seconds: u32, + /// Mode to use for TLS support. + /// If using Certificate mode, this must include a path to the TLS + /// certificates directory (with a `cert.pem` and a `key.pem` file). + #[serde(default = "default_tls_mode")] + pub tls_mode: TlsMode, + /// Inner type-specific configuration #[serde(flatten)] pub inner: SignerType, @@ -106,6 +124,11 @@ fn default_signer_image() -> String { SIGNER_IMAGE_DEFAULT.to_string() } +fn default_tls_mode() -> TlsMode { + TlsMode::Insecure // To make the default use TLS, do + // TlsMode::Certificate(PathBuf::from("./certs")) +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub struct DirkHostConfig { @@ -169,6 +192,7 @@ pub struct StartSignerConfig { pub jwt_auth_fail_limit: u32, pub jwt_auth_fail_timeout_seconds: u32, pub dirk: Option, + pub tls_certificates: Option<(Vec, Vec)>, } impl StartSignerConfig { @@ -208,6 +232,20 @@ impl StartSignerConfig { signer_config.jwt_auth_fail_timeout_seconds }; + // Load the TLS certificates if requested, generating self-signed ones if + // necessary + let tls_certificates = match signer_config.tls_mode { + TlsMode::Insecure => None, + TlsMode::Certificate(path) => { + let certs_path = load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV) + .map(PathBuf::from) + .unwrap_or(path); + let cert_path = certs_path.join(SIGNER_TLS_CERTIFICATE_NAME); + let key_path = certs_path.join(SIGNER_TLS_KEY_NAME); + Some((std::fs::read(cert_path)?, std::fs::read(key_path)?)) + } + }; + match signer_config.inner { SignerType::Local { loader, store, .. } => Ok(StartSignerConfig { chain: config.chain, @@ -219,6 +257,7 @@ impl StartSignerConfig { jwt_auth_fail_timeout_seconds, store, dirk: None, + tls_certificates, }), SignerType::Dirk { @@ -264,6 +303,7 @@ impl StartSignerConfig { None => None, }, }), + tls_certificates, }) } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 569797ac..7c6e63fa 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -9,6 +9,7 @@ version.workspace = true alloy.workspace = true axum.workspace = true axum-extra.workspace = true +axum-server.workspace = true bimap.workspace = true blsful.workspace = true cb-common.workspace = true @@ -22,6 +23,7 @@ parking_lot.workspace = true prometheus.workspace = true prost.workspace = true rand.workspace = true +rustls.workspace = true thiserror.workspace = true tokio.workspace = true tonic.workspace = true diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index e471a6fb..2d510eeb 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -16,6 +16,7 @@ use axum::{ Extension, Json, }; use axum_extra::TypedHeader; +use axum_server::tls_rustls::RustlsConfig; use cb_common::{ commit::{ constants::{ @@ -38,7 +39,8 @@ use cb_metrics::provider::MetricsProvider; use eyre::Context; use headers::{authorization::Bearer, Authorization}; use parking_lot::RwLock as ParkingRwLock; -use tokio::{net::TcpListener, sync::RwLock}; +use rustls::crypto::{aws_lc_rs, CryptoProvider}; +use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; use uuid::Uuid; @@ -142,14 +144,28 @@ impl SigningService { .route_layer(middleware::from_fn(log_request)) .route(STATUS_PATH, get(handle_status)); - let listener = TcpListener::bind(config.endpoint).await?; + if CryptoProvider::get_default().is_none() { + aws_lc_rs::default_provider() + .install_default() + .map_err(|_| eyre::eyre!("Failed to install TLS provider"))?; + } - axum::serve( - listener, - signer_app.merge(admin_app).into_make_service_with_connect_info::(), - ) - .await - .wrap_err("signer server exited") + let server_result = if let Some(tls_config) = config.tls_certificates { + let tls_config = RustlsConfig::from_pem(tls_config.0, tls_config.1).await?; + axum_server::bind_rustls(config.endpoint, tls_config) + .serve( + signer_app.merge(admin_app).into_make_service_with_connect_info::(), + ) + .await + } else { + warn!("Running in insecure HTTP mode, no TLS certificates provided"); + axum_server::bind(config.endpoint) + .serve( + signer_app.merge(admin_app).into_make_service_with_connect_info::(), + ) + .await + }; + server_result.wrap_err("signer service exited") } fn init_metrics(network: Chain) -> eyre::Result<()> { diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index bb963690..acff09e7 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -358,6 +358,37 @@ Delegation signatures will be stored in files with the format `/deleg A full example of a config file with Dirk can be found [here](https://github.com/Commit-Boost/commit-boost-client/blob/main/examples/configs/dirk_signer.toml). +### TLS + +By default, the Signer service runs in **insecure** mode, so its API service uses HTTP without any TLS encryption. This is sufficient for testing or if you're running locally within your machine's isolated Docker network and only intend to access it within the confines of your machine. However, for larger production setups, it's recommended to enable TLS - especially for traffic that spans across multiple machines. + +The Signer service in TLS mode supports **TLS 1.2** and **TLS 1.3**. Older protocol versions are not supported. + +To enable TLS, you must first create a **certificate / key pair**. We **strongly advise** using a well-known Certificate Authority to create and sign the certificate, such as [Let's Encrypt](https://letsencrypt.org/getting-started/) (a free service) or [Bluehost](https://www.bluehost.com/help/article/how-to-set-up-an-ssl-certificate-for-website-security) (free but requires an account). We do not recommend using a self-signed ceriticate / key pair for production environments. + +When configuring TLS support, the Signer service expects a single folder (which you can specify) that contains the following two files: +- `cert.pem`: The SSL certificate file signed by a certificate authority, in PEM format +- `key.pem`: The private key corresponding to `cert.pem` that will be used for signing TLS traffic, in PEM format + +Specifying it is done within Commit-Boost's configuration file using the `[signer.tls_mode]` table as follows: + +```toml +[pbs] +... +with_signer = true + +[signer] +port = 20000 +... + +[signer.tls_mode] +type = "certificate" +path = "path/to/your/cert/folder" +``` + +Where `path` is the aforementioned folder. It defaults to `./certs` but can be replaced with whichever directory your certificate and private key file reside in, as long as they're readable by the Signer service (or its Docker container, if using Docker). + + ## Custom module We currently provide a test module that needs to be built locally. To build the module run: diff --git a/docs/docs/get_started/running/binary.md b/docs/docs/get_started/running/binary.md index 0dfa9399..5d7265bf 100644 --- a/docs/docs/get_started/running/binary.md +++ b/docs/docs/get_started/running/binary.md @@ -30,6 +30,7 @@ Modules need some environment variables to work correctly. - `CB_SIGNER_ADMIN_JWT`: secret to use for admin JWT. - `CB_SIGNER_ENDPOINT`: optional, override to specify the `IP:port` endpoint to bind the signer server to. +- `CB_SIGNER_TLS_CERTIFICATES`: path to the TLS certificates for the server. - For loading keys we currently support: - `CB_SIGNER_LOADER_FILE`: path to a `.json` with plaintext keys (for testing purposes only). - `CB_SIGNER_LOADER_FORMAT`, `CB_SIGNER_LOADER_KEYS_DIR` and `CB_SIGNER_LOADER_SECRETS_DIR`: paths to the `keys` and `secrets` directories or files (ERC-2335 style keystores, see [Signer config](../configuration/#signer-module) for more info). diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 573cfa20..6cd2b829 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -12,6 +12,7 @@ cb-pbs.workspace = true cb-signer.workspace = true eyre.workspace = true jsonwebtoken.workspace = true +rcgen.workspace = true reqwest.workspace = true serde_json.workspace = true tempfile.workspace = true diff --git a/tests/src/signer_service.rs b/tests/src/signer_service.rs index e4cdd4e6..10e158fa 100644 --- a/tests/src/signer_service.rs +++ b/tests/src/signer_service.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, time::Duration}; use cb_common::{ - commit::request::GetPubkeysResponse, + commit::{constants::STATUS_PATH, request::GetPubkeysResponse}, config::{ModuleSigningConfig, StartSignerConfig}, signer::{SignerLoader, ValidatorKeysFormat}, types::{Chain, ModuleId}, @@ -9,7 +9,7 @@ use cb_common::{ }; use cb_signer::service::SigningService; use eyre::Result; -use reqwest::{Response, StatusCode}; +use reqwest::{Certificate, Response, StatusCode}; use tracing::info; use crate::utils::{get_signer_config, get_start_signer_config}; @@ -20,6 +20,7 @@ pub async fn start_server( port: u16, mod_signing_configs: &HashMap, admin_secret: String, + use_tls: bool, ) -> Result { let chain = Chain::Hoodi; @@ -29,7 +30,7 @@ pub async fn start_server( secrets_path: "data/keystores/secrets".into(), format: ValidatorKeysFormat::Lighthouse, }; - let mut config = get_signer_config(loader); + let mut config = get_signer_config(loader, use_tls); config.port = port; config.jwt_auth_fail_limit = 3; // Set a low fail limit for testing config.jwt_auth_fail_timeout_seconds = 3; // Set a short timeout for testing @@ -38,15 +39,37 @@ pub async fn start_server( // Run the Signer let server_handle = tokio::spawn(SigningService::run(start_config.clone())); - // Make sure the server is running - tokio::time::sleep(Duration::from_millis(100)).await; - if server_handle.is_finished() { - return Err(eyre::eyre!( - "Signer service failed to start: {}", - server_handle.await.unwrap_err() - )); + // Wait for the server to start + let (url, client) = match start_config.tls_certificates { + Some(ref certificates) => { + let url = format!("https://{}{}", start_config.endpoint, STATUS_PATH); + let client = reqwest::Client::builder() + .add_root_certificate(Certificate::from_pem(&certificates.0)?) + .build()?; + (url, client) + } + None => { + let url = format!("http://{}{}", start_config.endpoint, STATUS_PATH); + (url, reqwest::Client::new()) + } + }; + + let sleep_duration = Duration::from_millis(100); + for i in 0..100 { + // 10 second max wait + if i > 0 { + tokio::time::sleep(sleep_duration).await; + } + match client.get(&url).send().await { + Ok(_) => { + return Ok(start_config); + } + Err(e) => { + info!("Waiting for signer service to start: {}", e); + } + } } - Ok(start_config) + Err(eyre::eyre!("Signer service failed to start: {}", server_handle.await.unwrap_err())) } // Verifies that the pubkeys returned by the server match the pubkeys in the diff --git a/tests/src/utils.rs b/tests/src/utils.rs index 532496c4..55be8aa3 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddr}, + path::PathBuf, sync::{Arc, Once}, }; @@ -9,7 +10,7 @@ use cb_common::{ config::{ CommitBoostConfig, LogsSettings, ModuleKind, ModuleSigningConfig, PbsConfig, PbsModuleConfig, RelayConfig, SignerConfig, SignerType, StartSignerConfig, - StaticModuleConfig, StaticPbsConfig, SIGNER_IMAGE_DEFAULT, + StaticModuleConfig, StaticPbsConfig, TlsMode, SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_PORT_DEFAULT, }, @@ -19,6 +20,7 @@ use cb_common::{ utils::default_host, }; use eyre::Result; +use rcgen::generate_simple_self_signed; pub fn get_local_address(port: u16) -> String { format!("http://0.0.0.0:{port}") @@ -119,7 +121,7 @@ pub fn to_pbs_config( } } -pub fn get_signer_config(loader: SignerLoader) -> SignerConfig { +pub fn get_signer_config(loader: SignerLoader, tls: bool) -> SignerConfig { SignerConfig { host: default_host(), port: SIGNER_PORT_DEFAULT, @@ -127,6 +129,7 @@ pub fn get_signer_config(loader: SignerLoader) -> SignerConfig { jwt_auth_fail_limit: SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, jwt_auth_fail_timeout_seconds: SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, inner: SignerType::Local { loader, store: None }, + tls_mode: if tls { TlsMode::Certificate(PathBuf::new()) } else { TlsMode::Insecure }, } } @@ -136,6 +139,20 @@ pub fn get_start_signer_config( mod_signing_configs: &HashMap, admin_secret: String, ) -> StartSignerConfig { + let tls_certificates = match signer_config.tls_mode { + TlsMode::Insecure => None, + TlsMode::Certificate(_) => Some( + generate_simple_self_signed(vec![signer_config.host.to_string()]) + .map(|x| { + ( + x.cert.pem().as_bytes().to_vec(), + x.key_pair.serialize_pem().as_bytes().to_vec(), + ) + }) + .expect("Failed to generate TLS certificate"), + ), + }; + match signer_config.inner { SignerType::Local { loader, .. } => StartSignerConfig { chain, @@ -147,6 +164,7 @@ pub fn get_start_signer_config( jwt_auth_fail_limit: signer_config.jwt_auth_fail_limit, jwt_auth_fail_timeout_seconds: signer_config.jwt_auth_fail_timeout_seconds, dirk: None, + tls_certificates, }, _ => panic!("Only local signers are supported in tests"), } diff --git a/tests/tests/signer_jwt_auth.rs b/tests/tests/signer_jwt_auth.rs index a4510af0..c18f5ea6 100644 --- a/tests/tests/signer_jwt_auth.rs +++ b/tests/tests/signer_jwt_auth.rs @@ -41,7 +41,7 @@ async fn test_signer_jwt_auth_success() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20100, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20100, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run a pubkeys request @@ -61,7 +61,7 @@ async fn test_signer_jwt_auth_fail() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20101, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20101, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; // Run a pubkeys request - this should fail due to invalid JWT let jwt = create_jwt(&module_id, "incorrect secret", None)?; @@ -82,7 +82,7 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20102, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20102, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let mod_cfg = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run as many pubkeys requests as the fail limit @@ -116,7 +116,7 @@ async fn test_signer_revoked_jwt_fail() -> Result<()> { let admin_secret = ADMIN_SECRET.to_string(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20400, &mod_cfgs, admin_secret.clone()).await?; + let start_config = start_server(20400, &mod_cfgs, admin_secret.clone(), false).await?; // Run as many pubkeys requests as the fail limit let jwt = create_jwt(&module_id, JWT_SECRET, None)?; @@ -149,7 +149,7 @@ async fn test_signer_only_admin_can_revoke() -> Result<()> { let admin_secret = ADMIN_SECRET.to_string(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20500, &mod_cfgs, admin_secret.clone()).await?; + let start_config = start_server(20500, &mod_cfgs, admin_secret.clone(), false).await?; let revoke_body = RevokeModuleRequest { module_id: ModuleId(JWT_MODULE.to_string()) }; let body_bytes = serde_json::to_vec(&revoke_body)?; diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index fce8eaf7..c1325d17 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -52,7 +52,7 @@ async fn test_signer_sign_request_good() -> Result<()> { setup_test_env(); let module_id = ModuleId(MODULE_ID_1.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20200, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20200, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Send a signing request @@ -90,7 +90,7 @@ async fn test_signer_sign_request_different_module() -> Result<()> { setup_test_env(); let module_id = ModuleId(MODULE_ID_2.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20201, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20201, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); // Send a signing request @@ -131,7 +131,7 @@ async fn test_signer_sign_request_incorrect_hash() -> Result<()> { setup_test_env(); let module_id = ModuleId(MODULE_ID_2.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20202, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20202, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); // Send a signing request @@ -162,7 +162,7 @@ async fn test_signer_sign_request_missing_hash() -> Result<()> { setup_test_env(); let module_id = ModuleId(MODULE_ID_2.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20203, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20203, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); // Send a signing request diff --git a/tests/tests/signer_tls.rs b/tests/tests/signer_tls.rs new file mode 100644 index 00000000..787312df --- /dev/null +++ b/tests/tests/signer_tls.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use alloy::primitives::b256; +use cb_common::{ + commit::constants::GET_PUBKEYS_PATH, + config::{load_module_signing_configs, ModuleSigningConfig}, + types::ModuleId, + utils::create_jwt, +}; +use cb_tests::{ + signer_service::{start_server, verify_pubkeys}, + utils::{self, setup_test_env}, +}; +use eyre::{bail, Result}; +use reqwest::Certificate; + +const JWT_MODULE: &str = "test-module"; +const JWT_SECRET: &str = "test-jwt-secret"; +const ADMIN_SECRET: &str = "test-admin-secret"; + +async fn create_mod_signing_configs() -> HashMap { + let mut cfg = + utils::get_commit_boost_config(utils::get_pbs_static_config(utils::get_pbs_config(0))); + + let module_id = ModuleId(JWT_MODULE.to_string()); + let signing_id = b256!("0101010101010101010101010101010101010101010101010101010101010101"); + + cfg.modules = Some(vec![utils::create_module_config(module_id.clone(), signing_id)]); + + let jwts = HashMap::from([(module_id.clone(), JWT_SECRET.to_string())]); + + load_module_signing_configs(&cfg, &jwts).unwrap() +} + +#[tokio::test] +async fn test_signer_tls() -> Result<()> { + setup_test_env(); + let module_id = ModuleId(JWT_MODULE.to_string()); + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20100, &mod_cfgs, ADMIN_SECRET.to_string(), true).await?; + let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); + + // Run a pubkeys request + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, None)?; + let cert = match start_config.tls_certificates { + Some(ref certificates) => &certificates.0, + None => bail!("TLS certificates not found in start config"), + }; + let client = + reqwest::Client::builder().add_root_certificate(Certificate::from_pem(cert)?).build()?; + let url = format!("https://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); + let response = client.get(&url).bearer_auth(&jwt).send().await?; + + // Verify the expected pubkeys are returned + verify_pubkeys(response).await?; + + Ok(()) +}