diff --git a/Cargo.lock b/Cargo.lock index c52da7b48..976be8204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7324,6 +7324,7 @@ dependencies = [ "orb-endpoints", "orb-info", "orb-messages 0.0.0 (git+https://github.com/worldcoin/orb-messages?rev=af472fadb57ce55ac63f8f94bd2a0608e62405c7)", + "orb-security-utils", "orb-telemetry", "orb-update-agent-dbus", "portpicker", @@ -7485,6 +7486,7 @@ dependencies = [ "orb-dogd", "orb-info", "orb-secure-storage-ca", + "orb-security-utils", "orb-telemetry", "p256 0.13.2", "portpicker", @@ -8044,6 +8046,7 @@ dependencies = [ "orb-attest-dbus", "orb-build-info", "orb-info", + "orb-security-utils", "rand 0.8.5", "reqwest 0.12.24", "serde", diff --git a/attest/src/client.rs b/attest/src/client.rs index c85b514b2..a6f3ff3f4 100644 --- a/attest/src/client.rs +++ b/attest/src/client.rs @@ -25,7 +25,7 @@ pub enum Error { pub fn client() -> &'static Client { static CLIENT: OnceLock = OnceLock::new(); CLIENT.get_or_init(|| { - let builder = orb_security_utils::reqwest::http_client_builder() + let builder = orb_security_utils::reqwest::client_builder() .timeout(std::time::Duration::from_secs(60)) .user_agent(USER_AGENT); #[cfg(test)] diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 000000000..977c911aa --- /dev/null +++ b/clippy.toml @@ -0,0 +1,6 @@ +disallowed-methods = [ + { path = "reqwest::Client::builder", reason = "use orb_security_utils::reqwest::client_builder() instead" }, + { path = "reqwest::Client::new", reason = "use orb_security_utils::reqwest::client_builder().build() instead" }, + { path = "reqwest::blocking::Client::builder", allow-invalid = true, reason = "use orb_security_utils::reqwest::blocking::client_builder() instead" }, + { path = "reqwest::blocking::Client::new", allow-invalid = true, reason = "use orb_security_utils::reqwest::blocking::client_builder().build() instead" }, +] diff --git a/experiments/artificer/src/downloader/mod.rs b/experiments/artificer/src/downloader/mod.rs index 6b257b3fc..625836425 100644 --- a/experiments/artificer/src/downloader/mod.rs +++ b/experiments/artificer/src/downloader/mod.rs @@ -1,4 +1,5 @@ //! Download functionality for various sources of artifacts. +#![allow(clippy::disallowed_methods)] use color_eyre::{eyre::WrapErr, Result}; use octocrab::Octocrab; diff --git a/experiments/orb-blob/blob-cli/src/main.rs b/experiments/orb-blob/blob-cli/src/main.rs index 33d61ca2f..3f4c2e377 100644 --- a/experiments/orb-blob/blob-cli/src/main.rs +++ b/experiments/orb-blob/blob-cli/src/main.rs @@ -1,3 +1,5 @@ +// Allow regular reqwest builder without HTTPS enforsement +#![allow(clippy::disallowed_methods)] mod http_handler; use color_eyre::eyre::{Context, Result}; use http_handler::{download, info, upload}; diff --git a/experiments/orb-blob/p2p/src/bootstrap.rs b/experiments/orb-blob/p2p/src/bootstrap.rs index 02e9f6f46..17488a8b4 100644 --- a/experiments/orb-blob/p2p/src/bootstrap.rs +++ b/experiments/orb-blob/p2p/src/bootstrap.rs @@ -1,3 +1,5 @@ +// Allow regular reqwest builder without HTTPS enforsement +#![allow(clippy::disallowed_methods)] use std::collections::BTreeSet; use std::time::Duration; diff --git a/experiments/orb-blob/tests/blob_create.rs b/experiments/orb-blob/tests/blob_create.rs index ea9d2f8a8..57b95d03e 100644 --- a/experiments/orb-blob/tests/blob_create.rs +++ b/experiments/orb-blob/tests/blob_create.rs @@ -1,3 +1,5 @@ +// Allow regular reqwest builder without HTTPS enforsement +#![allow(clippy::disallowed_methods)] use async_tempfile::TempFile; use fixture::Fixture; use iroh_blobs::{store::fs::FsStore, Hash}; diff --git a/experiments/orb-blob/tests/blob_delete.rs b/experiments/orb-blob/tests/blob_delete.rs index 0bd4f6e70..11b5e4e3a 100644 --- a/experiments/orb-blob/tests/blob_delete.rs +++ b/experiments/orb-blob/tests/blob_delete.rs @@ -1,3 +1,4 @@ +#![allow(clippy::disallowed_methods)] use async_tempfile::TempFile; use fixture::Fixture; use iroh_blobs::{ diff --git a/experiments/orb-blob/tests/e2e_node_share.rs b/experiments/orb-blob/tests/e2e_node_share.rs index cbde6e262..ad58de13b 100644 --- a/experiments/orb-blob/tests/e2e_node_share.rs +++ b/experiments/orb-blob/tests/e2e_node_share.rs @@ -1,3 +1,5 @@ +// Allow regular reqwest builder without HTTPS enforsement +#![allow(clippy::disallowed_methods)] use async_tempfile::TempFile; use fixture::Fixture; use iroh::{NodeAddr, SecretKey}; diff --git a/orb-backend-status/Cargo.toml b/orb-backend-status/Cargo.toml index c4b55ecea..c7bcaf54f 100644 --- a/orb-backend-status/Cargo.toml +++ b/orb-backend-status/Cargo.toml @@ -32,6 +32,7 @@ orb-info = { workspace = true, features = [ "orb-token", ] } orb-dogd.workspace = true +orb-security-utils = { workspace = true, features = ["reqwest"] } orb-telemetry = { workspace = true, features = ["otel", "zbus-tracing"] } orb-update-agent-dbus.workspace = true reqwest = { workspace = true, features = ["json"] } @@ -57,6 +58,7 @@ zbus = { workspace = true, default-features = false, features = ["tokio"] } zenorb.workspace = true [dev-dependencies] +orb-security-utils = { workspace = true, features = ["reqwest", "dangerously-allow-http"] } dbus-launch.workspace = true eyre.workspace = true portpicker = "0.1.1" diff --git a/orb-backend-status/src/backend/client.rs b/orb-backend-status/src/backend/client.rs index fcbdf517f..1034b9d62 100644 --- a/orb-backend-status/src/backend/client.rs +++ b/orb-backend-status/src/backend/client.rs @@ -71,7 +71,7 @@ impl StatusClient { .retry_bounds(min_req_retry_interval, max_req_retry_interval) .build_with_max_retries(3); - let reqwest_client = reqwest::Client::builder() + let reqwest_client = orb_security_utils::reqwest::client_builder() .timeout(req_timeout) .user_agent("orb-backend-status") .build() diff --git a/orb-connd/Cargo.toml b/orb-connd/Cargo.toml index 7ecb8cf69..f63e42c8c 100644 --- a/orb-connd/Cargo.toml +++ b/orb-connd/Cargo.toml @@ -31,6 +31,7 @@ num-traits.workspace = true oes.workspace = true orb-backend-status-dbus.workspace = true orb-build-info.workspace = true +orb-security-utils = { workspace = true, features = ["reqwest"] } orb-connd-dbus.workspace = true orb-dogd.workspace = true orb-info = { workspace = true, features = ["orb-os-release", "async"] } diff --git a/orb-connd/nm_cfg/20-defaults.conf b/orb-connd/nm_cfg/20-defaults.conf index c6e0d61c3..1836bc7b1 100644 --- a/orb-connd/nm_cfg/20-defaults.conf +++ b/orb-connd/nm_cfg/20-defaults.conf @@ -21,6 +21,6 @@ ipv6.route-metric=500 [connectivity] enabled=true -uri=http://connectivity-check.worldcoin.org +uri=https://connectivity-check.worldcoin.org interval=300 response= diff --git a/orb-connd/src/conn_http_check.rs b/orb-connd/src/conn_http_check.rs index 7aa1f59dd..7dbec3b5e 100644 --- a/orb-connd/src/conn_http_check.rs +++ b/orb-connd/src/conn_http_check.rs @@ -17,9 +17,9 @@ impl ConnHttpCheck { /// iface will default to default route if `None` is passed as arg pub async fn run(connectivity_uri: &str, iface: Option<&str>) -> Result { let client = if let Some(iface) = iface { - reqwest::Client::builder().interface(iface) + orb_security_utils::reqwest::client_builder().interface(iface) } else { - reqwest::Client::builder() + orb_security_utils::reqwest::client_builder() } .timeout(Duration::from_secs(5)) .build()?; diff --git a/se050-reprovision/src/remote_api.rs b/se050-reprovision/src/remote_api.rs index e591a1a90..139020436 100644 --- a/se050-reprovision/src/remote_api.rs +++ b/se050-reprovision/src/remote_api.rs @@ -41,7 +41,7 @@ where } pub fn default_reqwest_client(self) -> Result>> { - let client = orb_security_utils::reqwest::http_client_builder() + let client = orb_security_utils::reqwest::client_builder() .user_agent(USER_AGENT) .build() .wrap_err("failed to create http client")?; diff --git a/se050-reprovision/tests/harness.rs b/se050-reprovision/tests/harness.rs index 2553f1d00..614a6b444 100644 --- a/se050-reprovision/tests/harness.rs +++ b/se050-reprovision/tests/harness.rs @@ -15,6 +15,8 @@ pub struct Harness { } impl HarnessBuilder { + // local_backend uses http://localhost — plain reqwest client is intentional here. + #[allow(clippy::disallowed_methods)] pub fn build(self) -> (Harness, orb_se050_reprovision::Config) where S: hb::IsComplete, diff --git a/security-utils/Cargo.toml b/security-utils/Cargo.toml index 5c1ac98ac..c9b76ed8b 100644 --- a/security-utils/Cargo.toml +++ b/security-utils/Cargo.toml @@ -13,6 +13,9 @@ rust-version.workspace = true [features] blocking = ["reqwest/blocking"] reqwest = ["dep:reqwest"] +# Disables https_only enforcement. Only intended for use in dev-dependencies +# of crates that spin up local HTTP test servers. +dangerously-allow-http = [] [dependencies] eyre = "0.6" diff --git a/security-utils/src/certs.rs b/security-utils/src/certs.rs new file mode 100644 index 000000000..7770a7cde --- /dev/null +++ b/security-utils/src/certs.rs @@ -0,0 +1,171 @@ +use eyre::{ensure, Result}; +use hex_literal::hex; + +// +// Amazon Trust Services - https://www.amazontrust.com/repository/ +// Updated by @oldgalileo (17/07/2024) +// +pub static AWS_ROOT_CA1_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/AmazonRootCA1.pem" +)); +pub static AWS_ROOT_CA1_SHA256: [u8; 32] = + hex!("2c43952ee9e000ff2acc4e2ed0897c0a72ad5fa72c3d934e81741cbd54f05bd1"); + +pub static AWS_ROOT_CA2_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/AmazonRootCA2.pem" +)); +pub static AWS_ROOT_CA2_SHA256: [u8; 32] = + hex!("a3a7fe25439d9a9b50f60af43684444d798a4c869305bf615881e5c84a44c1a2"); + +pub static AWS_ROOT_CA3_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/AmazonRootCA3.pem" +)); +pub static AWS_ROOT_CA3_SHA256: [u8; 32] = + hex!("3eb7c3258f4af9222033dc1bb3dd2c7cfa0982b98e39fb8e9dc095cfeb38126c"); + +pub static AWS_ROOT_CA4_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/AmazonRootCA4.pem" +)); +pub static AWS_ROOT_CA4_SHA256: [u8; 32] = + hex!("b0b7961120481e33670315b2f843e643c42f693c7a1010eb9555e06ddc730214"); + +// Starfield Root CA G2 certificate (acquired by Amazon) +pub static SFS_ROOT_G2_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/SFSRootCAG2.pem" +)); +pub static SFS_ROOT_G2_SHA256: [u8; 32] = + hex!("870f56d009d8aeb95b716b0e7b0020225d542c4b283b9ed896edf97428d6712e"); + +// +// Google Trust Services - https://pki.goog/ +// Updated by @oldgalileo (16/07/2024) +// +pub static GTS_ROOT_R1_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/GTS_Root_R1.pem" +)); +pub static GTS_ROOT_R1_SHA256: [u8; 32] = + hex!("4195ea007a7ef8d3e2d338e8d9ff0083198e36bfa025442ddf41bb5213904fc2"); + +pub static GTS_ROOT_R2_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/GTS_Root_R2.pem" +)); +pub static GTS_ROOT_R2_SHA256: [u8; 32] = + hex!("1a49076630e489e4b1056804fb6c768397a9de52b236609aaf6ec5b94ce508ec"); + +pub static GTS_ROOT_R3_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/GTS_Root_R3.pem" +)); +pub static GTS_ROOT_R3_SHA256: [u8; 32] = + hex!("39238e09bb7d30e39fbf87746ceac206f7ec206cff3d73c743e3f818ca2ec54f"); + +pub static GTS_ROOT_R4_CERT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/certs/GTS_Root_R4.pem" +)); +pub static GTS_ROOT_R4_SHA256: [u8; 32] = + hex!("7e8b80d078d3dd77d3ed2108dd2b33412c12d7d72cb0965741c70708691776a2"); + +pub fn all_pem_certs() -> [&'static [u8]; 9] { + [ + AWS_ROOT_CA1_CERT, + AWS_ROOT_CA2_CERT, + AWS_ROOT_CA3_CERT, + AWS_ROOT_CA4_CERT, + SFS_ROOT_G2_CERT, + GTS_ROOT_R1_CERT, + GTS_ROOT_R2_CERT, + GTS_ROOT_R3_CERT, + GTS_ROOT_R4_CERT, + ] +} + +pub fn verify_pinned_cert(cert_pem: &[u8], sha256: &[u8; 32]) -> Result<()> { + // Verify that the certificate has not been replaced. + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(cert_pem); + let digest = context.finish(); + ensure!( + digest.as_ref() == sha256, + "provided cert bytes did not match sha256 hash" + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::time::{Duration, SystemTime}; + + use super::*; + + #[test] + fn test_verify_pinned_cert() { + verify_pinned_cert(AWS_ROOT_CA1_CERT, &AWS_ROOT_CA1_SHA256) + .expect("Failed to verify AWS CA1 cert"); + verify_pinned_cert(AWS_ROOT_CA2_CERT, &AWS_ROOT_CA2_SHA256) + .expect("Failed to verify AWS CA2 cert"); + verify_pinned_cert(AWS_ROOT_CA3_CERT, &AWS_ROOT_CA3_SHA256) + .expect("Failed to verify AWS CA3 cert"); + verify_pinned_cert(AWS_ROOT_CA4_CERT, &AWS_ROOT_CA4_SHA256) + .expect("Failed to verify AWS CA4 cert"); + verify_pinned_cert(SFS_ROOT_G2_CERT, &SFS_ROOT_G2_SHA256) + .expect("Failed to verify SFS G2 cert"); + + verify_pinned_cert(GTS_ROOT_R1_CERT, >S_ROOT_R1_SHA256) + .expect("Failed to verify GTS R1 cert"); + verify_pinned_cert(GTS_ROOT_R2_CERT, >S_ROOT_R2_SHA256) + .expect("Failed to verify GTS R2 cert"); + verify_pinned_cert(GTS_ROOT_R3_CERT, >S_ROOT_R3_SHA256) + .expect("Failed to verify GTS R3 cert"); + verify_pinned_cert(GTS_ROOT_R4_CERT, >S_ROOT_R4_SHA256) + .expect("Failed to verify GTS R4 cert"); + } + + const EXPIRATION_GATE: Duration = Duration::from_secs(180 * 24 * 60 * 60); + + #[test] + fn pinned_certificates_do_not_expire_soon() { + for (name, cert_pem) in [ + ("AmazonRootCA1.pem", AWS_ROOT_CA1_CERT), + ("AmazonRootCA2.pem", AWS_ROOT_CA2_CERT), + ("AmazonRootCA3.pem", AWS_ROOT_CA3_CERT), + ("AmazonRootCA4.pem", AWS_ROOT_CA4_CERT), + ("SFSRootCAG2.pem", SFS_ROOT_G2_CERT), + ("GTS_Root_R1.pem", GTS_ROOT_R1_CERT), + ("GTS_Root_R2.pem", GTS_ROOT_R2_CERT), + ("GTS_Root_R3.pem", GTS_ROOT_R3_CERT), + ("GTS_Root_R4.pem", GTS_ROOT_R4_CERT), + ] { + let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem) + .unwrap_or_else(|e| panic!("{name} should parse as PEM: {e}")); + let cert = pem + .parse_x509() + .unwrap_or_else(|e| panic!("{name} should parse as X.509: {e}")); + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("system time should be after UNIX epoch"); + let expiry = Duration::from_secs( + cert.validity() + .not_after + .timestamp() + .try_into() + .expect("certificate expiry should be after UNIX epoch"), + ); + let remaining = expiry.saturating_sub(now); + + assert!( + remaining >= EXPIRATION_GATE, + "{name} expires in {remaining:?}" + ); + } + } +} diff --git a/security-utils/src/lib.rs b/security-utils/src/lib.rs index ec9e9549d..dc65e18b3 100644 --- a/security-utils/src/lib.rs +++ b/security-utils/src/lib.rs @@ -1,4 +1,6 @@ #![forbid(unsafe_code)] +pub mod certs; + #[cfg(feature = "reqwest")] pub mod reqwest; diff --git a/security-utils/src/reqwest.rs b/security-utils/src/reqwest.rs index 9d613376a..363ab54db 100644 --- a/security-utils/src/reqwest.rs +++ b/security-utils/src/reqwest.rs @@ -1,84 +1,18 @@ use std::sync::OnceLock; -use eyre::{ensure, Result, WrapErr}; -use hex_literal::hex; - +use eyre::{Result, WrapErr}; use reqwest::{Client, ClientBuilder}; pub use reqwest; use reqwest::Certificate; -// -// Amazon Trust Services - https://www.amazontrust.com/repository/ -// Updated by @oldgalileo (17/07/2024) -// -pub static AWS_ROOT_CA1_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/AmazonRootCA1.pem" -)); -pub static AWS_ROOT_CA1_SHA256: [u8; 32] = - hex!("2c43952ee9e000ff2acc4e2ed0897c0a72ad5fa72c3d934e81741cbd54f05bd1"); - -pub static AWS_ROOT_CA2_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/AmazonRootCA2.pem" -)); -pub static AWS_ROOT_CA2_SHA256: [u8; 32] = - hex!("a3a7fe25439d9a9b50f60af43684444d798a4c869305bf615881e5c84a44c1a2"); - -pub static AWS_ROOT_CA3_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/AmazonRootCA3.pem" -)); -pub static AWS_ROOT_CA3_SHA256: [u8; 32] = - hex!("3eb7c3258f4af9222033dc1bb3dd2c7cfa0982b98e39fb8e9dc095cfeb38126c"); - -pub static AWS_ROOT_CA4_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/AmazonRootCA4.pem" -)); -pub static AWS_ROOT_CA4_SHA256: [u8; 32] = - hex!("b0b7961120481e33670315b2f843e643c42f693c7a1010eb9555e06ddc730214"); - -// Starfield Root CA G2 certificate (acquired by Amazon) -pub static SFS_ROOT_G2_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/SFSRootCAG2.pem" -)); -pub static SFS_ROOT_G2_SHA256: [u8; 32] = - hex!("870f56d009d8aeb95b716b0e7b0020225d542c4b283b9ed896edf97428d6712e"); - -// -// Google Trust Services - https://pki.goog/ -// Updated by @oldgalileo (16/07/2024) -// -pub static GTS_ROOT_R1_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/GTS_Root_R1.pem" -)); -pub static GTS_ROOT_R1_SHA256: [u8; 32] = - hex!("4195ea007a7ef8d3e2d338e8d9ff0083198e36bfa025442ddf41bb5213904fc2"); - -pub static GTS_ROOT_R2_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/GTS_Root_R2.pem" -)); -pub static GTS_ROOT_R2_SHA256: [u8; 32] = - hex!("1a49076630e489e4b1056804fb6c768397a9de52b236609aaf6ec5b94ce508ec"); - -pub static GTS_ROOT_R3_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/GTS_Root_R3.pem" -)); -pub static GTS_ROOT_R3_SHA256: [u8; 32] = - hex!("39238e09bb7d30e39fbf87746ceac206f7ec206cff3d73c743e3f818ca2ec54f"); - -pub static GTS_ROOT_R4_CERT: &[u8] = include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/certs/GTS_Root_R4.pem" -)); -pub static GTS_ROOT_R4_SHA256: [u8; 32] = - hex!("7e8b80d078d3dd77d3ed2108dd2b33412c12d7d72cb0965741c70708691776a2"); +use crate::certs::{ + verify_pinned_cert, AWS_ROOT_CA1_CERT, AWS_ROOT_CA1_SHA256, AWS_ROOT_CA2_CERT, + AWS_ROOT_CA2_SHA256, AWS_ROOT_CA3_CERT, AWS_ROOT_CA3_SHA256, AWS_ROOT_CA4_CERT, + AWS_ROOT_CA4_SHA256, GTS_ROOT_R1_CERT, GTS_ROOT_R1_SHA256, GTS_ROOT_R2_CERT, + GTS_ROOT_R2_SHA256, GTS_ROOT_R3_CERT, GTS_ROOT_R3_SHA256, GTS_ROOT_R4_CERT, + GTS_ROOT_R4_SHA256, SFS_ROOT_G2_CERT, SFS_ROOT_G2_SHA256, +}; /// Important certificates we vendor for security #[derive(Debug)] @@ -134,15 +68,7 @@ pub fn get_certs() -> &'static VendoredCerts { } fn make_cert(cert_pem: &[u8], sha256: &[u8; 32]) -> Result { - // Verify that the certificate has not been replaced - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(cert_pem); - let digest = context.finish(); - ensure!( - digest.as_ref() == sha256, - "provided cert bytes did not match sha256 hash" - ); - + verify_pinned_cert(cert_pem, sha256)?; Certificate::from_pem(cert_pem).wrap_err("certificate failed to parse") } @@ -151,9 +77,9 @@ macro_rules! helper { ($builder:expr, $certs:expr) => {{ let certs = $certs; $builder - .min_tls_version(reqwest::tls::Version::TLS_1_2) + .min_tls_version(reqwest::tls::Version::TLS_1_3) .tls_built_in_root_certs(false) - .https_only(true) + .https_only(!cfg!(feature = "dangerously-allow-http")) .add_root_certificate(certs.aws_root_ca1.clone()) .add_root_certificate(certs.aws_root_ca2.clone()) .add_root_certificate(certs.aws_root_ca3.clone()) @@ -167,7 +93,8 @@ macro_rules! helper { }}; } -pub fn http_client_builder() -> ClientBuilder { +#[allow(clippy::disallowed_methods)] +pub fn client_builder() -> ClientBuilder { let certs = get_certs(); helper!(Client::builder(), certs) } @@ -178,79 +105,9 @@ pub mod blocking { use super::get_certs; - pub fn http_client_builder() -> ClientBuilder { + #[allow(clippy::disallowed_methods)] + pub fn client_builder() -> ClientBuilder { let certs = get_certs(); helper!(Client::builder(), certs) } } - -#[cfg(test)] -mod tests { - use std::time::{Duration, SystemTime}; - - use super::*; - - #[test] - fn test_make_certs() { - make_cert(AWS_ROOT_CA1_CERT, &AWS_ROOT_CA1_SHA256) - .expect("Failed to make AWS CA1 cert"); - make_cert(AWS_ROOT_CA2_CERT, &AWS_ROOT_CA2_SHA256) - .expect("Failed to make AWS CA2 cert"); - make_cert(AWS_ROOT_CA3_CERT, &AWS_ROOT_CA3_SHA256) - .expect("Failed to make AWS CA3 cert"); - make_cert(AWS_ROOT_CA4_CERT, &AWS_ROOT_CA4_SHA256) - .expect("Failed to make AWS CA4 cert"); - make_cert(SFS_ROOT_G2_CERT, &SFS_ROOT_G2_SHA256) - .expect("Failed to make SFS G2 cert"); - - make_cert(GTS_ROOT_R1_CERT, >S_ROOT_R1_SHA256) - .expect("Failed to make GTS R1 cert"); - make_cert(GTS_ROOT_R2_CERT, >S_ROOT_R2_SHA256) - .expect("Failed to make GTS R2 cert"); - make_cert(GTS_ROOT_R3_CERT, >S_ROOT_R3_SHA256) - .expect("Failed to make GTS R3 cert"); - make_cert(GTS_ROOT_R4_CERT, >S_ROOT_R4_SHA256) - .expect("Failed to make GTS R4 cert"); - } - - // assert expiration date is under 6 months - const EXPIRATION_GATE: Duration = Duration::from_secs(180 * 24 * 60 * 60); - - #[test] - fn pinned_certificates_do_not_expire_soon() { - for (name, cert_pem) in [ - ("AmazonRootCA1.pem", AWS_ROOT_CA1_CERT), - ("AmazonRootCA2.pem", AWS_ROOT_CA2_CERT), - ("AmazonRootCA3.pem", AWS_ROOT_CA3_CERT), - ("AmazonRootCA4.pem", AWS_ROOT_CA4_CERT), - ("SFSRootCAG2.pem", SFS_ROOT_G2_CERT), - ("GTS_Root_R1.pem", GTS_ROOT_R1_CERT), - ("GTS_Root_R2.pem", GTS_ROOT_R2_CERT), - ("GTS_Root_R3.pem", GTS_ROOT_R3_CERT), - ("GTS_Root_R4.pem", GTS_ROOT_R4_CERT), - ] { - let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem) - .unwrap_or_else(|e| panic!("{name} should parse as PEM: {e}")); - let cert = pem - .parse_x509() - .unwrap_or_else(|e| panic!("{name} should parse as X.509: {e}")); - - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("system time should be after UNIX epoch"); - let expiry = Duration::from_secs( - cert.validity() - .not_after - .timestamp() - .try_into() - .expect("certificate expiry should be after UNIX epoch"), - ); - let remaining = expiry.saturating_sub(now); - - assert!( - remaining >= EXPIRATION_GATE, - "{name} expires in {remaining:?}" - ); - } - } -} diff --git a/speed-test/Cargo.toml b/speed-test/Cargo.toml index 90b00ec09..c9b019d77 100644 --- a/speed-test/Cargo.toml +++ b/speed-test/Cargo.toml @@ -25,6 +25,7 @@ color-eyre.workspace = true flate2.workspace = true orb-attest-dbus.path = "../attest/dbus" orb-build-info.workspace = true +orb-security-utils = { workspace = true, features = ["reqwest"] } orb-info.path = "../orb-info" rand.workspace = true reqwest = { workspace = true, features = ["multipart", "stream", "json"] } diff --git a/speed-test/src/lib.rs b/speed-test/src/lib.rs index a0f1f7fc8..9dbdf3aaa 100644 --- a/speed-test/src/lib.rs +++ b/speed-test/src/lib.rs @@ -94,7 +94,9 @@ struct PackageRequest<'a> { pub async fn run_speed_test(test_size_bytes: usize) -> Result { let timeout = Duration::from_secs(CLOUDFLARE_TIMEOUT_SECS); - let client = reqwest::Client::builder().timeout(timeout).build()?; + let client = orb_security_utils::reqwest::client_builder() + .timeout(timeout) + .build()?; let upload_result = probe_upload(&client, test_size_bytes, timeout).await?; let download_result = probe_download(&client, test_size_bytes, timeout).await?; @@ -333,7 +335,7 @@ async fn request_presigned_url( id_commitment: "mock_id_commitment", }; - let client = reqwest::Client::builder() + let client = orb_security_utils::reqwest::client_builder() .timeout(Duration::from_secs(PCP_TIMEOUT_SECS)) .build()?; @@ -380,7 +382,7 @@ async fn upload_to_presigned_url( form = form.part("file", file_part); - let client = reqwest::Client::builder() + let client = orb_security_utils::reqwest::client_builder() .timeout(Duration::from_secs(PCP_TIMEOUT_SECS)) .build()?; diff --git a/update-agent-loader/src/download.rs b/update-agent-loader/src/download.rs index 963ba7dd7..32fdd8062 100644 --- a/update-agent-loader/src/download.rs +++ b/update-agent-loader/src/download.rs @@ -74,6 +74,8 @@ pub fn download( } /// Creates an HTTP client with security settings similar to the update-agent +// update-agent-loader intentionally uses system certs rather than pinned CAs. +#[allow(clippy::disallowed_methods)] fn create_client() -> Result { // Compile-time assertion to ensure allow_http feature isn't enabled in release mode #[cfg(all(feature = "allow_http", not(debug_assertions)))] @@ -81,6 +83,8 @@ fn create_client() -> Result { let builder = Client::builder() .tls_built_in_root_certs(true) + .min_tls_version(reqwest::tls::Version::TLS_1_3) + .redirect(reqwest::redirect::Policy::none()) .user_agent(concat!( env!("CARGO_PKG_NAME"), "/", diff --git a/update-agent/src/client.rs b/update-agent/src/client.rs index 78f48b3ba..2fd2aa070 100644 --- a/update-agent/src/client.rs +++ b/update-agent/src/client.rs @@ -20,6 +20,8 @@ pub fn normal() -> Result<&'static Client, Error> { INSTANCE.get_or_try_init(initialize) } +// update-agent intentionally uses system certs rather than pinned CAs — see comment below. +#[allow(clippy::disallowed_methods)] fn initialize() -> Result { // We explicitly do not pin certificates and default to using the system's // root CAs in the update-agent.