Skip to content

Commit 6d707da

Browse files
committed
feat: ✨ allow multiple ips and dns names as SANs
1 parent 40cc356 commit 6d707da

File tree

4 files changed

+127
-20
lines changed

4 files changed

+127
-20
lines changed

test-certs/src/configuration/certificates.rs

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Certificate generation configuration.
2-
//!
2+
33
use std::{collections::HashMap, net::IpAddr, sync::LazyLock};
44

55
use serde::{Deserialize, Serialize};
6+
use serde_with::{OneOrMany, formats::PreferOne, serde_as};
67

78
/// This is the root structure that contains all certificate chains.
89
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -25,18 +26,18 @@ pub struct CertificateAuthorityConfiguration {
2526
}
2627

2728
/// A certificate used for client authentication.
28-
// NOTE: A shared basic cert configuration could come in handy to not have to duplicate all cert properties for clients and servers
29+
// NOTE: A shared basic cert configuration could come in handy to not have to duplicate
30+
// all cert properties for clients and servers.
2931
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
3032
#[serde(deny_unknown_fields)]
3133
pub struct ClientConfiguration {
3234
/// Enables the export of the private key file.
3335
#[serde(default = "ClientConfiguration::default_export_key")]
3436
pub export_key: bool,
3537

36-
/// Ip address of the client.
37-
// TODO: maybe allow multiple Ip addresses?
38-
pub ip: IpAddr,
39-
// TODO: Have a dns name that is used for the subject alt names
38+
/// Properties that will be set as Subject Alternative Names (SAN)s.
39+
#[serde(flatten)]
40+
pub subject_alternative_names: SubjectAlternativeNames,
4041
}
4142

4243
/// A certificate used for server authentication.
@@ -47,10 +48,26 @@ pub struct ServerConfiguration {
4748
#[serde(default = "ServerConfiguration::default_export_key")]
4849
pub export_key: bool,
4950

50-
/// Ip address of the server.
51-
// TODO: maybe allow multiple Ip addresses?
52-
pub ip: IpAddr,
53-
// TODO: Have a dns name that is used for the subject alt names
51+
/// Properties that will be set as Subject Alternative Names (SAN)s.
52+
#[serde(flatten)]
53+
pub subject_alternative_names: SubjectAlternativeNames,
54+
}
55+
56+
/// Values that will be set as Subject Alternative Names (SAN) of the generated certificate.
57+
///
58+
/// [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.6) describes how dns names and
59+
/// IP addresses are handled in x509 certificates.
60+
#[serde_as]
61+
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
62+
#[serde(deny_unknown_fields)]
63+
pub struct SubjectAlternativeNames {
64+
/// Ip addresses of the client.
65+
#[serde_as(as = "OneOrMany<_, PreferOne>")]
66+
pub ip: Vec<IpAddr>,
67+
68+
/// DNS names of the client
69+
#[serde_as(as = "OneOrMany<_, PreferOne>")]
70+
pub dns_name: Vec<String>,
5471
}
5572

5673
/// All kinds of different certificates.
@@ -159,16 +176,22 @@ pub mod fixtures {
159176
/// Provides a [`CertificateType`] that is a client certificate.
160177
pub fn client_certificate_type() -> CertificateType {
161178
CertificateType::Client(ClientConfiguration {
162-
ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
163179
export_key: ClientConfiguration::default_export_key(),
180+
subject_alternative_names: SubjectAlternativeNames {
181+
ip: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
182+
dns_name: vec!["my-client.org".to_string()],
183+
},
164184
})
165185
}
166186

167187
/// Provides a [`CertificateType`] that is a server certificate.
168188
pub fn server_certificate_type() -> CertificateType {
169189
CertificateType::Server(ServerConfiguration {
170-
ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
171190
export_key: ServerConfiguration::default_export_key(),
191+
subject_alternative_names: SubjectAlternativeNames {
192+
ip: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
193+
dns_name: vec!["my-server.org".to_string()],
194+
},
172195
})
173196
}
174197
}
@@ -189,6 +212,8 @@ mod tests {
189212
}
190213

