diff --git a/.gitignore b/.gitignore index b8eaa77a..6e9851da 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ cb.docker-compose.yml targets.json .idea/ logs -.vscode/ \ No newline at end of file +.vscode/ +certs/ diff --git a/Cargo.lock b/Cargo.lock index 5ebc811a..0812fca1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,7 +341,7 @@ dependencies = [ "proptest", "rand 0.8.5", "ruint", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "sha3", "tiny-keccak", @@ -875,6 +875,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 = "ark-ff" version = "0.3.0" @@ -1081,6 +1087,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" @@ -1216,6 +1245,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" @@ -1264,6 +1315,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "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" @@ -1471,6 +1545,7 @@ dependencies = [ "docker-compose-types", "eyre", "indexmap 2.8.0", + "rcgen", "serde_yaml", ] @@ -1560,6 +1635,7 @@ dependencies = [ "alloy", "axum 0.8.1", "axum-extra", + "axum-server", "bimap", "blsful", "cb-common", @@ -1572,6 +1648,7 @@ dependencies = [ "prometheus", "prost", "rand 0.9.0", + "rustls", "thiserror 2.0.12", "tokio", "tonic", @@ -1605,15 +1682,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" @@ -1646,6 +1740,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" @@ -1686,6 +1791,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" @@ -2448,6 +2562,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" @@ -2589,8 +2719,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", ] @@ -2744,6 +2876,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" @@ -2826,6 +2967,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -3161,6 +3303,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" @@ -3226,12 +3377,28 @@ dependencies = [ "spin 0.9.8", ] +[[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" @@ -3337,6 +3504,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" @@ -3380,6 +3553,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" @@ -3659,6 +3842,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" @@ -3919,6 +4112,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 0.17.14", + "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" @@ -4004,6 +4251,19 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring 0.17.14", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -4090,7 +4350,10 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -4098,6 +4361,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower 0.5.2", "tower-service", @@ -4106,6 +4370,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", "windows-registry", ] @@ -4196,6 +4461,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" @@ -4258,6 +4529,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 0.17.14", @@ -4281,6 +4553,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" @@ -4288,6 +4563,7 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ + "aws-lc-rs", "ring 0.17.14", "rustls-pki-types", "untrusted 0.9.0", @@ -5716,6 +5992,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" @@ -5725,6 +6011,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" @@ -6001,6 +6299,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 aef26a94..4d598e01 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" @@ -44,13 +45,16 @@ eyre = "0.6.12" futures = "0.3.30" headers = "0.4.0" indexmap = "2.2.6" +jsonwebtoken = { version = "9.3.1", default-features = false } lazy_static = "1.5.0" parking_lot = "0.12.3" pbkdf2 = "0.12.2" prometheus = "0.13.4" prost = "0.13.4" rand = { version = "0.9", features = ["os_rng"] } -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" @@ -70,4 +74,3 @@ typenum = "1.17.0" unicode-normalization = "0.1.24" url = { version = "2.5.0", features = ["serde"] } uuid = { version = "1.8.0", features = ["fast-rng", "serde", "v4"] } -jsonwebtoken = { version = "9.3.1", default-features = false } diff --git a/config.example.toml b/config.example.toml index ae69c3ff..b5953e5c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -148,6 +148,9 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # Docker image to use for the Signer module. # OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest # docker_image = "ghcr.io/commit-boost/signer:latest" +# Path to the TLS certificates for the signer server +# OPTION, DEFAULT: ./certs +# tls_certificates = "./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 4453f597..afaf08c7 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -15,7 +15,9 @@ use cb_common::{ 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_JWT_SECRET_ENV, SIGNER_KEYS_ENV, - SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV, + SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_TLS_CERTIFICATES_DEFAULT, + SIGNER_TLS_CERTIFICATES_ENV, SIGNER_TLS_CERTIFICATE_NAME, SIGNER_TLS_KEY_NAME, + SIGNER_URL_ENV, }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, @@ -29,6 +31,7 @@ use docker_compose_types::{ }; use eyre::Result; use indexmap::IndexMap; +use rcgen::generate_simple_self_signed; /// Name of the docker compose file pub(super) const CB_COMPOSE_FILE: &str = "cb.docker-compose.yml"; @@ -78,7 +81,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re if let Some(SignerConfig { inner: SignerType::Remote { url }, .. }) = &cb_config.signer { url.to_string() } else { - format!("http://cb_signer:{signer_port}") + format!("https://cb_signer:{signer_port}") }; let builder_events_port = 30000; @@ -91,6 +94,10 @@ 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| config.tls_certificates.clone()).unwrap_or_default(); + // setup modules if let Some(modules_config) = cb_config.modules { for module in modules_config { @@ -111,6 +118,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_interp(MODULE_JWT_ENV, &jwt_name), get_env_val(SIGNER_URL_ENV, &signer_server), + get_env_val(SIGNER_TLS_CERTIFICATES_ENV, SIGNER_TLS_CERTIFICATES_DEFAULT), ]); // Pass on the env variables @@ -159,6 +167,12 @@ 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)); + module_volumes.push(Volumes::Simple(format!( + "{}:{}/{}:ro", + certs_path.join(SIGNER_TLS_CERTIFICATE_NAME).display(), + SIGNER_TLS_CERTIFICATES_DEFAULT, + SIGNER_TLS_CERTIFICATE_NAME + ))); // depends_on let mut module_dependencies = IndexMap::new(); @@ -299,6 +313,16 @@ 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 { + pbs_volumes.push(Volumes::Simple(format!( + "{}:{}/{}:ro", + certs_path.join(SIGNER_TLS_CERTIFICATE_NAME).display(), + SIGNER_TLS_CERTIFICATES_DEFAULT, + SIGNER_TLS_CERTIFICATE_NAME + ))); + let (key, val) = get_env_val(SIGNER_TLS_CERTIFICATES_ENV, SIGNER_TLS_CERTIFICATES_DEFAULT); + pbs_envs.insert(key, val); + } let pbs_service = Service { container_name: Some("cb_pbs".to_owned()), @@ -335,6 +359,7 @@ 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_uval(SIGNER_PORT_ENV, signer_port as u64), + get_env_val(SIGNER_TLS_CERTIFICATES_ENV, SIGNER_TLS_CERTIFICATES_DEFAULT), ]); let mut ports = vec![]; @@ -429,6 +454,35 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re volumes.extend(get_log_volume(&cb_config.logs, SIGNER_MODULE_NAME)); + if !certs_path.try_exists()? { + std::fs::create_dir(certs_path.clone())?; + } + + if !certs_path.join(SIGNER_TLS_CERTIFICATE_NAME).try_exists()? || + !certs_path.join(SIGNER_TLS_KEY_NAME).try_exists()? + { + let (cert, key): (String, String) = + generate_simple_self_signed(vec!["cb_signer".to_string()]) + .map(|x| (x.cert.pem(), x.key_pair.serialize_pem())) + .map_err(|e| eyre::eyre!("Failed to generate TLS certificate: {e}"))?; + + std::fs::write(certs_path.join(SIGNER_TLS_CERTIFICATE_NAME), &cert)?; + std::fs::write(certs_path.join(SIGNER_TLS_KEY_NAME), &key)?; + } + + volumes.push(Volumes::Simple(format!( + "{}:{}/{}:ro", + certs_path.join(SIGNER_TLS_CERTIFICATE_NAME).display(), + SIGNER_TLS_CERTIFICATES_DEFAULT, + SIGNER_TLS_CERTIFICATE_NAME + ))); + volumes.push(Volumes::Simple(format!( + "{}:{}/{}:ro", + certs_path.join(SIGNER_TLS_KEY_NAME).display(), + SIGNER_TLS_CERTIFICATES_DEFAULT, + SIGNER_TLS_KEY_NAME + ))); + // networks let signer_networks = vec![SIGNER_NETWORK.to_owned()]; @@ -441,7 +495,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 https://localhost:{signer_port}/status" ))), interval: Some("30s".into()), timeout: Some("5s".into()), diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index df78b046..ac0600eb 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -21,6 +21,7 @@ ethereum_serde_utils.workspace = true ethereum_ssz.workspace = true ethereum_ssz_derive.workspace = true eyre.workspace = true +jsonwebtoken.workspace = true pbkdf2.workspace = true rand.workspace = true reqwest.workspace = true @@ -40,4 +41,3 @@ tree_hash.workspace = true tree_hash_derive.workspace = true unicode-normalization.workspace = true url.workspace = true -jsonwebtoken.workspace = true diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 34413b65..4a143691 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -1,8 +1,7 @@ -use std::time::{Duration, Instant}; +use std::path::PathBuf; use alloy::{primitives::Address, rpc::types::beacon::BlsSignature}; -use eyre::WrapErr; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use reqwest::Certificate; use serde::Deserialize; use url::Url; @@ -15,7 +14,6 @@ use super::{ }, }; use crate::{ - constants::SIGNER_JWT_EXPIRATION, signer::{BlsPublicKey, EcdsaSignature}, types::{Jwt, ModuleId}, utils::create_jwt, @@ -28,65 +26,44 @@ pub struct SignerClient { /// Url endpoint of the Signer Module url: Url, client: reqwest::Client, - last_jwt_refresh: Instant, module_id: ModuleId, jwt_secret: Jwt, + nonce: usize, } 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)?; + pub fn new( + signer_server_url: Url, + cert_path: PathBuf, + jwt_secret: Jwt, + module_id: ModuleId, + ) -> eyre::Result { + let mut builder = + reqwest::Client::builder().timeout(DEFAULT_REQUEST_TIMEOUT).use_rustls_tls(); - 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); - - let client = reqwest::Client::builder() - .timeout(DEFAULT_REQUEST_TIMEOUT) - .default_headers(headers) - .build()?; + builder = builder.add_root_certificate(Certificate::from_pem(&std::fs::read(cert_path)?)?); Ok(Self { url: signer_server_url, - client, - last_jwt_refresh: Instant::now(), + client: builder.build()?, module_id, jwt_secret, + nonce: 0, }) } - 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)?; - - 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(()) + fn build_jwt(&self) -> Result { + create_jwt(&self.module_id, self.nonce, &self.jwt_secret).map_err(SignerClientError::from) } /// Request a list of validator pubkeys for which signatures can be /// requested. // TODO: add more docs on how proxy keys work pub async fn get_pubkeys(&mut self) -> Result { - self.refresh_jwt()?; - + let jwt = self.build_jwt()?; let url = self.url.join(GET_PUBKEYS_PATH)?; - let res = self.client.get(url).send().await?; + let res = self.client.get(url).bearer_auth(jwt).send().await?; if !res.status().is_success() { return Err(SignerClientError::FailedRequest { @@ -95,6 +72,7 @@ impl SignerClient { }); } + self.nonce += 1; Ok(serde_json::from_slice(&res.bytes().await?)?) } @@ -103,10 +81,9 @@ impl SignerClient { where T: for<'de> Deserialize<'de>, { - self.refresh_jwt()?; - + let jwt = self.build_jwt()?; let url = self.url.join(REQUEST_SIGNATURE_PATH)?; - let res = self.client.post(url).json(&request).send().await?; + let res = self.client.post(url).bearer_auth(jwt).json(&request).send().await?; let status = res.status(); let response_bytes = res.bytes().await?; @@ -118,6 +95,7 @@ impl SignerClient { }); } + self.nonce += 1; let signature = serde_json::from_slice(&response_bytes)?; Ok(signature) @@ -151,10 +129,9 @@ impl SignerClient { where T: ProxyId + for<'de> Deserialize<'de>, { - self.refresh_jwt()?; - + let jwt = self.build_jwt()?; let url = self.url.join(GENERATE_PROXY_KEY_PATH)?; - let res = self.client.post(url).json(&request).send().await?; + let res = self.client.post(url).bearer_auth(jwt).json(&request).send().await?; let status = res.status(); let response_bytes = res.bytes().await?; @@ -166,6 +143,7 @@ impl SignerClient { }); } + self.nonce += 1; let signed_proxy_delegation = serde_json::from_slice(&response_bytes)?; Ok(signed_proxy_delegation) diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 422af7e7..62f229a2 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -40,6 +40,12 @@ pub const JWTS_ENV: &str = "CB_JWTS"; /// The JWT secret for the signer to validate the modules requests pub const SIGNER_JWT_SECRET_ENV: &str = "CB_SIGNER_JWT_SECRET"; +/// Path to the certificates folder where the cert.pem and key.pem files are +/// stored/generated +pub const SIGNER_TLS_CERTIFICATES_ENV: &str = "CB_SIGNER_TLS_CERTIFICATES"; +pub const SIGNER_TLS_CERTIFICATES_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 16b089ca..80e4be06 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 eyre::{ContextCompat, Result}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -10,7 +10,7 @@ use crate::{ constants::{CONFIG_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, SIGNER_URL_ENV}, load_env_var, utils::load_file_from_env, - BUILDER_PORT_ENV, + SignerConfig, BUILDER_PORT_ENV, SIGNER_TLS_CERTIFICATES_ENV, SIGNER_TLS_CERTIFICATE_NAME, }, types::{Chain, Jwt, ModuleId}, }; @@ -82,6 +82,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 +105,11 @@ pub fn load_commit_module_config() -> Result() -> Result<(PbsModuleC chain: Chain, relays: Vec, pbs: CustomPbsConfig, + signer: SignerConfig, muxes: Option, } @@ -336,8 +338,13 @@ 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 = load_env_var(SIGNER_TLS_CERTIFICATES_ENV) + .map(PathBuf::from) + .unwrap_or(cb_config.signer.tls_certificates) + .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 9df6b948..844241e7 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -7,7 +7,7 @@ use url::Url; use super::{ constants::SIGNER_IMAGE_DEFAULT, load_jwt_secrets, utils::load_env_var, CommitBoostConfig, - SIGNER_PORT_ENV, + SIGNER_PORT_ENV, SIGNER_TLS_CERTIFICATES_ENV, SIGNER_TLS_CERTIFICATE_NAME, SIGNER_TLS_KEY_NAME, }; use crate::{ config::{DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV}, @@ -24,12 +24,18 @@ pub struct SignerConfig { /// Inner type-specific configuration #[serde(flatten)] pub inner: SignerType, + #[serde(default = "default_certs_path")] + pub tls_certificates: PathBuf, } fn default_signer() -> String { SIGNER_IMAGE_DEFAULT.to_string() } +fn default_certs_path() -> PathBuf { + PathBuf::from("./certs") +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub struct DirkHostConfig { @@ -90,6 +96,7 @@ pub struct StartSignerConfig { pub server_port: u16, pub jwts: HashMap, pub dirk: Option, + pub tls_certificates: (Vec, Vec), } impl StartSignerConfig { @@ -99,9 +106,15 @@ impl StartSignerConfig { let jwts = load_jwt_secrets()?; let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; - let signer = config.signer.ok_or_eyre("Signer config is missing")?.inner; + let signer = config.signer.ok_or_eyre("Signer config is missing")?; + + let certs_path = load_env_var(SIGNER_TLS_CERTIFICATES_ENV) + .map(PathBuf::from) + .unwrap_or(signer.tls_certificates); + let cert = std::fs::read(certs_path.join(SIGNER_TLS_CERTIFICATE_NAME))?; + let key = std::fs::read(certs_path.join(SIGNER_TLS_KEY_NAME))?; - match signer { + match signer.inner { SignerType::Local { loader, store, .. } => Ok(StartSignerConfig { chain: config.chain, loader: Some(loader), @@ -109,6 +122,7 @@ impl StartSignerConfig { jwts, store, dirk: None, + tls_certificates: (cert, key), }), SignerType::Dirk { @@ -151,6 +165,7 @@ impl StartSignerConfig { None => None, }, }), + tls_certificates: (cert, key), }) } diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 5293a789..eeb9455c 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -21,6 +21,7 @@ pub struct Jwt(pub String); pub struct JwtClaims { pub exp: u64, pub module: String, + pub jti: usize, } #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 37119580..9b90c182 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -274,13 +274,14 @@ pub fn blst_pubkey_to_alloy(pubkey: &PublicKey) -> BlsPublicKey { BlsPublicKey::from_slice(&pubkey.to_bytes()) } -/// Create a JWT for the given module id with expiration -pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { +/// Create a JWT for the given module id and nonce with expiration +pub fn create_jwt(module_id: &ModuleId, nonce: usize, secret: &str) -> eyre::Result { jsonwebtoken::encode( &jsonwebtoken::Header::default(), &JwtClaims { module: module_id.to_string(), exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, + jti: nonce, }, &jsonwebtoken::EncodingKey::from_secret(secret.as_ref()), ) @@ -307,17 +308,21 @@ pub fn decode_jwt(jwt: Jwt) -> eyre::Result { } /// Validate a JWT with the given secret -pub fn validate_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> { +pub fn validate_jwt(jwt: Jwt, expected_nonce: usize, secret: &str) -> eyre::Result<()> { let mut validation = jsonwebtoken::Validation::default(); validation.leeway = 10; - jsonwebtoken::decode::( + let jwt = jsonwebtoken::decode::( jwt.as_str(), &jsonwebtoken::DecodingKey::from_secret(secret.as_ref()), &validation, - ) - .map(|_| ()) - .map_err(From::from) + )?; + + if jwt.claims.jti == expected_nonce { + Ok(()) + } else { + eyre::bail!("Unexpected nonce") + } } /// Generates a random string @@ -369,21 +374,21 @@ mod test { #[test] fn test_jwt_validation() { // Check valid JWT - let jwt = create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret").unwrap(); + let jwt = create_jwt(&ModuleId("DA_COMMIT".to_string()), 5, "secret").unwrap(); let module_id = decode_jwt(jwt.clone()).unwrap(); assert_eq!(module_id, ModuleId("DA_COMMIT".to_string())); - let response = validate_jwt(jwt, "secret".as_ref()); + let response = validate_jwt(jwt, 5, "secret".as_ref()); assert!(response.is_ok()); // Check expired JWT - let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.iiq4Z2ed2hk3c3c-cn2QOQJWE5XUOc5BoaIPT-I8q-s".to_string()); - let response = validate_jwt(expired_jwt, "secret"); + let expired_jwt = Jwt::from("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyMzQ1Njc4OTAsIm1vZHVsZSI6IkRBX0NPTU1JVCIsImp0aSI6NX0.8P_2nScYiMhs9yUzI5M5QgJ_onQHxjeZ5C28ryx3nM0".to_string()); + let response = validate_jwt(expired_jwt, 5, "secret"); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "ExpiredSignature"); // Check invalid signature JWT - let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.w9WYdDNzgDjYTvjBkk4GGzywGNBYPxnzU2uJWzPUT1s".to_string()); - let response = validate_jwt(invalid_jwt, "secret"); + let invalid_jwt = Jwt::from("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyMzQ1Njc4OTAsIm1vZHVsZSI6IkRBX0NPTU1JVCIsImp0aSI6NX0.G8qe1KJkrJZMg6cOjqAWR05z_F0pODVBnl-f6f5_oKs".to_string()); + let response = validate_jwt(invalid_jwt, 5, "secret"); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "InvalidSignature"); } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 69f92886..c6ad734c 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 @@ -16,17 +17,18 @@ cb-metrics.workspace = true eyre.workspace = true futures.workspace = true headers.workspace = true +jsonwebtoken.workspace = true lazy_static.workspace = true prometheus.workspace = true prost.workspace = true rand.workspace = true +rustls.workspace = true thiserror.workspace = true tokio.workspace = true tonic.workspace = true tracing.workspace = true tree_hash.workspace = true uuid.workspace = true -jsonwebtoken.workspace = true [build-dependencies] tonic-build.workspace = true diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 28a1d934..5aab62f8 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -9,6 +9,7 @@ use axum::{ Extension, Json, }; use axum_extra::TypedHeader; +use axum_server::tls_rustls::RustlsConfig; use cb_common::{ commit::{ constants::{ @@ -28,7 +29,8 @@ use cb_common::{ use cb_metrics::provider::MetricsProvider; use eyre::Context; use headers::{authorization::Bearer, Authorization}; -use tokio::{net::TcpListener, sync::RwLock}; +use rustls::crypto::aws_lc_rs; +use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; use uuid::Uuid; @@ -45,9 +47,9 @@ pub struct SigningService; struct SigningState { /// Manager handling different signing methods manager: Arc>, - /// Map of modules ids to JWT secrets. This also acts as registry of all - /// modules running - jwts: Arc>, + /// Map of modules ids to (nonce, JWT secret). This also acts as registry of + /// all modules running + jwts: Arc>>, } impl SigningService { @@ -61,7 +63,13 @@ impl SigningService { let state = SigningState { manager: Arc::new(RwLock::new(start_manager(config.clone()).await?)), - jwts: config.jwts.into(), + jwts: Arc::new(RwLock::new( + config + .jwts + .iter() + .map(|(module_id, jwt_secret)| (module_id.clone(), (0, jwt_secret.clone()))) + .collect::>(), + )), }; let loaded_consensus = state.manager.read().await.available_consensus_signers(); @@ -82,9 +90,17 @@ impl SigningService { .route(STATUS_PATH, get(handle_status)); let address = SocketAddr::from(([0, 0, 0, 0], config.server_port)); - let listener = TcpListener::bind(address).await?; - axum::serve(listener, app).await.wrap_err("signer server exited") + aws_lc_rs::default_provider() + .install_default() + .map_err(|_| eyre::eyre!("Failed to install TLS provider"))?; + let tls_config = + RustlsConfig::from_pem(config.tls_certificates.0, config.tls_certificates.1).await?; + + axum_server::bind_rustls(address, tls_config) + .serve(app.into_make_service()) + .await + .wrap_err("signer server exited") } fn init_metrics(network: Chain) -> eyre::Result<()> { @@ -108,16 +124,19 @@ async fn jwt_auth( SignerModuleError::Unauthorized })?; - let jwt_secret = state.jwts.get(&module_id).ok_or_else(|| { + let mut jwt_guard = state.jwts.write().await; + let (expected_nonce, jwt_secret) = jwt_guard.get_mut(&module_id).ok_or_else(|| { error!("Unauthorized request. Was the module started correctly?"); SignerModuleError::Unauthorized })?; - validate_jwt(jwt, jwt_secret).map_err(|e| { + validate_jwt(jwt, *expected_nonce, jwt_secret.as_str()).map_err(|e| { error!("Unauthorized request. Invalid JWT: {e}"); SignerModuleError::Unauthorized })?; + *expected_nonce += 1; + req.extensions_mut().insert(module_id); Ok(next.run(req).await) diff --git a/docs/docs/get_started/running/binary.md b/docs/docs/get_started/running/binary.md index 3708ab19..f6d7451b 100644 --- a/docs/docs/get_started/running/binary.md +++ b/docs/docs/get_started/running/binary.md @@ -28,6 +28,7 @@ Modules need some environment variables to work correctly. ### Signer Module - `CB_SIGNER_JWT_SECRET`: secret to use for JWT authentication with the Signer module. - `CB_SIGNER_PORT`: required, port to open the signer server on. +- `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).