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