Skip to content

Commit 0e7a59a

Browse files
committed
feat: ✨ create certificate files
1 parent 1f30703 commit 0e7a59a

File tree

7 files changed

+124
-48
lines changed

7 files changed

+124
-48
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Cargo.lock
1313
# MSVC Windows builds of rustc generate these, which store debugging information
1414
*.pdb
1515

16+
**/certificates.d
1617
# RustRover
1718
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
1819
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore

test-certs/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ serde_with = "3.11"
1818
clap = { version = "4.5", features = ["derive"] }
1919
tracing = "0.1"
2020
tracing-subscriber = "0.3"
21+
22+
[dev-dependencies]
23+
testdir = "0.9"

test-certs/src/configuration/certificates.rs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
66

77
/// This is the root structure that contains all certificate chains.
88
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
9-
pub struct Certificates {
9+
pub struct CertificateRoot {
1010
/// All certificates
1111
#[serde(flatten)]
1212
pub certificates: HashMap<String, CertificateTypes>,
@@ -15,7 +15,7 @@ pub struct Certificates {
1515
/// The certificate authority to sign other certificates.
1616
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
1717
#[serde(default, deny_unknown_fields)]
18-
pub struct CertificateAuthority {
18+
pub struct CertificateAuthorityConfiguration {
1919
/// Enables the export of the private key file.
2020
pub export_key: bool,
2121

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

3535
/// A certificate used for server authentication.
3636
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
3737
#[serde(default, deny_unknown_fields)]
38-
pub struct Server {
38+
pub struct ServerConfiguration {
3939
/// Enables the export of the private key file.
4040
pub export_key: bool,
4141
}
@@ -46,13 +46,13 @@ pub struct Server {
4646
pub enum CertificateTypes {
4747
/// A certificate that acts as a Certificate Authority.
4848
#[serde(alias = "ca")]
49-
CertificateAuthority(CertificateAuthority),
49+
CertificateAuthority(CertificateAuthorityConfiguration),
5050

5151
/// A certificate for client authentication.
52-
Client(Client),
52+
Client(ClientConfiguration),
5353

5454
/// A certificate for server authentication.
55-
Server(Server),
55+
Server(ServerConfiguration),
5656
}
5757

5858
impl CertificateTypes {
@@ -83,13 +83,13 @@ impl CertificateTypes {
8383
/// The [`LazyLock`] is required as a HashMap::new is not usable in const expressions.
8484
static NO_CERTIFICATES: LazyLock<HashMap<String, CertificateTypes>> = LazyLock::new(HashMap::new);
8585

86-
impl Default for Client {
86+
impl Default for ClientConfiguration {
8787
fn default() -> Self {
8888
Self { export_key: true }
8989
}
9090
}
9191

92-
impl Default for Server {
92+
impl Default for ServerConfiguration {
9393
fn default() -> Self {
9494
Self { export_key: true }
9595
}
@@ -101,46 +101,46 @@ pub mod fixtures {
101101
use super::*;
102102

103103
/// 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 {
104+
pub fn certificate_ca_with_client() -> CertificateRoot {
105+
let certs = CertificateRoot {
106106
certificates: HashMap::from([("ca".to_string(), ca_with_client())]),
107107
};
108108
certs
109109
}
110110

111111
/// Creates a certificate of type ca with a client cert.
112112
pub fn ca_with_client() -> CertificateTypes {
113-
CertificateTypes::CertificateAuthority(CertificateAuthority {
113+
CertificateTypes::CertificateAuthority(CertificateAuthorityConfiguration {
114114
certificates: HashMap::from([(
115115
"client".to_string(),
116-
CertificateTypes::Client(Client::default()),
116+
CertificateTypes::Client(ClientConfiguration::default()),
117117
)]),
118118
..Default::default()
119119
})
120120
}
121121

122122
/// Creates a client certificate.
123-
pub fn certificate_client() -> Certificates {
124-
let certs = Certificates {
123+
pub fn certificate_client() -> CertificateRoot {
124+
let certs = CertificateRoot {
125125
certificates: HashMap::from([(
126126
"client".to_string(),
127-
CertificateTypes::Client(Client::default()),
127+
CertificateTypes::Client(ClientConfiguration::default()),
128128
)]),
129129
};
130130
certs
131131
}
132132

133133
/// Creates a certificate of type client.
134134
pub fn client() -> CertificateTypes {
135-
CertificateTypes::Client(Client::default())
135+
CertificateTypes::Client(ClientConfiguration::default())
136136
}
137137

138138
/// Creates a certificate authority without any other certificates.
139-
pub fn certificate_ca() -> Certificates {
140-
let certs = Certificates {
139+
pub fn certificate_ca() -> CertificateRoot {
140+
let certs = CertificateRoot {
141141
certificates: HashMap::from([(
142142
"ca".to_string(),
143-
CertificateTypes::CertificateAuthority(CertificateAuthority::default()),
143+
CertificateTypes::CertificateAuthority(CertificateAuthorityConfiguration::default()),
144144
)]),
145145
};
146146
certs
@@ -153,7 +153,7 @@ mod tests {
153153
use fixtures::certificate_ca_with_client;
154154
use serde_json::json;
155155

156-
fn get_ca(cert: &CertificateTypes) -> &CertificateAuthority {
156+
fn get_ca(cert: &CertificateTypes) -> &CertificateAuthorityConfiguration {
157157
assert!(matches!(cert, CertificateTypes::CertificateAuthority(_)));
158158
let ca = match cert {
159159
CertificateTypes::CertificateAuthority(certificate_authority) => certificate_authority,
@@ -222,7 +222,7 @@ mod tests {
222222
let certs = certificate_ca_with_client();
223223

224224
let serialized = serde_json::to_string(&certs).unwrap();
225-
let deserialized: Certificates = serde_json::from_str(&serialized).unwrap();
225+
let deserialized: CertificateRoot = serde_json::from_str(&serialized).unwrap();
226226

227227
assert_eq!(deserialized, certs)
228228
}
@@ -233,10 +233,10 @@ mod tests {
233233

234234
#[test]
235235
fn should_serialize_certificateauthority() {
236-
let certificates = Certificates {
236+
let certificates = CertificateRoot {
237237
certificates: HashMap::from([(
238238
"my-ca".to_string(),
239-
CertificateTypes::CertificateAuthority(CertificateAuthority {
239+
CertificateTypes::CertificateAuthority(CertificateAuthorityConfiguration {
240240
certificates: HashMap::new(),
241241
export_key: false,
242242
}),
@@ -277,7 +277,7 @@ mod tests {
277277
let certs = certificate_ca_with_client();
278278

279279
let serialized = serde_yaml::to_string(&certs).unwrap();
280-
let deserialized: Certificates = serde_yaml::from_str(&serialized).unwrap();
280+
let deserialized: CertificateRoot = serde_yaml::from_str(&serialized).unwrap();
281281

282282
assert_eq!(deserialized, certs)
283283
}

test-certs/src/configuration/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ pub struct Args {
1313
#[arg(short, long, default_value = "./certificate_generation.yaml")]
1414
pub configuration: PathBuf,
1515

16+
/// Folder where all generated certificates will be saved.
17+
#[arg(short, long = "out-dir", default_value = "./certificates.d/")]
18+
pub outdir: PathBuf,
19+
1620
/// Use the provided configuration language.
1721
#[arg(default_value_t = ConfigFormat::Yaml)]
1822
pub format: ConfigFormat,
@@ -42,9 +46,12 @@ mod tests {
4246

4347
#[test]
4448
fn should_parse_args() {
45-
let args = Args::try_parse_from(&["", "--configuration", "./file.yaml", "json"]).unwrap();
49+
let args =
50+
Args::try_parse_from(&["", "--configuration", "./file.yaml", "--out-dir", "./certs", "json"])
51+
.unwrap();
4652

4753
assert_eq!(args.configuration.display().to_string(), "./file.yaml");
4854
assert_eq!(args.format.to_string(), "json");
55+
assert_eq!(args.outdir.display().to_string(), "./certs");
4956
}
5057
}

test-certs/src/lib.rs

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
#![warn(missing_debug_implementations)]
55
#![warn(clippy::unwrap_used)]
66

7-
use std::fmt::Debug;
7+
use std::{fmt::Debug, io::Write, path::PathBuf};
88

99
use configuration::certificates::CertificateTypes;
1010
use rcgen::{
11-
BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair,
12-
KeyUsagePurpose,
11+
BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, KeyUsagePurpose,
1312
};
1413

1514
pub mod configuration;
@@ -20,26 +19,61 @@ pub enum Error {
2019
/// Errors when working with rcgen to create and sign certificates.
2120
#[error("Could not generate certificate")]
2221
FailedToCreateCertificate(#[from] rcgen::Error),
22+
23+
/// Error to write the certificate to disk
24+
#[error("Could not write certificate to '{0}'")]
25+
FailedToWriteCertificate(PathBuf),
26+
27+
/// Error to write the certificate key to disk
28+
#[error("Could not write certificate key to '{0}'")]
29+
FailedToWriteKey(PathBuf),
30+
31+
/// Multiple errors that occurred while working with certificates
32+
#[error("Multiple errors occurred")]
33+
ErrorCollection(Vec<Error>),
2334
}
2435

2536
/// A pair of a certificate and the corresponding private key.
2637
#[allow(unused, reason = "Initial draft therefore values are not used yet")]
27-
pub struct CertKeyPair {
28-
certificate: Certificate,
38+
pub struct Certificate {
39+
certificate: rcgen::Certificate,
2940
key: KeyPair,
3041
export_key: bool,
3142
name: String,
3243
}
3344

45+
impl Certificate {
46+
/// Write the certificate and the key if marked for export to the specified folder.
47+
///
48+
/// This fails if the folder is not accessible.
49+
pub fn write(&self, directory: &PathBuf) -> Result<(), Error> {
50+
let cert_file = directory.join(format!("{}.pem", &self.name));
51+
52+
let mut cert = std::fs::File::create(&cert_file)
53+
.map_err(|_| Error::FailedToWriteCertificate(cert_file.clone()))?;
54+
cert.write_fmt(format_args!("{}", self.certificate.pem()))
55+
.map_err(|_| Error::FailedToWriteCertificate(cert_file))?;
56+
57+
if self.export_key {
58+
let key_file = directory.join(format!("{}.key", &self.name));
59+
let mut key = std::fs::File::create(&key_file)
60+
.map_err(|_| Error::FailedToWriteCertificate(key_file.clone()))?;
61+
key.write_fmt(format_args!("{}", self.key.serialize_pem()))
62+
.map_err(|_| Error::FailedToWriteCertificate(key_file))?;
63+
}
64+
Ok(())
65+
}
66+
}
67+
3468
/// Generates all certificates that are present in the configuration file.
3569
///
3670
/// Each certificate chain is evaluated from the specified root certificate.
37-
/// If one certificate in the chain could not be created the corresponding
71+
/// If one certificate in the chain could not be created the corresponding
3872
/// error is reported and the chain will not be generated.
3973
pub fn generate(
40-
certificate_config: configuration::certificates::Certificates,
41-
) -> Result<Vec<CertKeyPair>, Vec<Error>> {
42-
let certs: Vec<Result<Vec<CertKeyPair>, Error>> = certificate_config
74+
certificate_config: configuration::certificates::CertificateRoot,
75+
) -> Result<Vec<Certificate>, Error> {
76+
let certs: Vec<Result<Vec<Certificate>, Error>> = certificate_config
4377
.certificates
4478
.iter()
4579
.map(|(name, config)| generate_certificates(name, config, None))
@@ -55,7 +89,7 @@ pub fn generate(
5589
}
5690

5791
if !errors.is_empty() {
58-
return Err(errors);
92+
return Err(Error::ErrorCollection(errors));
5993
}
6094

6195
Ok(certificates)
@@ -65,8 +99,8 @@ pub fn generate(
6599
fn generate_certificates(
66100
name: &str,
67101
config: &CertificateTypes,
68-
issuer: Option<&CertKeyPair>,
69-
) -> Result<Vec<CertKeyPair>, Error> {
102+
issuer: Option<&Certificate>,
103+
) -> Result<Vec<Certificate>, Error> {
70104
let mut result = vec![];
71105
let issuer = create_certificate(name, config, issuer)?;
72106

@@ -83,8 +117,8 @@ fn generate_certificates(
83117
fn create_certificate(
84118
name: &str,
85119
certificate_config: &CertificateTypes,
86-
issuer: Option<&CertKeyPair>,
87-
) -> Result<CertKeyPair, Error> {
120+
issuer: Option<&Certificate>,
121+
) -> Result<Certificate, Error> {
88122
let key = KeyPair::generate()?;
89123

90124
// TODO: right now the certificate type is ignored and no client or server auth certs are generated
@@ -94,15 +128,15 @@ fn create_certificate(
94128
issuer_params(name).self_signed(&key)?
95129
};
96130

97-
Ok(CertKeyPair {
131+
Ok(Certificate {
98132
certificate,
99133
key,
100134
export_key: certificate_config.export_key(),
101135
name: name.to_string(),
102136
})
103137
}
104138

105-
impl Debug for CertKeyPair {
139+
impl Debug for Certificate {
106140
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107141
f.debug_struct("CertKey")
108142
.field("certificate", &self.certificate.pem())
@@ -129,6 +163,7 @@ mod test {
129163
use configuration::certificates::fixtures::{
130164
ca_with_client, certificate_ca_with_client, client,
131165
};
166+
use testdir::testdir;
132167

133168
use super::*;
134169

@@ -147,13 +182,32 @@ mod test {
147182
fn should_create_certificate() {
148183
let config = client();
149184
let result = create_certificate("test", &config, None);
185+
150186
assert!(result.is_ok())
151187
}
152188

153189
#[test]
154190
fn should_generate_certificates() {
155191
let config = ca_with_client();
156192
let result = generate_certificates("my-ca", &config, None);
193+
157194
assert!(result.is_ok())
158195
}
196+
197+
#[test]
198+
fn should_write_certificate_to_file() {
199+
let dir = testdir!();
200+
let config = client();
201+
let certificates = generate_certificates("my-client", &config, None).unwrap();
202+
let certificate = certificates.first().unwrap();
203+
204+
certificate.write(&dir).unwrap();
205+
206+
let file_exists = dir
207+
.read_dir()
208+
.unwrap()
209+
.filter_map(|e| e.ok())
210+
.any(|f| f.file_name() == "my-client.pem");
211+
assert!(file_exists)
212+
}
159213
}

0 commit comments

Comments
 (0)