Skip to content

Commit df01509

Browse files
committed
Add Certificate newtype that supports CA chains.
1 parent 6bacb1d commit df01509

File tree

4 files changed

+178
-5
lines changed

4 files changed

+178
-5
lines changed

.ci/certs/ca-chain.crt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
ISRG Root X1 (self-signed):
2+
-----BEGIN CERTIFICATE-----
3+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
4+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
5+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
6+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
7+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
8+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
9+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
10+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
11+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
12+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
13+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
14+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
15+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
16+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
17+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
18+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
19+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
20+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
21+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
22+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
23+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
24+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
25+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
26+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
27+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
28+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
29+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
30+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
31+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
32+
-----END CERTIFICATE-----
33+
Let's Encrypt Authority X3 (Signed by ISRG Root X1):
34+
-----BEGIN CERTIFICATE-----
35+
MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw
36+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
37+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1
38+
WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
39+
RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi
40+
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX
41+
NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf
42+
89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl
43+
Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc
44+
Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz
45+
uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB
46+
AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU
47+
BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB
48+
FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo
49+
SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js
50+
LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF
51+
BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG
52+
AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD
53+
VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB
54+
ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx
55+
A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM
56+
UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2
57+
DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1
58+
eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu
59+
OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw
60+
p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY
61+
2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0
62+
ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR
63+
PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b
64+
rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt
65+
-----END CERTIFICATE-----

elasticsearch/src/cert.rs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
//! Certificate components
20-
pub use reqwest::Certificate;
19+
20+
use crate::error::Error;
21+
use std::{io::BufRead, ops::Deref, vec};
2122

2223
/// Validation applied to a SSL/TLS certificate, to establish a HTTPS connection.
2324
///
@@ -182,3 +183,88 @@ pub enum CertificateValidation {
182183
/// attempting to resolve TLS errors, and **its use on production clusters is strongly discouraged**.
183184
None,
184185
}
186+
187+
/// Start marker for PEM encoded certificates.
188+
const BEGIN_CERTIFICATE: &str = "-----BEGIN CERTIFICATE-----";
189+
190+
/// End marker for PEM encoded certificates.
191+
const END_CERTIFICATE: &str = "-----END CERTIFICATE-----";
192+
193+
/// Represents a server X509 certificate chain.
194+
pub struct Certificate(Vec<reqwest::Certificate>);
195+
196+
impl Certificate {
197+
/// Create a `Certificate` chain from PEM encoded certificates.
198+
///
199+
/// The `pem` input data may contain one or more PEM encoded CA certificates.
200+
///
201+
/// # Optional
202+
/// This requires the `native-tls`, or `rustls-tls` feature to be enabled.
203+
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
204+
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
205+
let reader = std::io::BufReader::new(std::io::Cursor::new(pem));
206+
207+
// Split the PEM cert into parts without validating the
208+
// contents as this will be done by the
209+
// `reqwest::Certificate::from_pem` call itself.
210+
let mut certs = Vec::new();
211+
let mut cert = Vec::new();
212+
let mut begin = false;
213+
for line in reader.lines() {
214+
let line = line?;
215+
match line.as_ref() {
216+
BEGIN_CERTIFICATE if !begin => {
217+
begin = true;
218+
cert.push(line);
219+
}
220+
END_CERTIFICATE if begin => {
221+
begin = false;
222+
cert.push(line);
223+
certs.push(reqwest::Certificate::from_pem(cert.join("\n").as_bytes())?);
224+
cert = Vec::new();
225+
}
226+
_ if begin => cert.push(line),
227+
_ => {}
228+
}
229+
}
230+
231+
if certs.is_empty() {
232+
Err(Error::lib(
233+
"could not find PEM certificate in input data".to_string(),
234+
))
235+
} else {
236+
Ok(Self(certs))
237+
}
238+
}
239+
240+
/// Create a `Certificate` from a binary DER encoded certificate.
241+
///
242+
/// # Optional
243+
/// This requires the `native-tls`, or `rustls-tls` feature to be enabled.
244+
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
245+
pub fn from_der(der: &[u8]) -> Result<Self, Error> {
246+
Ok(Self(vec![reqwest::Certificate::from_der(der)?]))
247+
}
248+
249+
/// Append a `Certificate` to the chain.
250+
pub fn append(&mut self, mut cert: Self) {
251+
self.0.append(&mut cert.0);
252+
}
253+
}
254+
255+
impl IntoIterator for Certificate {
256+
type Item = reqwest::Certificate;
257+
type IntoIter = vec::IntoIter<Self::Item>;
258+
259+
fn into_iter(self) -> Self::IntoIter {
260+
self.0.into_iter()
261+
}
262+
}
263+
264+
impl Deref for Certificate {
265+
type Target = Vec<reqwest::Certificate>;
266+
267+
fn deref(&self) -> &Self::Target {
268+
&self.0
269+
}
270+
}

elasticsearch/src/http/transport.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,17 @@ impl TransportBuilder {
216216
client_builder = match v {
217217
CertificateValidation::Default => client_builder,
218218
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
219-
CertificateValidation::Full(c) => client_builder.add_root_certificate(c),
219+
CertificateValidation::Full(chain) => {
220+
chain.into_iter().fold(client_builder, |client_builder, c| {
221+
client_builder.add_root_certificate(c)
222+
})
223+
}
220224
#[cfg(feature = "native-tls")]
221-
CertificateValidation::Certificate(c) => client_builder
222-
.add_root_certificate(c)
225+
CertificateValidation::Certificate(chain) => chain
226+
.into_iter()
227+
.fold(client_builder, |client_builder, c| {
228+
client_builder.add_root_certificate(c)
229+
})
223230
.danger_accept_invalid_hostnames(true),
224231
CertificateValidation::None => client_builder.danger_accept_invalid_certs(true),
225232
}

elasticsearch/tests/cert.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use os_type::OSType;
2727
// TODO: These tests require a cluster configured with Security. Figure out best way to surface this e.g. test category, naming convention, etc.
2828

2929
static CA_CERT: &[u8] = include_bytes!("../../.ci/certs/ca.crt");
30+
static CA_CHAIN_CERT: &[u8] = include_bytes!("../../.ci/certs/ca-chain.crt");
3031
static TESTNODE_SAN_CERT: &[u8] = include_bytes!("../../.ci/certs/testnode_san.crt");
3132
static TESTNODE_CERT: &[u8] = include_bytes!("../../.ci/certs/testnode.crt");
3233

@@ -90,6 +91,20 @@ async fn full_certificate_ca_validation() -> Result<(), failure::Error> {
9091
Ok(())
9192
}
9293

94+
/// Try to load a certificate chain.
95+
#[tokio::test]
96+
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
97+
async fn full_certificate_ca_chain_validation() -> Result<(), failure::Error> {
98+
let mut cert = Certificate::from_pem(CA_CHAIN_CERT)?;
99+
cert.append(Certificate::from_pem(CA_CERT)?);
100+
assert_eq!(cert.len(), 3, "expected three certificates in CA chain");
101+
let builder =
102+
client::create_default_builder().cert_validation(CertificateValidation::Full(cert));
103+
let client = client::create(builder);
104+
let _response = client.ping().send().await?;
105+
Ok(())
106+
}
107+
93108
/// Certificate provided by the server is the one given to the client and hostname matches
94109
#[tokio::test]
95110
#[cfg(all(windows, any(feature = "native-tls", feature = "rustls-tls")))]

0 commit comments

Comments
 (0)