Skip to content

Commit f6fd07f

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

File tree

4 files changed

+182
-5
lines changed

4 files changed

+182
-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: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@
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::{
22+
io::{BufRead, BufReader, Cursor},
23+
ops::Deref,
24+
vec,
25+
};
2126

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

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)