Skip to main content

redis_test/
utils.rs

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/// Client certificate and key paths for mTLS authentication
16#[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    // Based on shell script in redis's server tests
28    // https://github.com/redis/redis/blob/8c291b97b95f2e011977b522acf77ead23e26f55/utils/gen-test-certs.sh
29    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    // Build CA Key
51    make_key(&ca_key, 4096);
52
53    // Build redis key
54    make_key(&redis_key, 2048);
55
56    // Build CA Cert
57    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    // Build x509v3 extensions file
83    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    // Read redis key
112    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    // build redis cert
126    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
171/// Build a client certificate with a custom common name (CN) field
172/// Redis 8.6+ allows certificate-based authentication where the common name (CN)
173/// is mapped to an ACL username
174pub 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    // Generate client private key
185    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    // Create a basic extensions file for X.509 v3 client certificate
202    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    // Create certificate signing request with custom CN
211    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    // Sign the certificate with CA (X.509 v3)
225    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
273/// Finds a random open port available for listening at, by spawning a TCP server with
274/// port "zero" (which prompts the OS to just use any available port). Between calling
275/// this function and trying to bind to this port, the port may be given to another
276/// process, so this must be used with care (since here we only use it for tests, it's
277/// mostly okay).
278pub 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}