1use std::net::{SocketAddr, TcpListener};
2use std::path::PathBuf;
3use std::{fs, process};
4
5use socket2::{Domain, Socket, Type};
6use tempfile::TempDir;
7
8#[derive(Clone, Debug)]
9pub struct TlsFilePaths {
10 pub redis_crt: PathBuf,
11 pub redis_key: PathBuf,
12 pub ca_crt: PathBuf,
13}
14
15#[derive(Clone, Debug)]
17pub struct ClientCertPaths {
18 pub client_crt: PathBuf,
19 pub client_key: PathBuf,
20}
21
22pub fn build_keys_and_certs_for_tls(tempdir: &TempDir) -> TlsFilePaths {
23 build_keys_and_certs_for_tls_ext(tempdir, true)
24}
25
26pub fn build_keys_and_certs_for_tls_ext(tempdir: &TempDir, with_ip_alts: bool) -> TlsFilePaths {
27 let ca_crt = tempdir.path().join("ca.crt");
30 let ca_key = tempdir.path().join("ca.key");
31 let ca_serial = tempdir.path().join("ca.txt");
32 let redis_crt = tempdir.path().join("redis.crt");
33 let redis_key = tempdir.path().join("redis.key");
34 let ext_file = tempdir.path().join("openssl.cnf");
35
36 fn make_key<S: AsRef<std::ffi::OsStr>>(name: S, size: usize) {
37 process::Command::new("openssl")
38 .arg("genrsa")
39 .arg("-out")
40 .arg(name)
41 .arg(format!("{size}"))
42 .stdout(process::Stdio::piped())
43 .stderr(process::Stdio::piped())
44 .spawn()
45 .expect("failed to spawn openssl")
46 .wait()
47 .expect("failed to create key");
48 }
49
50 make_key(&ca_key, 4096);
52
53 make_key(&redis_key, 2048);
55
56 let status = process::Command::new("openssl")
58 .arg("req")
59 .arg("-x509")
60 .arg("-new")
61 .arg("-nodes")
62 .arg("-sha256")
63 .arg("-key")
64 .arg(&ca_key)
65 .arg("-days")
66 .arg("3650")
67 .arg("-subj")
68 .arg("/O=Redis Test/CN=Certificate Authority")
69 .arg("-out")
70 .arg(&ca_crt)
71 .stdout(process::Stdio::piped())
72 .stderr(process::Stdio::piped())
73 .spawn()
74 .expect("failed to spawn openssl")
75 .wait()
76 .expect("failed to create CA cert");
77 assert!(
78 status.success(),
79 "`openssl req` failed to create CA cert: {status}"
80 );
81
82 let ext = if with_ip_alts {
84 "\
85 keyUsage = digitalSignature, keyEncipherment\n\
86 subjectAltName = @alt_names\n\
87 [alt_names]\n\
88 IP.1 = 127.0.0.1\n\
89 "
90 } else {
91 "\
92 [req]\n\
93 distinguished_name = req_distinguished_name\n\
94 x509_extensions = v3_req\n\
95 prompt = no\n\
96 \n\
97 [req_distinguished_name]\n\
98 CN = localhost.example.com\n\
99 \n\
100 [v3_req]\n\
101 basicConstraints = CA:FALSE\n\
102 keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n\
103 subjectAltName = @alt_names\n\
104 \n\
105 [alt_names]\n\
106 DNS.1 = localhost.example.com\n\
107 "
108 };
109 fs::write(&ext_file, ext).expect("failed to create x509v3 extensions file");
110
111 let mut key_cmd = process::Command::new("openssl")
113 .arg("req")
114 .arg("-new")
115 .arg("-sha256")
116 .arg("-subj")
117 .arg("/O=Redis Test/CN=Generic-cert")
118 .arg("-key")
119 .arg(&redis_key)
120 .stdout(process::Stdio::piped())
121 .stderr(process::Stdio::piped())
122 .spawn()
123 .expect("failed to spawn openssl");
124
125 let mut command2 = process::Command::new("openssl");
127 command2
128 .arg("x509")
129 .arg("-req")
130 .arg("-sha256")
131 .arg("-CA")
132 .arg(&ca_crt)
133 .arg("-CAkey")
134 .arg(&ca_key)
135 .arg("-CAserial")
136 .arg(&ca_serial)
137 .arg("-CAcreateserial")
138 .arg("-days")
139 .arg("365")
140 .arg("-extfile")
141 .arg(&ext_file);
142 if !with_ip_alts {
143 command2.arg("-extensions").arg("v3_req");
144 }
145 let status2 = command2
146 .arg("-out")
147 .arg(&redis_crt)
148 .stdin(key_cmd.stdout.take().expect("should have stdout"))
149 .spawn()
150 .expect("failed to spawn openssl")
151 .wait()
152 .expect("failed to create redis cert");
153
154 let status = key_cmd.wait().expect("failed to create redis key");
155 assert!(
156 status.success(),
157 "`openssl req` failed to create request for Redis cert: {status}"
158 );
159 assert!(
160 status2.success(),
161 "`openssl x509` failed to create Redis cert: {status2}"
162 );
163
164 TlsFilePaths {
165 redis_crt,
166 redis_key,
167 ca_crt,
168 }
169}
170
171pub fn build_client_cert_with_custom_cn(
175 tempdir: &TempDir,
176 common_name: &str,
177 ca_crt: &PathBuf,
178 ca_key: &PathBuf,
179) -> ClientCertPaths {
180 let client_crt = tempdir.path().join(format!("{}.crt", common_name));
181 let client_key = tempdir.path().join(format!("{}.key", common_name));
182 let ca_serial = tempdir.path().join("ca.txt");
183
184 let status = process::Command::new("openssl")
186 .arg("genrsa")
187 .arg("-out")
188 .arg(&client_key)
189 .arg("2048")
190 .stdout(process::Stdio::piped())
191 .stderr(process::Stdio::piped())
192 .spawn()
193 .expect("failed to spawn openssl")
194 .wait()
195 .expect("failed to create client key");
196 assert!(
197 status.success(),
198 "`openssl genrsa` failed to create client key: {status}"
199 );
200
201 let client_ext_file = tempdir.path().join("client_ext.cnf");
203 let client_ext_content = "\
204 basicConstraints = CA:FALSE\n\
205 keyUsage = digitalSignature, keyEncipherment\n\
206 ";
207 fs::write(&client_ext_file, client_ext_content)
208 .expect("failed to create client extensions file");
209
210 let mut csr_cmd = process::Command::new("openssl")
212 .arg("req")
213 .arg("-new")
214 .arg("-sha256")
215 .arg("-subj")
216 .arg(format!("/O=Redis Test/CN={}", common_name))
217 .arg("-key")
218 .arg(&client_key)
219 .stdout(process::Stdio::piped())
220 .stderr(process::Stdio::piped())
221 .spawn()
222 .expect("failed to spawn openssl for CSR");
223
224 let cert_status = process::Command::new("openssl")
226 .arg("x509")
227 .arg("-req")
228 .arg("-sha256")
229 .arg("-CA")
230 .arg(ca_crt)
231 .arg("-CAkey")
232 .arg(ca_key)
233 .arg("-CAserial")
234 .arg(&ca_serial)
235 .arg("-CAcreateserial")
236 .arg("-days")
237 .arg("365")
238 .arg("-extfile")
239 .arg(&client_ext_file)
240 .arg("-out")
241 .arg(&client_crt)
242 .stdin(csr_cmd.stdout.take().expect("should have stdout"))
243 .spawn()
244 .expect("failed to spawn openssl for certificate signing")
245 .wait()
246 .expect("failed to sign client certificate");
247
248 let csr_status = csr_cmd.wait().expect("failed to create CSR");
249 assert!(
250 csr_status.success(),
251 "`openssl req` failed to create CSR: {csr_status}"
252 );
253 assert!(
254 cert_status.success(),
255 "`openssl x509` failed to sign client certificate"
256 );
257
258 ClientCertPaths {
259 client_crt,
260 client_key,
261 }
262}
263
264pub fn get_listener_on_free_port() -> TcpListener {
265 let addr = &"127.0.0.1:0".parse::<SocketAddr>().unwrap().into();
266 let socket = Socket::new(Domain::IPV4, Type::STREAM, None).unwrap();
267 socket.set_reuse_address(true).unwrap();
268 socket.bind(addr).unwrap();
269 socket.listen(1).unwrap();
270 TcpListener::from(socket)
271}
272
273pub fn get_random_available_port() -> u16 {
279 for _ in 0..10000 {
280 let listener = get_listener_on_free_port();
281 let port = listener.local_addr().unwrap().port();
282 if port < 55535 {
283 return port;
284 }
285 }
286 panic!("Couldn't get a valid port");
287}