test_cert_gen/
lib.rs

1//! Utilities to generate keys for tests.
2//!
3//! Uses OpenSSL command line utility to generate the certificates.
4
5use std::fs;
6use std::io::Read;
7use std::io::Write;
8use std::process::Command;
9use std::process::Stdio;
10
11use tempfile::Builder as TempBuilder;
12
13pub use cert::pem_to_cert_key_pair;
14pub use cert::Cert;
15pub use cert::Pkcs12;
16pub use cert::Pkcs12AndPassword;
17pub use cert::PrivateKey;
18use once_cell::sync::Lazy;
19
20mod cert;
21
22#[derive(Debug, PartialEq, Clone)]
23pub struct CertAndPrivateKey {
24    pub cert: Cert,
25    pub key: PrivateKey,
26}
27
28impl CertAndPrivateKey {
29    /// Incorrect because key is serialized incorrectly
30    pub fn to_pem_incorrect(&self) -> String {
31        self.cert.to_pem() + &self.key.to_pem_incorrect()
32    }
33}
34
35/// Client certificate
36pub struct ClientKeys {
37    pub ca: Cert,
38}
39
40/// Server keys
41pub struct ServerKeys {
42    /// Server certificate
43    pub cert_and_key_pkcs12: Pkcs12AndPassword,
44
45    /// Server certificate
46    pub cert_and_key: CertAndPrivateKey,
47}
48
49/// Client and server keys
50pub struct Keys {
51    /// Client keys
52    pub client: ClientKeys,
53    /// Server keys
54    pub server: ServerKeys,
55}
56
57fn gen_root_ca() -> CertAndPrivateKey {
58    let temp_dir = TempBuilder::new()
59        .prefix("rust-test-cert-gen-gen-root-ca")
60        .tempdir()
61        .unwrap();
62
63    let config = temp_dir.path().join("openssl.config");
64    let keyfile = temp_dir.path().join("root_ca.key");
65    let certfile = temp_dir.path().join("root_ca.crt");
66
67    fs::write(
68        &config,
69        b"\
70                [req]\n\
71                distinguished_name=dn\n\
72                [dn]\n\
73                CN=my.ca\n\
74                [ext]\n\
75                basicConstraints=CA:TRUE,pathlen:0\n\
76                subjectAltName = @alt_names\n\
77                extendedKeyUsage=serverAuth,clientAuth\n\
78                [alt_names]\n\
79                DNS.1 = my.ca\n\
80            ",
81    )
82    .unwrap();
83
84    let subj = "/C=US/ST=Denial/L=Sprintfield/O=Dis/CN=my.ca";
85    // Making root CA
86    let gen_ca = Command::new("openssl")
87        .arg("req")
88        .arg("-nodes")
89        .arg("-x509")
90        .args(["-newkey", "rsa:2048"])
91        .arg("-config")
92        .arg(&config)
93        .args(["-extensions", "ext"])
94        .arg("-subj")
95        .arg(subj)
96        .arg("-keyout")
97        .arg(&keyfile)
98        .arg("-out")
99        .arg(&certfile)
100        .args(["-days", "1"])
101        // TODO: print on error
102        // .stderr(Stdio::inherit())
103        .output()
104        .unwrap();
105    assert!(gen_ca.status.success());
106
107    let cert = fs::read_to_string(&certfile).unwrap();
108    let key = fs::read_to_string(&keyfile).unwrap();
109
110    assert_eq!(1, pem::parse_many(cert.as_bytes()).len());
111    assert_eq!(1, pem::parse_many(key.as_bytes()).len());
112
113    CertAndPrivateKey {
114        cert: Cert::from_pem(&cert),
115        key: PrivateKey::from_pem(&key),
116    }
117}
118
119fn gen_cert_for_domain(domain: &str, ca: &CertAndPrivateKey) -> CertAndPrivateKey {
120    assert!(!domain.is_empty());
121
122    let temp_dir = TempBuilder::new().prefix("pem-to-der").tempdir().unwrap();
123    let privkey_pem_path = temp_dir.path().join("privkey.pem");
124    let csr = temp_dir.path().join("csr.pem");
125    let ca_pem = temp_dir.path().join("ca.pem");
126    let ca_key_path = temp_dir.path().join("ca-key.pem");
127    let cert_path = temp_dir.path().join("cert.pem");
128    let conf_path = temp_dir.path().join("conf");
129    let conf2_path = temp_dir.path().join("conf2");
130
131    fs::write(&ca_pem, ca.cert.to_pem()).unwrap();
132    fs::write(&ca_key_path, ca.key.to_pem_incorrect()).unwrap();
133
134    assert!(Command::new("openssl")
135        .arg("genrsa")
136        .arg("-out")
137        .arg(&privkey_pem_path)
138        .arg("2048")
139        .output()
140        .unwrap()
141        .status
142        .success());
143
144    fs::write(
145        &conf_path,
146        format!(
147            "\
148            [req]\n\
149            req_extensions = v3_req\n\
150            distinguished_name = req_distinguished_name\n\
151            [v3_req]\n\
152            basicConstraints = CA:FALSE\n\
153            keyUsage = digitalSignature, keyEncipherment\n\
154            extendedKeyUsage = serverAuth\n\
155            subjectAltName = DNS.0:{}\n\
156            [req_distinguished_name]\n\
157            # empty\n\
158        ",
159            domain
160        ),
161    )
162    .unwrap();
163
164    // CSR
165    assert!(Command::new("openssl")
166        .arg("req")
167        .arg("-new")
168        .arg("-key")
169        .arg(&privkey_pem_path)
170        .arg("-sha256")
171        .arg("-out")
172        .arg(&csr)
173        .args([
174            "-subj",
175            &format!("/C=US/ST=Utah/L=Provo/O=ACME Service/CN={}", domain)
176        ])
177        .arg("-config")
178        .arg(&conf_path)
179        .stderr(Stdio::inherit())
180        .output()
181        .unwrap()
182        .status
183        .success());
184
185    fs::write(
186        &conf2_path,
187        format!(
188            "\
189                subjectAltName = DNS.0:{}\n\
190                extendedKeyUsage = serverAuth\n\
191              ",
192            domain
193        ),
194    )
195    .unwrap();
196
197    // Sign the request from Server with your Root CA
198    assert!(Command::new("openssl")
199        .arg("x509")
200        .arg("-req")
201        .arg("-in")
202        .arg(&csr)
203        .arg("-CA")
204        .arg(&ca_pem)
205        .arg("-CAkey")
206        .arg(&ca_key_path)
207        .arg("-CAcreateserial")
208        .arg("-extfile")
209        .arg(&conf2_path)
210        .arg("-out")
211        .arg(&cert_path)
212        .args(["-days", "1"])
213        .arg("-sha256")
214        .output()
215        .unwrap()
216        .status
217        .success());
218
219    let key = fs::read_to_string(&privkey_pem_path).unwrap();
220    let cert = fs::read_to_string(&cert_path).unwrap();
221
222    // verify
223    assert_eq!(1, pem::parse_many(cert.as_bytes()).len());
224    assert_eq!(1, pem::parse_many(key.as_bytes()).len());
225
226    CertAndPrivateKey {
227        cert: Cert::from_pem(&cert),
228        key: PrivateKey::from_pem(&key),
229    }
230}
231
232pub fn gen_keys() -> Keys {
233    let root_ca_pem = gen_root_ca();
234
235    let server_cert_pem = gen_cert_for_domain("localhost", &root_ca_pem);
236
237    let server_cert_pkcs12 = pem_to_pkcs12_some_password(&server_cert_pem);
238
239    Keys {
240        client: ClientKeys {
241            ca: root_ca_pem.cert,
242        },
243        server: ServerKeys {
244            cert_and_key: server_cert_pem,
245            cert_and_key_pkcs12: server_cert_pkcs12,
246        },
247    }
248}
249
250/// Generate keys
251pub fn keys() -> &'static Keys {
252    static KEYS: Lazy<Keys> = Lazy::new(|| gen_keys());
253    &KEYS
254}
255
256fn _pkcs12_to_pem(pkcs12: &Pkcs12, passin: &str) -> String {
257    let mut command = Command::new("openssl")
258        .arg("pkcs12")
259        .args(["-passin", &format!("pass:{}", passin)])
260        .arg("-nodes")
261        .stdin(Stdio::piped())
262        .stdout(Stdio::piped())
263        .spawn()
264        .unwrap();
265
266    command
267        .stdin
268        .as_mut()
269        .unwrap()
270        .write_all(&pkcs12.0)
271        .unwrap();
272
273    let mut pem = String::new();
274    command
275        .stdout
276        .as_mut()
277        .unwrap()
278        .read_to_string(&mut pem)
279        .unwrap();
280
281    assert!(command.wait().unwrap().success());
282
283    pem
284}
285
286fn pem_to_pkcs12(cert: &CertAndPrivateKey, pass: &str) -> Pkcs12 {
287    let temp_dir = TempBuilder::new()
288        .prefix("pem-to-pkcs12")
289        .tempdir()
290        .unwrap();
291
292    let certfile = temp_dir.path().join("cert.pem");
293    let keyfile = temp_dir.path().join("key.pem");
294
295    fs::write(&certfile, cert.cert.to_pem()).unwrap();
296    fs::write(&keyfile, cert.key.to_pem_incorrect()).unwrap();
297
298    let pkcs12out = Command::new("openssl")
299        .arg("pkcs12")
300        .arg("-export")
301        .arg("-descert")
302        .arg("-inkey")
303        .arg(&keyfile)
304        .arg("-in")
305        .arg(&certfile)
306        .args(["-password", &format!("pass:{}", pass)])
307        .output()
308        .unwrap();
309    assert!(pkcs12out.status.success());
310    Pkcs12(pkcs12out.stdout)
311}
312
313fn pem_to_pkcs12_some_password(cert: &CertAndPrivateKey) -> Pkcs12AndPassword {
314    let password = "serp".to_owned();
315    let pkcs12 = pem_to_pkcs12(cert, &password);
316    Pkcs12AndPassword { pkcs12, password }
317}
318
319#[cfg(test)]
320mod test {
321    use crate::gen_keys;
322    use std::fs;
323    use std::io::BufRead;
324    use std::io::BufReader;
325    use std::io::Write;
326    use std::process::Command;
327    use std::process::Stdio;
328    use std::sync::mpsc;
329    use std::thread;
330
331    use tempfile::Builder as TempBuilder;
332
333    #[test]
334    fn test() {
335        // just check it does something
336        super::keys();
337    }
338
339    #[test]
340    fn verify() {
341        let temp_dir = TempBuilder::new().prefix("t").tempdir().unwrap();
342
343        let keys = gen_keys();
344
345        let ca_pem = temp_dir.path().join("ca.pem");
346        let server_pem = temp_dir.path().join("server.pem");
347
348        fs::write(&ca_pem, keys.client.ca.to_pem()).unwrap();
349        fs::write(&server_pem, &keys.server.cert_and_key.to_pem_incorrect()).unwrap();
350
351        // error is, what does it mean?
352        // ```
353        // error 18 at 0 depth lookup:self signed certificate
354        // ```
355        let status = Command::new("openssl")
356            .arg("verify")
357            .arg("-CAfile")
358            .arg(&ca_pem)
359            .arg(&server_pem)
360            .stderr(Stdio::inherit())
361            .spawn()
362            .unwrap()
363            .wait()
364            .unwrap();
365        assert!(status.success())
366    }
367
368    #[test]
369    #[ignore] // TODO: hangs on CI
370    fn client_server() {
371        let temp_dir = TempBuilder::new()
372            .prefix("client_server")
373            .tempdir()
374            .unwrap();
375
376        let keys = gen_keys();
377
378        let client = temp_dir.path().join("client");
379        let server = temp_dir.path().join("server.pem");
380
381        fs::write(&client, keys.client.ca.get_der()).unwrap();
382        fs::write(&server, keys.server.cert_and_key.to_pem_incorrect()).unwrap();
383
384        let port = 1234;
385
386        let mut s_server = Command::new("openssl")
387            .arg("s_server")
388            .arg("-accept")
389            .arg(port.to_string())
390            .arg("-cert")
391            .arg(&server)
392            .stdin(Stdio::piped())
393            .stdout(Stdio::piped())
394            .spawn()
395            .unwrap();
396
397        let (signal_tx, signal_rx) = mpsc::channel();
398
399        let client = thread::spawn(move || {
400            let mut s_client = Command::new("openssl")
401                .arg("s_client")
402                .stdin(Stdio::piped())
403                .stdout(Stdio::piped())
404                .arg("-connect")
405                .arg(format!("localhost:{}", port))
406                .arg("-verify_return_error")
407                .spawn()
408                .unwrap();
409            s_client
410                .stdin
411                .as_mut()
412                .unwrap()
413                .write_all(b"ping\n")
414                .unwrap();
415            let _ = signal_rx.recv();
416            s_client.kill().unwrap();
417        });
418
419        let lines = BufReader::new(s_server.stdout.as_mut().unwrap()).lines();
420        for line in lines {
421            let line = line.unwrap();
422            println!("> {}", line);
423            if line == "ping" {
424                break;
425            }
426        }
427
428        s_server.kill().unwrap();
429        let _ = signal_tx.send(());
430        client.join().unwrap();
431    }
432}