viceroy_lib/config/backends/
client_cert_info.rs

1use rustls::{Certificate, PrivateKey};
2use std::fmt;
3use std::io::{BufReader, Cursor};
4
5#[derive(Clone, PartialEq)]
6pub struct ClientCertInfo {
7    certificates: Vec<Certificate>,
8    key: PrivateKey,
9}
10
11impl fmt::Debug for ClientCertInfo {
12    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13        self.certs().fmt(f)
14    }
15}
16
17#[derive(Debug, thiserror::Error)]
18pub enum ClientCertError {
19    #[error("Certificate/key read error: {0}")]
20    CertificateRead(#[from] std::io::Error),
21    #[error("No keys found for client certificate")]
22    NoKeysFound,
23    #[error("Too many keys found for client certificate (found {0})")]
24    TooManyKeys(usize),
25    #[error("Expected a TOML table, found something else")]
26    InvalidToml,
27    #[error("No certificates found in client cert definition")]
28    NoCertsFound,
29    #[error("Expected a string value for key {0}, got something else")]
30    InvalidTomlData(&'static str),
31}
32
33impl ClientCertInfo {
34    pub fn new(certificate_bytes: &[u8], certificate_key: &[u8]) -> Result<Self, ClientCertError> {
35        let mut certificate_bytes_reader = Cursor::new(certificate_bytes);
36        let mut key_bytes_reader = Cursor::new(certificate_key);
37        let cert_info = rustls_pemfile::read_all(&mut certificate_bytes_reader)?;
38        let key_info = rustls_pemfile::read_all(&mut key_bytes_reader)?;
39
40        let mut certificates = Vec::new();
41        let mut keys = Vec::new();
42
43        for item in cert_info.into_iter().chain(key_info) {
44            match item {
45                rustls_pemfile::Item::X509Certificate(x) => certificates.push(Certificate(x)),
46                rustls_pemfile::Item::RSAKey(x) => keys.push(PrivateKey(x)),
47                rustls_pemfile::Item::PKCS8Key(x) => keys.push(PrivateKey(x)),
48                rustls_pemfile::Item::ECKey(x) => keys.push(PrivateKey(x)),
49                _ => {}
50            }
51        }
52
53        let key = if keys.is_empty() {
54            return Err(ClientCertError::NoKeysFound);
55        } else if keys.len() > 1 {
56            return Err(ClientCertError::TooManyKeys(keys.len()));
57        } else {
58            keys.remove(0)
59        };
60
61        Ok(ClientCertInfo { certificates, key })
62    }
63
64    pub fn certs(&self) -> Vec<Certificate> {
65        self.certificates.clone()
66    }
67
68    pub fn key(&self) -> PrivateKey {
69        self.key.clone()
70    }
71}
72
73fn inline_reader_for_field<'a>(
74    table: &'a toml::value::Table,
75    key: &'static str,
76) -> Result<Option<Cursor<&'a [u8]>>, ClientCertError> {
77    if let Some(base_field) = table.get(key) {
78        match base_field {
79            toml::Value::String(s) => Ok(Some(Cursor::new(s.as_bytes()))),
80            _ => Err(ClientCertError::InvalidTomlData(key)),
81        }
82    } else {
83        Ok(None)
84    }
85}
86
87fn file_reader_for_field(
88    table: &toml::value::Table,
89    key: &'static str,
90) -> Result<Option<BufReader<std::fs::File>>, ClientCertError> {
91    if let Some(base_field) = table.get(key) {
92        match base_field {
93            toml::Value::String(s) => {
94                let file = std::fs::File::open(s)?;
95                Ok(Some(BufReader::new(file)))
96            }
97            _ => Err(ClientCertError::InvalidTomlData(key)),
98        }
99    } else {
100        Ok(None)
101    }
102}
103
104fn read_certificates<R: std::io::BufRead>(
105    reader: &mut R,
106) -> Result<Vec<Certificate>, ClientCertError> {
107    rustls_pemfile::certs(reader)
108        .map(|mut x| x.drain(..).map(Certificate).collect::<Vec<Certificate>>())
109        .map_err(Into::into)
110}
111
112fn read_key<R: std::io::BufRead>(reader: &mut R) -> Result<PrivateKey, ClientCertError> {
113    for item in rustls_pemfile::read_all(reader)? {
114        match item {
115            rustls_pemfile::Item::RSAKey(x) => return Ok(PrivateKey(x)),
116            rustls_pemfile::Item::PKCS8Key(x) => return Ok(PrivateKey(x)),
117            rustls_pemfile::Item::ECKey(x) => return Ok(PrivateKey(x)),
118            _ => {}
119        }
120    }
121    Err(ClientCertError::NoKeysFound)
122}
123
124impl TryFrom<toml::Value> for ClientCertInfo {
125    type Error = ClientCertError;
126
127    fn try_from(value: toml::Value) -> Result<Self, Self::Error> {
128        match value {
129            toml::Value::Table(t) => {
130                let mut found_cert = None;
131                let mut found_key = None;
132
133                if let Some(mut reader) = inline_reader_for_field(&t, "certificate")? {
134                    found_cert = Some(read_certificates(&mut reader)?);
135                }
136
137                if let Some(mut reader) = file_reader_for_field(&t, "certificate_file")? {
138                    found_cert = Some(read_certificates(&mut reader)?);
139                }
140
141                if let Some(mut reader) = inline_reader_for_field(&t, "key")? {
142                    found_key = Some(read_key(&mut reader)?);
143                }
144
145                if let Some(mut reader) = file_reader_for_field(&t, "key_file")? {
146                    found_key = Some(read_key(&mut reader)?);
147                }
148
149                match (found_cert, found_key) {
150                    (None, _) => Err(ClientCertError::NoCertsFound),
151                    (_, None) => Err(ClientCertError::NoKeysFound),
152                    (Some(certificates), Some(key)) => Ok(ClientCertInfo { certificates, key }),
153                }
154            }
155            _ => Err(ClientCertError::InvalidToml),
156        }
157    }
158}
159
160#[test]
161fn client_certs_parse() {
162    let basic = r#"
163description = "a test case"
164language = "foul"
165manifest_version = 2
166
167[local_server]
168[local_server.backends]
169[local_server.backends.origin]
170url = "https://127.0.0.1:443"
171"#;
172
173    let basic_parsed = crate::config::FastlyConfig::from_str(basic).unwrap();
174    let basic_origin = basic_parsed.local_server.backends.0.get("origin").unwrap();
175    assert!(basic_origin.client_cert.is_none());
176
177    let files = r#"
178description = "a test case"
179language = "foul"
180manifest_version = 2
181
182[local_server]
183[local_server.backends]
184[local_server.backends.origin]
185url = "https://127.0.0.1:443"
186[local_server.backends.origin.client_certificate]
187certificate_file = "test-fixtures/data/client.crt"
188key_file = "test-fixtures/data/client.key"
189"#;
190
191    let files_parsed = crate::config::FastlyConfig::from_str(files).unwrap();
192    let files_origin = files_parsed.local_server.backends.0.get("origin").unwrap();
193    assert!(files_origin.client_cert.is_some());
194
195    let inline = r#"
196description = "a test case"
197language = "foul"
198manifest_version = 2
199
200[local_server]
201[local_server.backends]
202[local_server.backends.origin]
203url = "https://127.0.0.1:443"
204[local_server.backends.origin.client_certificate]
205key = """
206-----BEGIN RSA PRIVATE KEY-----
207MIIEpQIBAAKCAQEAz27x1GpD46K6b9/3PNyZYKgTL9GBbpLAVF8Uebd34ftUfnWZ
2083ER+x6A1YbacHnL112diPPevyYkpXuiujwCeswYNrZHEtiRfAvrzBRhnhL8owQTx
209jOcG4EOzR7Je556FTq8kNth5iHckORjmXiV9ZahbLv/zBFpkXpDeze62zd8y9chP
210NEqcrLZBOb4UoKXmOt1lIdeo23nysR4rC6XemWNSFcZv9zagUzliMeca3XN2RIUA
211FZv4o+gYPqqXQi+0a+OOq0jnKpawW+avn2UG7wzXGlLcVOvLe5BOCA1RfWtR8w03
212MFdvoBAesXJ4xGX1ROUzelldedmpqtvORdhmGQIDAQABAoIBAQCsbu6KhDehMDHJ
213NCWjK0I4zh78/iyZDVbiDBPKRpBag4GuifX329yD95LIgnNvAGOKxz8rrT4sy19f
214rQ8Ggx5pdVvDcExUmRF+Obvw/WN4PywSoBhn59iYbs7Gh+lKo0Tvrrns+bC1l0y+
215RguiMYn3CqeZ/1w1vyp2TflYuNqvcR4zMzJ4dN474CCLPIUX9OfK21Lbv/UMdguF
216Rs/BuStucqaCzEtTLyZYlxQc1i8S8Uy2yukXR6TYWJOsWZj0KIgH/YI7ZgzvTIxL
217ax4Hn4jIHPFSJ+vl2ehDKffkQQ0lzm60ASkjaJY6GsFoTQzsmuafpLIAoJbDbZR1
218txPSFC+BAoGBAPbp6+LsXoEY+4RfStg4c/oLWmK3aTxzQzMY90vxnMm6SJTwTPAm
219pO+Pp2UGyEGHV7hg3d+ItWpM9QGVmsjm+punIfc0W/0+AVUonjPLfv44dz7+geYt
220/oeMv4RTqCclROvtQTqV6hHn4E3Xg061miEe6OxYmqfZuLD2nv2VlsQRAoGBANcR
221GAqeClQtraTnu+yU9U+FJZfvSxs1yHr7XItCMtwxeU6+nipa+3pXNnKu0dKKekUG
222PCdUipXgggA6OUm2YFKPUhiXJUNoHCj45Tkv2NshGplW33U3NcCkDqL7vvZoBBfP
223OPxEVRVEIlwp/WzEambs9MjWoecEaOe7/3UCVumJAoGANlfVquQLCK7O7JtshZon
224LGlDQ2bKqptTtvNPuk87CssNHnqk9FYNBwy+8uVDPejjzZjEPGaCRxsY8XhT0NPF
225ZGysdRP5CwuSj4OZDh1DngAffqXVQSvuUTcRD7a506PIP4TATnygP8ChBYDhTXl6
226qr961EnMABVTKN+eroE15YECgYEAv+YLyqV71+KuNx9i6lV7kcnfYnNtU8koqruQ
227tt2Jnjoy4JVrcaWfEGmzNp9Qr4lKUj6e/AUOZ29c8DEDnwcxaVliynhLEptZzSFQ
228/zb3S4d9QWdnmiJ6Pvrj6H+yxBDJ3ijT0xxxwrj547y/2QZlXpN+U5pX+ldP974i
2290dgVjukCgYEArxv0dO2VEguWLx5YijHiN72nDDI+skbfkQkvWQjA7x8R9Xx1SWUl
230WeyeaaV5rqfJZF1wBCK5VJndjbOGhPh6u/0mpeYw4Ty3+CKN2WoikQO27qYfMZW5
231vvT7m9ZR+gkm2TjZ+pZuilz2gqu/yMJKl8Fi8Q7dsb8eWedWQXjbUZg=
232-----END RSA PRIVATE KEY-----
233"""
234certificate = """
235-----BEGIN CERTIFICATE-----
236MIIDvjCCAqagAwIBAgIUOp97gvMlYdBYI/3yrpDeHbdx5RgwDQYJKoZIhvcNAQEL
237BQAwZDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9y
238dGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxHzAdBgkqhkiG9w0BCQEWEGF3aWNrQGZh
239c3RseS5jb20wHhcNMjMwNzI3MDAxOTU0WhcNMzMwNzI0MDAxOTU0WjB1MQswCQYD
240VQQGEwJVUzEPMA0GA1UECAwGT3JlZ29uMREwDwYDVQQHDAhQb3J0bGFuZDEQMA4G
241A1UECgwHVmljZXJveTEPMA0GA1UECwwGQ2xpZW50MR8wHQYJKoZIhvcNAQkBFhBh
242d2lja0BmYXN0bHkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
243z27x1GpD46K6b9/3PNyZYKgTL9GBbpLAVF8Uebd34ftUfnWZ3ER+x6A1YbacHnL1
24412diPPevyYkpXuiujwCeswYNrZHEtiRfAvrzBRhnhL8owQTxjOcG4EOzR7Je556F
245Tq8kNth5iHckORjmXiV9ZahbLv/zBFpkXpDeze62zd8y9chPNEqcrLZBOb4UoKXm
246Ot1lIdeo23nysR4rC6XemWNSFcZv9zagUzliMeca3XN2RIUAFZv4o+gYPqqXQi+0
247a+OOq0jnKpawW+avn2UG7wzXGlLcVOvLe5BOCA1RfWtR8w03MFdvoBAesXJ4xGX1
248ROUzelldedmpqtvORdhmGQIDAQABo1cwVTAfBgNVHSMEGDAWgBRmDOh4T/Mmde3l
2498OZzn0Pe9btZfTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAaBgNVHREEEzARggls
250b2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBAJ84GzmmqsmmtqXcmZIH
251i644p8wIc/DXPqb7zzAVm9FXpFgW3mN4xu1JYWu+rb1sge8uIm7Vt5Isd4CZ89XI
252F2Q2DS/rKMQmjgSDReWm9G+qZROwuhNDzK85e73Rw2EdX6cXtAGR1h3IdOTIv1FC
253UElFER31U8i4J9pxUZF/FTzlPEA1agqMsO6hQlj/A9B6TtzL7SSxCFBBaFbNCLMC
254D/WCrIoklNV5TwutYG80EYZhJlfUJPDQBphkcetDBI0L/KL/n20bg8OR/epGD5++
255qKIulxf9iUR5QHm2fWKdTLOuADmV+lc925gIqGhFhjVvpNPOcdckecQUp3vCNu2/
256HrM=
257-----END CERTIFICATE-----
258"""
259"#;
260
261    let inline_parsed = crate::config::FastlyConfig::from_str(inline).unwrap();
262    let inline_origin = inline_parsed.local_server.backends.0.get("origin").unwrap();
263    assert!(inline_origin.client_cert.is_some());
264
265    assert_eq!(files_origin.client_cert, inline_origin.client_cert);
266}