Skip to content

Commit c1cb75e

Browse files
committed
chore: 🚧 generate certificates from config file
1 parent baf8cdd commit c1cb75e

File tree

4 files changed

+206
-50
lines changed

4 files changed

+206
-50
lines changed

test-certs/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ version = "0.1.0"
44
edition = "2021"
55
default-run = "test-certs"
66

7+
[features]
8+
fixtures = []
9+
710
[dependencies]
811
thiserror = "2"
912
rcgen = "0.13"

test-certs/src/configuration/certificates.rs

Lines changed: 104 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Certificate generation configuration.
22
//!
3-
use std::collections::HashMap;
3+
use std::{collections::HashMap, sync::LazyLock};
44

55
use serde::{Deserialize, Serialize};
66

@@ -16,45 +16,73 @@ pub struct Certificates {
1616
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
1717
#[serde(default, deny_unknown_fields)]
1818
pub struct CertificateAuthority {
19-
/// Enables the export of the private key file
19+
/// Enables the export of the private key file.
2020
pub export_key: bool,
2121

22-
/// Certificates that are signed by this CA
22+
/// Certificates that are signed by this CA.
2323
#[serde(skip_serializing_if = "HashMap::is_empty")]
2424
pub certificates: HashMap<String, CertificateTypes>,
2525
}
2626

27-
/// A certificate used for client authentication
27+
/// A certificate used for client authentication.
2828
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
2929
#[serde(default, deny_unknown_fields)]
3030
pub struct Client {
31-
/// Enables the export of the private key file
31+
/// Enables the export of the private key file.
3232
pub export_key: bool,
3333
}
3434

35-
/// A certificate used for server authentication
35+
/// A certificate used for server authentication.
3636
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
3737
#[serde(default, deny_unknown_fields)]
3838
pub struct Server {
39-
/// Enables the export of the private key file
39+
/// Enables the export of the private key file.
4040
pub export_key: bool,
4141
}
4242

43-
/// All kinds of different certificates
43+
/// All kinds of different certificates.
4444
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
4545
#[serde(tag = "type", rename_all = "lowercase")]
4646
pub enum CertificateTypes {
47-
/// A certificate that acts as a Certificate Authority
47+
/// A certificate that acts as a Certificate Authority.
4848
#[serde(alias = "ca")]
4949
CertificateAuthority(CertificateAuthority),
5050

51-
/// A certificate for client authentication
51+
/// A certificate for client authentication.
5252
Client(Client),
5353

54-
/// A certificate for server authentication
54+
/// A certificate for server authentication.
5555
Server(Server),
5656
}
5757

58+
impl CertificateTypes {
59+
/// Should the private key be exported or not
60+
pub fn export_key(&self) -> bool {
61+
match self {
62+
CertificateTypes::CertificateAuthority(certificate_authority) => {
63+
certificate_authority.export_key
64+
}
65+
CertificateTypes::Client(client) => client.export_key,
66+
CertificateTypes::Server(server) => server.export_key,
67+
}
68+
}
69+
70+
/// Certificates issued by this certificate.
71+
pub fn certificates(&self) -> &HashMap<String, CertificateTypes> {
72+
match self {
73+
CertificateTypes::CertificateAuthority(certificate_authority) => {
74+
&certificate_authority.certificates
75+
}
76+
CertificateTypes::Client(_client) => &NO_CERTIFICATES,
77+
CertificateTypes::Server(_server) => &NO_CERTIFICATES,
78+
}
79+
}
80+
}
81+
82+
/// Is used to provide a reference to an empty HashMap.
83+
/// The [`LazyLock`] is required as a HashMap::new is not usable in const expressions.
84+
static NO_CERTIFICATES: LazyLock<HashMap<String, CertificateTypes>> = LazyLock::new(HashMap::new);
85+
5886
impl Default for Client {
5987
fn default() -> Self {
6088
Self { export_key: true }
@@ -67,11 +95,63 @@ impl Default for Server {
6795
}
6896
}
6997

98+
/// Fixtures for testing certificate generation.
99+
#[cfg(any(test, feature = "fixtures"))]
100+
pub mod fixtures {
101+
use super::*;
102+
103+
/// Creates a certificate authority and a server and client certificated that are issued by it.
104+
pub fn certificate_ca_with_client() -> Certificates {
105+
let certs = Certificates {
106+
certificates: HashMap::from([("ca".to_string(), ca_with_client())]),
107+
};
108+
certs
109+
}
110+
111+
/// Creates a certificate of type ca with a client cert.
112+
pub fn ca_with_client() -> CertificateTypes {
113+
CertificateTypes::CertificateAuthority(CertificateAuthority {
114+
certificates: HashMap::from([(
115+
"client".to_string(),
116+
CertificateTypes::Client(Client::default()),
117+
)]),
118+
..Default::default()
119+
})
120+
}
121+
122+
/// Creates a client certificate.
123+
pub fn certificate_client() -> Certificates {
124+
let certs = Certificates {
125+
certificates: HashMap::from([(
126+
"client".to_string(),
127+
CertificateTypes::Client(Client::default()),
128+
)]),
129+
};
130+
certs
131+
}
132+
133+
/// Creates a certificate of type client.
134+
pub fn client() -> CertificateTypes {
135+
CertificateTypes::Client(Client::default())
136+
}
137+
138+
/// Creates a certificate authority without any other certificates.
139+
pub fn certificate_ca() -> Certificates {
140+
let certs = Certificates {
141+
certificates: HashMap::from([(
142+
"ca".to_string(),
143+
CertificateTypes::CertificateAuthority(CertificateAuthority::default()),
144+
)]),
145+
};
146+
certs
147+
}
148+
}
149+
70150
#[cfg(test)]
71151
mod tests {
72-
use serde_json::json;
73-
74152
use super::*;
153+
use fixtures::certificate_ca_with_client;
154+
use serde_json::json;
75155

76156
fn get_ca(cert: &CertificateTypes) -> &CertificateAuthority {
77157
assert!(matches!(cert, CertificateTypes::CertificateAuthority(_)));
@@ -139,18 +219,7 @@ mod tests {
139219

140220
#[test]
141221
fn should_serde_roundtrip() {
142-
let certs = Certificates {
143-
certificates: HashMap::from_iter([(
144-
"my-ca".to_string(),
145-
CertificateTypes::CertificateAuthority(CertificateAuthority {
146-
export_key: true,
147-
certificates: HashMap::from_iter([(
148-
"client".to_string(),
149-
CertificateTypes::Client(Client { export_key: true }),
150-
)]),
151-
}),
152-
)]),
153-
};
222+
let certs = certificate_ca_with_client();
154223

155224
let serialized = serde_json::to_string(&certs).unwrap();
156225
let deserialized: Certificates = serde_json::from_str(&serialized).unwrap();
@@ -202,5 +271,15 @@ mod tests {
202271

203272
assert!(matches!(ca, CertificateTypes::CertificateAuthority(_)))
204273
}
274+
275+
#[test]
276+
fn should_serde_roundtrip() {
277+
let certs = certificate_ca_with_client();
278+
279+
let serialized = serde_yaml::to_string(&certs).unwrap();
280+
let deserialized: Certificates = serde_yaml::from_str(&serialized).unwrap();
281+
282+
assert_eq!(deserialized, certs)
283+
}
205284
}
206285
}

test-certs/src/configuration/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ pub struct Args {
1818
pub format: ConfigFormat,
1919
}
2020

21-
/// Available configuration formats
21+
/// Available configuration formats.
2222
#[derive(Debug, Clone, ValueEnum)]
2323
pub enum ConfigFormat {
24-
/// YAML Ain't Markup Language
24+
/// YAML Ain't Markup Language.
2525
Yaml,
26-
/// JavaScript Object Notation
26+
/// JavaScript Object Notation.
2727
Json,
2828
}
2929

test-certs/src/lib.rs

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use std::fmt::Debug;
88

9+
use configuration::certificates::CertificateTypes;
910
use rcgen::{
1011
BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair,
1112
KeyUsagePurpose,
@@ -22,9 +23,82 @@ pub enum Error {
2223
}
2324

2425
/// A pair of a certificate and the corresponding private key.
26+
#[allow(unused, reason = "Initial draft therefore values are not used yet")]
2527
pub struct CertKeyPair {
2628
certificate: Certificate,
2729
key: KeyPair,
30+
export_key: bool,
31+
name: String,
32+
}
33+
34+
/// Generates all certificates that are present in the configuration file.
35+
///
36+
/// Each certificate chain is evaluated from the specified root certificate.
37+
/// If one certificate in the chain could not be created the corresponding error is reported and the chain will not be generated.
38+
pub fn generate(
39+
certificate_config: configuration::certificates::Certificates,
40+
) -> Result<Vec<CertKeyPair>, Vec<Error>> {
41+
let certs: Vec<Result<Vec<CertKeyPair>, Error>> = certificate_config
42+
.certificates
43+
.iter()
44+
.map(|(name, config)| generate_certificates(name, config, None))
45+
.collect();
46+
47+
let mut errors = vec![];
48+
let mut certificates = vec![];
49+
for result in certs.into_iter() {
50+
match result {
51+
Ok(mut certs) => certificates.append(&mut certs),
52+
Err(error) => errors.push(error),
53+
}
54+
}
55+
56+
if !errors.is_empty() {
57+
return Err(errors);
58+
}
59+
60+
Ok(certificates)
61+
}
62+
63+
/// Generates the certificate and all certificates issued by this one.
64+
fn generate_certificates(
65+
name: &str,
66+
config: &CertificateTypes,
67+
issuer: Option<&CertKeyPair>,
68+
) -> Result<Vec<CertKeyPair>, Error> {
69+
let mut result = vec![];
70+
let issuer = create_certificate(name, config, issuer)?;
71+
72+
for (name, config) in config.certificates().iter() {
73+
let mut certificates = generate_certificates(name, config, Some(&issuer))?;
74+
result.append(&mut certificates);
75+
}
76+
77+
result.push(issuer);
78+
Ok(result)
79+
}
80+
81+
/// Create the actual certificate and private key.
82+
fn create_certificate(
83+
name: &str,
84+
certificate_config: &CertificateTypes,
85+
issuer: Option<&CertKeyPair>,
86+
) -> Result<CertKeyPair, Error> {
87+
let key = KeyPair::generate()?;
88+
89+
// TODO: right now the certificate type is ignored and no client or server auth certs are generated
90+
let certificate = if let Some(issuer) = issuer {
91+
issuer_params(name).signed_by(&key, &issuer.certificate, &issuer.key)?
92+
} else {
93+
issuer_params(name).self_signed(&key)?
94+
};
95+
96+
Ok(CertKeyPair {
97+
certificate,
98+
key,
99+
export_key: certificate_config.export_key(),
100+
name: name.to_string(),
101+
})
28102
}
29103

30104
impl Debug for CertKeyPair {
@@ -36,16 +110,6 @@ impl Debug for CertKeyPair {
36110
}
37111
}
38112

39-
/// Create a [`CertKeyPair`] that is our certificate authority to sign other certificates.
40-
pub fn create_root_ca() -> Result<CertKeyPair, Error> {
41-
let root_key = KeyPair::generate()?;
42-
let root_ca = issuer_params(env!("CARGO_PKG_NAME")).self_signed(&root_key)?;
43-
Ok(CertKeyPair {
44-
certificate: root_ca,
45-
key: root_key,
46-
})
47-
}
48-
49113
fn issuer_params(common_name: &str) -> CertificateParams {
50114
let mut issuer_name = DistinguishedName::new();
51115
issuer_name.push(DnType::CommonName, common_name);
@@ -61,24 +125,34 @@ fn issuer_params(common_name: &str) -> CertificateParams {
61125

62126
#[cfg(test)]
63127
mod test {
128+
use configuration::certificates::fixtures::{
129+
ca_with_client, certificate_ca_with_client, client,
130+
};
131+
64132
use super::*;
65133

66134
#[test]
67-
fn should_create_root_ca() {
68-
let result = create_root_ca();
69-
assert!(result.is_ok())
135+
fn should_create_certificates() {
136+
let certificate_config = certificate_ca_with_client();
137+
let certificates = generate(certificate_config).unwrap();
138+
assert_eq!(
139+
certificates.len(),
140+
2,
141+
"Expected to generate one ca certificate and one client certificate: {certificates:?}"
142+
)
70143
}
71144

72145
#[test]
73-
fn root_ca_should_be_ca() {
74-
let CertKeyPair {
75-
certificate,
76-
key: _,
77-
} = create_root_ca().unwrap();
146+
fn should_create_certificate() {
147+
let config = client();
148+
let result = create_certificate("test", &config, None);
149+
assert!(result.is_ok())
150+
}
78151

79-
assert_eq!(
80-
certificate.params().is_ca,
81-
IsCa::Ca(BasicConstraints::Unconstrained)
82-
);
152+
#[test]
153+
fn should_generate_certificates() {
154+
let config = ca_with_client();
155+
let result = generate_certificates("my-ca", &config, None);
156+
assert!(result.is_ok())
83157
}
84158
}

0 commit comments

Comments
 (0)