Skip to content

Commit 1e800c4

Browse files
committed
Add KDL node for CertificateLists & command to generate them
It's common for lists of certificates to be concatenated and stored or transferred as a single file / unit. Different applications / protocols may expect or produce a particular ordering. In rare cases certs in a list may be out of order, or even contain multiple branches. To support as many use-cases as possible we do not enforce any particular ordering or relationship between the certificates in a certificate list. We simply concatenate them in the order specified. A `CertificateList` is defined in KDL as: ```kdl certificate-list "name" \ "cert-name-1" \ "cert-name-2" ``` The output file name prefix is taken from the first attribute of the KDL ("name" in this example). The suffix `.certlist.pem` is appended to this `name`. The remaining attributes are the string names assigned to `certificate` nodes defined elsewhere in the document.
1 parent b7321bd commit 1e800c4

File tree

3 files changed

+71
-0
lines changed

3 files changed

+71
-0
lines changed

examples/simple-chain-client-server_ed25519/config.kdl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ certificate "TEST USE ONLY - Test Server A" {
175175
serial-number "01"
176176
}
177177

178+
certificate-list "TEST USE ONLY - Test Server A" \
179+
"TEST USE ONLY - Test Server A" \
180+
"TEST USE ONLY - Test Int A"
178181

179182
certificate "TEST USE ONLY - Test Client A" {
180183
subject-entity "TEST USE ONLY - Test Client A"

src/config.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ pub struct Document {
2020

2121
#[knuffel(children(name = "certificate-request"))]
2222
pub certificate_requests: Vec<CertificateRequest>,
23+
24+
#[knuffel(children(name = "certificate-list"))]
25+
pub certificate_lists: Vec<CertificateList>,
2326
}
2427

2528
#[derive(knuffel::Decode, Debug)]
@@ -109,6 +112,15 @@ pub struct CertificateRequest {
109112
pub digest_algorithm: Option<DigestAlgorithm>,
110113
}
111114

115+
#[derive(knuffel::Decode, Debug)]
116+
pub struct CertificateList {
117+
#[knuffel(argument)]
118+
pub name: String,
119+
120+
#[knuffel(arguments)]
121+
pub certificates: Vec<String>,
122+
}
123+
112124
#[derive(knuffel::DecodeScalar, Debug)]
113125
#[allow(non_camel_case_types)]
114126
pub enum DigestAlgorithm {
@@ -481,5 +493,13 @@ pub fn load_and_validate(path: &std::path::Path) -> Result<Document> {
481493
}
482494
}
483495

496+
for certlist in &doc.certificate_lists {
497+
for cert in &certlist.certificates {
498+
if !cert_names.contains(cert.as_str()) {
499+
miette::bail!("certificate \"{}\" does not exist", cert,)
500+
}
501+
}
502+
}
503+
484504
Ok(doc)
485505
}

src/main.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ enum Action {
4646
GenerateKeyPairs(GenerateKeyPairsOpts),
4747
GenerateCertificateRequests(GenerateCertificateRequestsOpts),
4848
GenerateCertificates(GenerateCertificatesOpts),
49+
GenerateCertificateLists(GenerateCertificateListsOpts),
4950
}
5051

5152
#[derive(clap::Args)]
@@ -69,6 +70,13 @@ struct GenerateCertificatesOpts {
6970
output_exists: OutputFileExistsBehavior,
7071
}
7172

73+
#[derive(clap::Args)]
74+
struct GenerateCertificateListsOpts {
75+
/// action to take if an output file already exists
76+
#[arg(long, default_value = "overwrite")]
77+
output_exists: OutputFileExistsBehavior,
78+
}
79+
7280
fn write_to_file(
7381
filename: &str,
7482
contents: &[u8],
@@ -131,6 +139,26 @@ fn load_entities(entities: &Vec<config::Entity>) -> Result<HashMap<String, Entit
131139
Ok(entity_map)
132140
}
133141

142+
fn load_certificates(
143+
certificates_cfg: &Vec<config::Certificate>,
144+
) -> Result<HashMap<String, x509_cert::Certificate>> {
145+
let mut certs = HashMap::new();
146+
147+
for cert_cfg in certificates_cfg {
148+
let cert_filename = format!("{}.cert.pem", cert_cfg.name);
149+
let cert_pem = std::fs::read_to_string(&cert_filename)
150+
.into_diagnostic()
151+
.wrap_err(format!(
152+
"Unable to load certificate \"{}\" from \"{}\"",
153+
cert_cfg.name, &cert_filename
154+
))?;
155+
let cert = x509_cert::Certificate::from_pem(cert_pem).into_diagnostic()?;
156+
certs.insert(cert_cfg.name.clone(), cert);
157+
}
158+
159+
Ok(certs)
160+
}
161+
134162
fn main() -> Result<()> {
135163
let opts = Options::parse();
136164

@@ -145,6 +173,26 @@ fn main() -> Result<()> {
145173
))?;
146174

147175
match opts.action {
176+
Action::GenerateCertificateLists(action_opts) => {
177+
let certificates = load_certificates(&doc.certificates)?;
178+
for certlist_cfg in &doc.certificate_lists {
179+
let mut cert_chain = String::new();
180+
let certlist_filename = format!("{}.certlist.pem", certlist_cfg.name);
181+
println!("Writing pki path to \"{}\"", &certlist_filename);
182+
for cert_name in &certlist_cfg.certificates {
183+
let cert = certificates.get(cert_name).ok_or(miette!(
184+
"Certificate does not exist: {}",
185+
&certlist_cfg.name
186+
))?;
187+
cert_chain += &cert.to_pem(LineEnding::CRLF).into_diagnostic()?;
188+
}
189+
write_to_file(
190+
&certlist_filename,
191+
cert_chain.as_bytes(),
192+
action_opts.output_exists,
193+
)?
194+
}
195+
}
148196
Action::GenerateKeyPairs(action_opts) => {
149197
for kp_config in &doc.key_pairs {
150198
let kp = <dyn KeyPair>::new(kp_config)?;

0 commit comments

Comments
 (0)