Skip to content

Commit 4215ec7

Browse files
authored
load intermediate cert (#496)
* load intermediate cert * using rcgen to sign with intermediate cert, update unit test
1 parent 77a8e95 commit 4215ec7

2 files changed

Lines changed: 125 additions & 31 deletions

File tree

crates/integration-tests/src/test/tls_client_certificate.rs

Lines changed: 113 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use crate::kumod::{DaemonWithMaildir, DeliverySummary, MailGenParams};
22
use k9::assert_equal;
33
use kumo_log_types::RecordType;
4-
use rustls_cert_gen::{Ca, CertificateBuilder, EndEntity};
4+
use rcgen::{
5+
BasicConstraints, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
6+
ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose,
7+
};
58
use std::collections::BTreeMap;
69
use std::time::Duration;
710

@@ -39,17 +42,15 @@ async fn tls_client_certificate_openssl_no_client_cert() -> anyhow::Result<()> {
3942
/// Use rustls as TLS library and confirm delivery is successful
4043
#[tokio::test]
4144
async fn tls_client_certificate_rustls_success() -> anyhow::Result<()> {
42-
let (ca, entity) = generate_certs()?;
43-
let ca_pem = ca.serialize_pem().cert_pem;
44-
let cert_pem = entity.serialize_pem().cert_pem;
45-
let key_pem = entity.serialize_pem().private_key_pem;
45+
let (ca_pem, intermediate_pem, entity_pem, key_pem) = generate_certs()?;
46+
let cert_pem = format!("{entity_pem}\n{intermediate_pem}");
4647
let ex = DeliverySummary {
4748
source_counts: BTreeMap::from([(RecordType::Reception, 1), (RecordType::Delivery, 1)]),
4849
sink_counts: BTreeMap::from([(RecordType::Reception, 1), (RecordType::Delivery, 1)]),
4950
};
5051
let env = vec![
5152
("KUMOD_ENABLE_TLS", "OpportunisticInsecure"),
52-
("KUMOD_PREFER_OPENSSL", "true"),
53+
("KUMOD_PREFER_OPENSSL", "false"),
5354
("KUMOD_CLIENT_CERTIFICATE", &cert_pem),
5455
("KUMOD_CLIENT_PRIVATE_KEY", &key_pem),
5556
("KUMOD_CLIENT_REQUIRED_CA", &ca_pem),
@@ -60,8 +61,7 @@ async fn tls_client_certificate_rustls_success() -> anyhow::Result<()> {
6061
/// Adding fake private key to confirm rustls injection succeeds
6162
#[tokio::test]
6263
async fn tls_client_certificate_rustls_fail() -> anyhow::Result<()> {
63-
let (_ca, entity) = generate_certs()?;
64-
let cert_pem = entity.serialize_pem().cert_pem;
64+
let (_ca_pem, _intermediate_pem, cert_pem, _key_pem) = generate_certs()?;
6565
let ex = DeliverySummary {
6666
source_counts: BTreeMap::from([
6767
(RecordType::Reception, 1),
@@ -79,33 +79,72 @@ async fn tls_client_certificate_rustls_fail() -> anyhow::Result<()> {
7979

8080
const COMMON_NAME: &str = "Testing Common Name";
8181

82-
fn generate_certs() -> anyhow::Result<(Ca, EndEntity)> {
83-
let ca = CertificateBuilder::new()
84-
.certificate_authority()
85-
.country_name("GB")?
86-
.organization_name("kumo-testing")
87-
.build()?;
82+
fn generate_certs() -> anyhow::Result<(String, String, String, String)> {
83+
// Root CA
84+
let mut root_params = CertificateParams::default();
85+
root_params.distinguished_name = DistinguishedName::new();
86+
root_params
87+
.distinguished_name
88+
.push(DnType::CountryName, "GB");
89+
root_params
90+
.distinguished_name
91+
.push(DnType::OrganizationName, "kumo-testing");
92+
root_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
93+
root_params.key_usages = vec![
94+
KeyUsagePurpose::DigitalSignature,
95+
KeyUsagePurpose::KeyCertSign,
96+
KeyUsagePurpose::CrlSign,
97+
];
98+
let root_key = KeyPair::generate()?;
99+
let root_ca = CertifiedIssuer::self_signed(root_params, root_key)?;
100+
101+
// Intermediate CA (signed by root)
102+
let mut intermediate_params = CertificateParams::default();
103+
intermediate_params.distinguished_name = DistinguishedName::new();
104+
intermediate_params
105+
.distinguished_name
106+
.push(DnType::CountryName, "GB");
107+
intermediate_params
108+
.distinguished_name
109+
.push(DnType::OrganizationName, "kumo-intermediate-testing");
110+
intermediate_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
111+
intermediate_params.key_usages = vec![
112+
KeyUsagePurpose::DigitalSignature,
113+
KeyUsagePurpose::KeyCertSign,
114+
KeyUsagePurpose::CrlSign,
115+
];
116+
intermediate_params.use_authority_key_identifier_extension = true;
117+
let intermediate_key = KeyPair::generate()?;
118+
let intermediate_ca =
119+
CertifiedIssuer::signed_by(intermediate_params, intermediate_key, &root_ca)?;
88120

89-
let mut entity = CertificateBuilder::new()
90-
.end_entity()
91-
.common_name(COMMON_NAME)
92-
.subject_alternative_names(vec![rcgen::SanType::DnsName(
93-
"smtp.example.com".try_into().unwrap(),
94-
)]);
95-
entity.client_auth();
121+
// End entity certificate for client authentication (signed by intermediate)
122+
let mut entity_params = CertificateParams::new(vec!["smtp.example.com".to_string()])?;
123+
entity_params.distinguished_name = DistinguishedName::new();
124+
entity_params
125+
.distinguished_name
126+
.push(DnType::CommonName, COMMON_NAME);
127+
entity_params.is_ca = IsCa::NoCa;
128+
entity_params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
129+
entity_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
130+
entity_params.use_authority_key_identifier_extension = true;
96131

97-
let entity = entity.build(&ca)?;
132+
let entity_key = KeyPair::generate()?;
133+
let entity = entity_params.signed_by(&entity_key, &intermediate_ca)?;
98134

99-
Ok((ca, entity))
135+
Ok((
136+
root_ca.pem(),
137+
intermediate_ca.pem(),
138+
entity.pem(),
139+
entity_key.serialize_pem(),
140+
))
100141
}
101142

102143
/// Use openssl as TLS library, confirm delivery succeeds
103144
#[tokio::test]
104145
async fn tls_client_certificate_rustls_openssl_success() -> anyhow::Result<()> {
105-
let (ca, entity) = generate_certs()?;
106-
let ca_pem = ca.serialize_pem().cert_pem;
107-
let cert_pem = entity.serialize_pem().cert_pem;
108-
let key_pem = entity.serialize_pem().private_key_pem;
146+
let (ca_pem, intermediate_pem, entity_pem, key_pem) = generate_certs()?;
147+
let cert_pem = format!("{entity_pem}\n{intermediate_pem}");
109148

110149
let ex = DeliverySummary {
111150
source_counts: BTreeMap::from([(RecordType::Reception, 1), (RecordType::Delivery, 1)]),
@@ -122,11 +161,56 @@ async fn tls_client_certificate_rustls_openssl_success() -> anyhow::Result<()> {
122161
tls_client_certificate(env, ex).await
123162
}
124163

164+
/// Use openssl as TLS library, confirm we're failing to deliver due to missing intermediate certificate in the chain.
165+
#[tokio::test]
166+
async fn tls_client_certificate_rustls_openssl_missing_intermediate() -> anyhow::Result<()> {
167+
let (ca_pem, _intermediate_pem, entity_pem, key_pem) = generate_certs()?;
168+
169+
let ex = DeliverySummary {
170+
source_counts: BTreeMap::from([
171+
(RecordType::Reception, 1),
172+
(RecordType::TransientFailure, 1),
173+
]),
174+
sink_counts: BTreeMap::new(),
175+
};
176+
177+
let env = vec![
178+
("KUMOD_ENABLE_TLS", "OpportunisticInsecure"),
179+
("KUMOD_PREFER_OPENSSL", "true"),
180+
("KUMOD_CLIENT_CERTIFICATE", &entity_pem),
181+
("KUMOD_CLIENT_PRIVATE_KEY", &key_pem),
182+
("KUMOD_CLIENT_REQUIRED_CA", &ca_pem),
183+
];
184+
tls_client_certificate(env, ex).await
185+
}
186+
187+
/// Use rustls as TLS library, confirm we're failing to deliver due to missing intermediate certificate in the chain.
188+
#[tokio::test]
189+
async fn tls_client_certificate_rustls_missing_intermediate() -> anyhow::Result<()> {
190+
let (ca_pem, _intermediate_pem, entity_pem, key_pem) = generate_certs()?;
191+
192+
let ex = DeliverySummary {
193+
source_counts: BTreeMap::from([
194+
(RecordType::Reception, 1),
195+
(RecordType::TransientFailure, 1),
196+
]),
197+
sink_counts: BTreeMap::new(),
198+
};
199+
200+
let env = vec![
201+
("KUMOD_ENABLE_TLS", "OpportunisticInsecure"),
202+
("KUMOD_PREFER_OPENSSL", "false"),
203+
("KUMOD_CLIENT_CERTIFICATE", &entity_pem),
204+
("KUMOD_CLIENT_PRIVATE_KEY", &key_pem),
205+
("KUMOD_CLIENT_REQUIRED_CA", &ca_pem),
206+
];
207+
tls_client_certificate(env, ex).await
208+
}
209+
125210
/// Adding fake private key to confirm openssl injection would temp fail
126211
#[tokio::test]
127212
async fn tls_client_certificate_rustls_openssl_fail() -> anyhow::Result<()> {
128-
let (_ca, entity) = generate_certs()?;
129-
let cert_pem = entity.serialize_pem().cert_pem;
213+
let (_ca_pem, _intermediate_pem, cert_pem, _key_pem) = generate_certs()?;
130214
let ex = DeliverySummary {
131215
source_counts: BTreeMap::from([
132216
(RecordType::Reception, 1),

crates/kumo-tls-helper/src/lib.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,18 @@ impl TlsOptions {
261261
if let (Some(cert_data), Some(key_data)) =
262262
(&self.certificate_from_pem, &self.private_key_from_pem)
263263
{
264-
let cert = X509::from_pem(cert_data)?;
265-
builder.set_certificate(&cert)?;
264+
let certs = X509::stack_from_pem(cert_data)?;
265+
let Some(leaf) = certs.get(0).map(Clone::clone) else {
266+
return Err(OpensslConnectorError::SslErrorStack(
267+
"certificate PEM data is empty".to_string(),
268+
));
269+
};
270+
builder.set_certificate(&leaf)?;
271+
272+
// Add intermediates
273+
for cert in certs.iter().skip(1) {
274+
builder.add_extra_chain_cert(cert.clone())?;
275+
}
266276

267277
let key = PKey::private_key_from_pem(key_data)?;
268278
builder.set_private_key(&key)?;

0 commit comments

Comments
 (0)