Skip to content

Commit 07588a9

Browse files
committed
feat: ✨ include certificate chain for client and server certs
1 parent 8ac56e2 commit 07588a9

File tree

4 files changed

+125
-16
lines changed

4 files changed

+125
-16
lines changed

test-certs/src/configuration/certificates.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ pub struct ClientConfiguration {
3535
#[serde(default = "ClientConfiguration::default_export_key")]
3636
pub export_key: bool,
3737

38+
/// Includes all public certificates that are required for this certificate to be validated.
39+
#[serde(default = "ClientConfiguration::default_include_certificate_chain")]
40+
pub include_certificate_chain: bool,
41+
3842
/// Properties that will be set as Subject Alternative Names (SAN)s.
3943
#[serde(flatten)]
4044
pub subject_alternative_names: SubjectAlternativeNames,
@@ -48,6 +52,10 @@ pub struct ServerConfiguration {
4852
#[serde(default = "ServerConfiguration::default_export_key")]
4953
pub export_key: bool,
5054

55+
/// Includes all public certificates that are required for this certificate to be validated.
56+
#[serde(default = "ServerConfiguration::default_include_certificate_chain")]
57+
pub include_certificate_chain: bool,
58+
5159
/// Properties that will be set as Subject Alternative Names (SAN)s.
5260
#[serde(flatten)]
5361
pub subject_alternative_names: SubjectAlternativeNames,
@@ -117,12 +125,18 @@ impl ServerConfiguration {
117125
fn default_export_key() -> bool {
118126
true
119127
}
128+
fn default_include_certificate_chain() -> bool {
129+
true
130+
}
120131
}
121132

122133
impl ClientConfiguration {
123134
fn default_export_key() -> bool {
124135
true
125136
}
137+
fn default_include_certificate_chain() -> bool {
138+
true
139+
}
126140
}
127141
/// Fixtures for testing certificate generation.
128142
#[cfg(any(test, feature = "fixtures"))]
@@ -181,6 +195,7 @@ pub mod fixtures {
181195
ip: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
182196
dns_name: vec!["my-client.org".to_string()],
183197
},
198+
include_certificate_chain: ClientConfiguration::default_include_certificate_chain(),
184199
})
185200
}
186201

@@ -192,6 +207,7 @@ pub mod fixtures {
192207
ip: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
193208
dns_name: vec!["my-server.org".to_string()],
194209
},
210+
include_certificate_chain: ServerConfiguration::default_include_certificate_chain(),
195211
})
196212
}
197213
}
@@ -290,11 +306,13 @@ mod tests {
290306
ip: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))],
291307
dns_name: vec!["my-client.org".to_string()],
292308
},
309+
include_certificate_chain: false,
293310
};
294311
let json = json!({
295312
"export_key": false,
296313
"ip": "192.168.1.10",
297-
"dns_name": "my-client.org"
314+
"dns_name": "my-client.org",
315+
"include_certificate_chain": false
298316
});
299317

300318
let deserialized: ClientConfiguration = serde_json::from_value(json).unwrap();
@@ -313,11 +331,13 @@ mod tests {
313331
],
314332
dns_name: vec!["my-server.org".to_string(), "my-server.com".to_string()],
315333
},
334+
include_certificate_chain: false,
316335
};
317336
let json = json!({
318337
"export_key": false,
319338
"ip": ["192.168.1.1", "192.168.1.2"],
320-
"dns_name": ["my-server.org", "my-server.com"]
339+
"dns_name": ["my-server.org", "my-server.com"],
340+
"include_certificate_chain": false
321341
});
322342

323343
let deserialized: ServerConfiguration = serde_json::from_value(json).unwrap();
@@ -333,11 +353,35 @@ mod tests {
333353
ip: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))],
334354
dns_name: vec!["my-server.org".to_string()],
335355
},
356+
include_certificate_chain: true,
357+
};
358+
let json = json!({
359+
"export_key": false,
360+
"ip": "192.168.1.1",
361+
"dns_name": "my-server.org",
362+
"include_certificate_chain": true
363+
});
364+
365+
let deserialized: ServerConfiguration = serde_json::from_value(json).unwrap();
366+
367+
assert_eq!(deserialized, expected)
368+
}
369+
370+
#[test]
371+
fn should_deserialize_cert_chain() {
372+
let expected = ServerConfiguration {
373+
export_key: false,
374+
subject_alternative_names: SubjectAlternativeNames {
375+
ip: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))],
376+
dns_name: vec!["my-server.org".to_string()],
377+
},
378+
include_certificate_chain: true,
336379
};
337380
let json = json!({
338381
"export_key": false,
339382
"ip": "192.168.1.1",
340-
"dns_name": "my-server.org"
383+
"dns_name": "my-server.org",
384+
"include_certificate_chain": true
341385
});
342386