191214
mod json {
215+
use std::net::Ipv4Addr;
216+
192217
use super::*;
193218

194219
#[test]
@@ -226,11 +251,13 @@ mod tests {
226251
"type": "client",
227252
"export_key": true,
228253
"ip": "192.168.1.10",
254+
"dns_name": "my-client.org"
229255
},
230256
"server_cert": {
231257
"type": "client",
232258
"export_key": true,
233259
"ip": "192.168.1.1",
260+
"dns_name": "my-server.org"
234261
}
235262
}
236263
}
@@ -254,6 +281,69 @@ mod tests {
254281

255282
assert_eq!(deserialized, certs)
256283
}
284+
285+
#[test]
286+
fn should_deserialize_client() {
287+
let expected = ClientConfiguration {
288+
export_key: false,
289+
subject_alternative_names: SubjectAlternativeNames {
290+
ip: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))],
291+
dns_name: vec!["my-client.org".to_string()],
292+
},
293+
};
294+
let json = json!({
295+
"export_key": false,
296+
"ip": "192.168.1.10",
297+
"dns_name": "my-client.org"
298+
});
299+
300+
let deserialized: ClientConfiguration = serde_json::from_value(json).unwrap();
301+
302+
assert_eq!(deserialized, expected)
303+
}
304+
305+
#[test]
306+
fn should_deserialize_multiple_ips_and_dns_snames() {
307+
let expected = ServerConfiguration {
308+
export_key: false,
309+
subject_alternative_names: SubjectAlternativeNames {
310+
ip: vec![
311+
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)),
312+
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)),
313+
],
314+
dns_name: vec!["my-server.org".to_string(), "my-server.com".to_string()],
315+
},
316+
};
317+
let json = json!({
318+
"export_key": false,
319+
"ip": ["192.168.1.1", "192.168.1.2"],
320+
"dns_name": ["my-server.org", "my-server.com"]
321+
});
322+
323+
let deserialized: ServerConfiguration = serde_json::from_value(json).unwrap();
324+
325+
assert_eq!(deserialized, expected)
326+
}
327+
328+
#[test]
329+
fn should_deserialize_server() {
330+
let expected = ServerConfiguration {
331+
export_key: false,
332+
subject_alternative_names: SubjectAlternativeNames {
333+
ip: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))],
334+
dns_name: vec!["my-server.org".to_string()],
335+
},
336+
};
337+
let json = json!({
338+
"export_key": false,
339+
"ip": "192.168.1.1",
340+
"dns_name": "my-server.org"
341+
});
342+
343+
let deserialized: ServerConfiguration = serde_json::from_value(json).unwrap();
344+
345+
assert_eq!(deserialized, expected)
346+
}
257347
}
258348

259349
mod yaml {

test-certs/src/generation.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
Certificate, Error,
88
configuration::certificates::{
99
CertificateAuthorityConfiguration, CertificateType, ClientConfiguration,
10-
ServerConfiguration,
10+
ServerConfiguration, SubjectAlternativeNames,
1111
},
1212
};
1313

@@ -61,7 +61,7 @@ impl ToCertificate for ClientConfiguration {
6161
fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error> {
6262
let key = KeyPair::generate()?;
6363

64-
let mut certificate_params = certificate_params(name, self.ip)?;
64+
let mut certificate_params = certificate_params(name, &self.subject_alternative_names)?;
6565
certificate_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
6666

6767
let certificate = sign_cert(certificate_params, &key, issuer)?;
@@ -79,7 +79,7 @@ impl ToCertificate for ServerConfiguration {
7979
fn certificate(&self, name: &str, issuer: Option<&Certificate>) -> Result<Certificate, Error> {
8080
let key = KeyPair::generate()?;
8181

82-
let mut certificate_params = certificate_params(name, self.ip)?;
82+
let mut certificate_params = certificate_params(name, &self.subject_alternative_names)?;
8383
certificate_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
8484

8585
let certificate = sign_cert(certificate_params, &key, issuer)?;
@@ -127,8 +127,17 @@ fn issuer_params(common_name: &str) -> CertificateParams {
127127
/// Sets basic certificate parameter for client and server auth certificates.
128128
///
129129
/// Sets the subject alt names to the name and the ip.
130-
fn certificate_params(name: &str, ip: IpAddr) -> Result<CertificateParams, Error> {
131-
let mut certificate_params = CertificateParams::new(vec![name.to_string(), ip.to_string()])?;
130+
fn certificate_params(
131+
name: &str,
132+
san: &SubjectAlternativeNames,
133+
) -> Result<CertificateParams, Error> {
134+
let params: Vec<String> = san
135+
.ip
136+
.iter()
137+
.map(|ip| ip.to_string())
138+
.chain(san.dns_name.iter().cloned())
139+
.collect();
140+
let mut certificate_params = CertificateParams::new(params)?;
132141
let mut common_name = DistinguishedName::new();
133142
common_name.push(DnType::CommonName, name);
134143
certificate_params.distinguished_name = common_name;

test-certs/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
use clap::Parser;
55
use test_certs::{
6-
configuration::{certificates::CertificateRoot, Args},
6+
configuration::{Args, certificates::CertificateRoot},
77
generate,
88
};
99
use tracing::info;
@@ -30,7 +30,7 @@ fn main() -> anyhow::Result<()> {
3030
.recursive(true)
3131
.create(&args.outdir)?;
3232

33-
let certificates = generate(root)?;
33+
let certificates = generate(&root)?;
3434

3535
for cert in certificates {
3636
cert.write(&args.outdir)?;

test-certs/tests/examples/intermediate_ca.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ my-root-ca:
1313
my-client:
1414
type: client
1515
ip: 192.168.1.10
16+
dns_name: "my-client.org"
1617
# Create a server auth certificate issued by my-intermediate-ca
1718
my-server:
1819
type: server
19-
ip: 192.168.1.1
20+
# Multiple IPs are also possible
21+
ip:
22+
- 192.168.1.1
23+
- 192.168.1.2
24+
# Multiple dns names are also possible
25+
dns_name:
26+
- "my-server.org"
27+
- "my-server.com"

0 commit comments

Comments
 (0)