forge_agent/
http_trust.rs1use std::fs;
19
20pub fn with_extra_roots(mut b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
28 for var in ["NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE"] {
29 let Ok(path) = std::env::var(var) else { continue };
30 if path.trim().is_empty() {
31 continue;
32 }
33 let (next, added) = with_roots_from(b, &path, var);
34 b = next;
35 if added > 0 {
38 break;
39 }
40 }
41 b
42}
43
44fn with_roots_from(
47 mut b: reqwest::ClientBuilder,
48 path: &str,
49 var: &str,
50) -> (reqwest::ClientBuilder, usize) {
51 let pem = match fs::read(path) {
52 Ok(pem) => pem,
53 Err(error) => {
54 tracing::warn!(path = %path, var, %error, "extra CA bundle unreadable");
55 return (b, 0);
56 }
57 };
58 match reqwest::Certificate::from_pem_bundle(&pem) {
59 Ok(certs) => {
60 let added = certs.len();
61 for cert in certs {
62 b = b.add_root_certificate(cert);
63 }
64 tracing::info!(path = %path, var, added, "loaded extra CA roots into reqwest");
65 (b, added)
66 }
67 Err(error) => {
68 tracing::warn!(path = %path, var, %error, "failed to parse CA bundle");
69 (b, 0)
70 }
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
80 fn a_valid_bundle_adds_its_certificates() {
81 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/extra-root.pem");
82
83 let (_, added) = with_roots_from(reqwest::Client::builder(), path, "NODE_EXTRA_CA_CERTS");
84
85 assert_eq!(added, 1);
86 }
87
88 #[test]
90 fn an_unreadable_bundle_adds_nothing() {
91 let (_, added) =
92 with_roots_from(reqwest::Client::builder(), "/nonexistent/ca.pem", "SSL_CERT_FILE");
93
94 assert_eq!(added, 0);
95 }
96
97 #[tokio::test]
100 async fn the_extra_roots_reach_the_client() {
101 use std::io::BufReader;
102 use tokio::io::{AsyncReadExt, AsyncWriteExt};
103 use tokio_rustls::TlsAcceptor;
104 use tokio_rustls::rustls::ServerConfig;
105
106 let fixtures = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
107 let root_path = format!("{fixtures}/extra-root.pem");
108 let cert_path = format!("{fixtures}/server.pem");
109 let key_path = format!("{fixtures}/server-key.pem");
110 let certs = rustls_pemfile::certs(&mut BufReader::new(
111 std::fs::File::open(&cert_path).expect("the certificate"),
112 ))
113 .collect::<Result<Vec<_>, _>>()
114 .expect("the certificate parses");
115 let key = rustls_pemfile::private_key(&mut BufReader::new(
116 std::fs::File::open(&key_path).expect("the key"),
117 ))
118 .expect("the key parses")
119 .expect("the key exists");
120 let config = ServerConfig::builder_with_provider(
123 tokio_rustls::rustls::crypto::aws_lc_rs::default_provider().into(),
124 )
125 .with_safe_default_protocol_versions()
126 .expect("the protocol versions")
127 .with_no_client_auth()
128 .with_single_cert(certs, key)
129 .expect("the server config");
130 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind");
131 let address = listener.local_addr().expect("addr");
132 let acceptor = TlsAcceptor::from(std::sync::Arc::new(config));
133 let server = tokio::spawn(async move {
134 let (socket, _) = listener.accept().await.expect("accept");
135 let mut stream = acceptor.accept(socket).await.expect("the handshake");
136 let mut buf = [0u8; 1024];
137 let _ = stream.read(&mut buf).await.expect("the request");
138 let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok").await;
139 });
140
141 let (builder, added) =
142 with_roots_from(reqwest::Client::builder(), &root_path, "NODE_EXTRA_CA_CERTS");
143 assert_eq!(added, 1);
144 let client = builder.build().expect("the client");
145 let response = client
146 .get(format!("https://127.0.0.1:{}/", address.port()))
147 .send()
148 .await
149 .expect("the extra root is trusted");
150
151 assert_eq!(response.text().await.expect("the body"), "ok");
152 server.await.expect("the server");
153 }
154}