343387
let deserialized: ServerConfiguration = serde_json::from_value(json).unwrap();

test-certs/src/generation.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::Arc;
2+
13
use rcgen::{
24
BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa,
35
KeyPair, KeyUsagePurpose,
@@ -11,21 +13,24 @@ use crate::{
1113
},
1214
};
1315

16+
/// Type alias to make code more readable.
17+
type Issuer = Arc<Certificate>;
18+
1419
/// Extension trait to convert [`CertificateType`] to [`Certificate`].
1520
// NOTE: Instead of a trait use actual types?
1621
pub trait CertificateGenerator {
1722
/// Build a [`Certificate`].
18-
fn build(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error>;
23+
fn build(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error>;
1924
}
2025

2126
/// Internal trait to actually implement the logic to create a certificate from a specific
2227
/// certificate configuration.
2328
trait ToCertificate {
24-
fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error>;
29+
fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error>;
2530
}
2631

2732
impl CertificateGenerator for CertificateType {
28-
fn build(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error> {
33+
fn build(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error> {
2934
match self {
3035
CertificateType::CertificateAuthority(certificate_authority_configuration) => {
3136
certificate_authority_configuration.certificate(name, issuer)
@@ -41,7 +46,7 @@ impl CertificateGenerator for CertificateType {
4146
}
4247

4348
impl ToCertificate for CertificateAuthorityConfiguration {
44-
fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error> {
49+
fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error> {
4550
let key = KeyPair::generate()?;
4651

4752
let certificate_params = issuer_params(name);
@@ -53,42 +58,53 @@ impl ToCertificate for CertificateAuthorityConfiguration {
5358
key,
5459
export_key: self.export_key,
5560
name: name.to_string(),
61+
issuer: issuer.cloned(),
5662
})
5763
}
5864
}
5965

6066
impl ToCertificate for ClientConfiguration {
61-
fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error> {
67+
fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error> {
6268
let key = KeyPair::generate()?;
6369

6470
let mut certificate_params = certificate_params(name, &self.subject_alternative_names)?;
6571
certificate_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
6672

6773
let certificate = sign_cert(certificate_params, &key, issuer)?;
74+
let issuer = self
75+
.include_certificate_chain
76+
.then(|| issuer.cloned())
77+
.flatten();
6878

6979
Ok(Certificate {
7080
certificate,
7181
key,
7282
export_key: self.export_key,
7383
name: name.to_string(),
84+
issuer,
7485
})
7586
}
7687
}
7788

7889
impl ToCertificate for ServerConfiguration {
79-
fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error> {
90+
fn certificate(&self, name: &str, issuer: Option<&Issuer>) -> Result<Certificate, Error> {
8091
let key = KeyPair::generate()?;
8192

8293
let mut certificate_params = certificate_params(name, &self.subject_alternative_names)?;
8394
certificate_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
8495

8596
let certificate = sign_cert(certificate_params, &key, issuer)?;
97+
let issuer = self
98+
.include_certificate_chain
99+
.then(|| issuer.cloned())
100+
.flatten();
86101

87102
Ok(Certificate {
88103
certificate,
89104
key,
90105
export_key: self.export_key,
91106
name: name.to_string(),
107+
issuer,
92108
})
93109
}
94110
}
@@ -97,7 +113,7 @@ impl ToCertificate for ServerConfiguration {
97113
fn sign_cert(
98114
certificate_params: CertificateParams,
99115
key: &KeyPair,
100-
issuer: Option<&Certificate>,
116+
issuer: Option<&Issuer>,
101117
) -> Result<rcgen::Certificate, Error> {
102118
let certificate = if let Some(issuer) = issuer {
103119
certificate_params.signed_by(key, &issuer.certificate, &issuer.key)
@@ -178,5 +194,16 @@ mod tests {
178194
assert!(result.is_ok())
179195
}
180196

197+
#[test]
198+
fn should_include_certificate_chain() {
199+
let ca = ca_certificate_type();
200+
let ca_cert = Issuer::new(ca.build("my-ca", None).unwrap());
201+
let client = client_certificate_type();
202+
let client_cert = client.build("client", Some(&ca_cert)).unwrap();
203+
let parent = client_cert.issuer.unwrap();
204+
205+
assert_eq!(parent, ca_cert);
206+
}
207+
181208
// TODO: write test to check wether client/server certs are really issued by a ca
182209
}

test-certs/src/lib.rs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::{
44
fmt::{Debug, Display},
55
io::Write,
66
path::Path,
7+
sync::Arc,
78
};
89

910
use configuration::certificates::{CertificateRoot, CertificateType};
@@ -39,6 +40,24 @@ pub struct Certificate {
3940
key: KeyPair,
4041
export_key: bool,
4142
name: String,
43+
issuer: Option<Arc<Certificate>>,
44+
}
45+
46+
impl PartialEq for Certificate {
47+
fn eq(&self, other: &Self) -> bool {
48+
let Certificate {
49+
certificate,
50+
key,
51+
export_key,
52+
name,
53+
issuer,
54+
} = self;
55+
certificate.der() == other.certificate.der()
56+
&& key.serialized_der() == other.key.serialize_der()
57+
&& *export_key == other.export_key
58+
&& *name == other.name
59+
&& *issuer == other.issuer
60+
}
4261
}
4362

4463
impl Certificate {
@@ -48,6 +67,19 @@ impl Certificate {
4867

4968
let mut cert =
5069
std::fs::File::create(&cert_file).map_err(Error::FailedToWriteCertificate)?;
70+
71+
let mut issuer = self.issuer.clone();
72+
while let Some(ref current_issuer) = issuer {
73+
if current_issuer.issuer.is_none() {
74+
// NOTE: If we have no issuer anymore we are at top level
75+
// and do not include the root ca.
76+
break;
77+
}
78+
cert.write_fmt(format_args!("{}", current_issuer.certificate.pem()))
79+
.map_err(Error::FailedToWriteCertificate)?;
80+
issuer = current_issuer.issuer.clone();
81+
}
82+
5183
cert.write_fmt(format_args!("{}", self.certificate.pem()))
5284
.map_err(Error::FailedToWriteCertificate)?;
5385

@@ -63,8 +95,8 @@ impl Certificate {
6395

6496
/// Generates all certificates that are present in the configuration file.
6597
// TODO: Make builder and return errors and certificates at the same time, maybe with an Iterator?
66-
pub fn generate(certificate_config: &CertificateRoot) -> Result<Vec<Certificate>, Error> {
67-
let certs: Vec<Result<Vec<Certificate>, Error>> = certificate_config
98+
pub fn generate(certificate_config: &CertificateRoot) -> Result<Vec<Arc<Certificate>>, Error> {
99+
let certs: Vec<Result<Vec<Arc<Certificate>>, Error>> = certificate_config
68100
.certificates
69101
.iter()
70102
.map(|(name, config)| generate_certificates(name, config, None))
@@ -90,17 +122,17 @@ pub fn generate(certificate_config: &CertificateRoot) -> Result<Vec<Certificate>
90122
fn generate_certificates(
91123
name: &str,
92124
config: &CertificateType,
93-
issuer: Option<&Certificate>,
94-
) -> Result<Vec<Certificate>, Error> {
125+
issuer: Option<&Arc<Certificate>>,
126+
) -> Result<Vec<Arc<Certificate>>, Error> {
95127
let mut result = vec![];
96128
let issuer = config.build(name, issuer)?;
97-
129+
let issuer = Arc::new(issuer);
98130
for (name, config) in config.certificates() {
99131
let mut certificates = generate_certificates(name, config, Some(&issuer))?;
100132
result.append(&mut certificates);
101133
}
102134

103-
result.push(issuer);
135+
result.push(issuer.clone());
104136
Ok(result)
105137
}
106138

@@ -117,13 +149,15 @@ impl Debug for Certificate {
117149
key,
118150
export_key,
119151
name,
152+
issuer,
120153
} = self;
121154

122155
f.debug_struct("CertKey")
123156
.field("certificate", &certificate.pem())
124157
.field("key", &key.serialize_pem())
125158
.field("export_key", export_key)
126159
.field("name", name)
160+
.field("issuer", issuer)
127161
.finish()
128162
}
129163
}
@@ -172,4 +206,6 @@ mod test {
172206
.any(|f| f.file_name() == "my-client.pem");
173207
assert!(file_exists)
174208
}
209+
210+
// TODO: Test if cert chain in file does match up
175211
}

test-certs/tests/examples/intermediate_ca.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ my-root-ca:
1717
# Create a server auth certificate issued by my-intermediate-ca
1818
my-server:
1919
type: server
20+
# By default all intermediate certificate authorities are included in the generated pem file
21+
include_certificate_chain: false
2022
# Multiple IPs are also possible
2123
ip:
2224
- 192.168.1.1

0 commit comments

Comments
 (0)