Skip to content

Commit 8f86a93

Browse files
committed
Support SAN IPs
1 parent ac18bd9 commit 8f86a93

File tree

1 file changed

+65
-18
lines changed

1 file changed

+65
-18
lines changed

crates/stackable-certs/src/cert_builder.rs

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::fmt::Debug;
1+
use std::{fmt::Debug, net::IpAddr};
22

33
use bon::Builder;
44
use const_oid::db::rfc5280::{ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH};
@@ -52,9 +52,11 @@ where
5252
#[snafu(display("failed to add certificate extension"))]
5353
AddCertificateExtension { source: x509_cert::builder::Error },
5454

55-
#[snafu(display("The subject alternative DNS name \"{dns_name}\" is not a Ia5String"))]
56-
SaDnsNameNotAIa5String {
57-
dns_name: String,
55+
#[snafu(display(
56+
"failed to parse subject alternative DNS name \"{subject_alternative_dns_name}\" as a Ia5 string"
57+
))]
58+
ParseSubjectAlternativeDnsName {
59+
subject_alternative_dns_name: String,
5860
source: x509_cert::der::Error,
5961
},
6062

@@ -93,11 +95,16 @@ where
9395
/// Required subject of the certificate, usually starts with `CN=`.
9496
subject: &'a str,
9597

96-
/// Optional list of subject alternative names (SAN) DNS entries,
98+
/// Optional list of subject alternative name DNS entries
9799
/// that are added to the certificate.
98100
#[builder(default)]
99101
subject_alterative_dns_names: &'a [&'a str],
100102

103+
/// Optional list of subject alternative name IP address entries
104+
/// that are added to the certificate.
105+
#[builder(default)]
106+
subject_alterative_ip_addresses: &'a [IpAddr],
107+
101108
/// Validity/lifetime of the certificate.
102109
///
103110
/// If not specified the default of [`DEFAULT_CERTIFICATE_VALIDITY`] will be used.
@@ -194,17 +201,23 @@ where
194201
]))
195202
.context(AddCertificateExtensionSnafu)?;
196203

197-
let sans = self
198-
.subject_alterative_dns_names
204+
let san_dns = self.subject_alterative_dns_names.iter().map(|dns_name| {
205+
Ok(GeneralName::DnsName(
206+
Ia5String::new(dns_name).with_context(|_| ParseSubjectAlternativeDnsNameSnafu {
207+
subject_alternative_dns_name: dns_name.to_string(),
208+
})?,
209+
))
210+
});
211+
let san_ips = self
212+
.subject_alterative_ip_addresses
199213
.iter()
200-
.map(|dns_name| {
201-
Ok(GeneralName::DnsName(Ia5String::new(dns_name).context(
202-
SaDnsNameNotAIa5StringSnafu {
203-
dns_name: dns_name.to_string(),
204-
},
205-
)?))
206-
})
214+
.copied()
215+
.map(GeneralName::from)
216+
.map(Result::Ok);
217+
let sans = san_dns
218+
.chain(san_ips)
207219
.collect::<Result<Vec<_>, CreateCertificateError<KP::Error>>>()?;
220+
208221
builder
209222
.add_extension(&SubjectAltName(sans))
210223
.context(AddCertificateExtensionSnafu)?;
@@ -221,6 +234,8 @@ where
221234

222235
#[cfg(test)]
223236
mod tests {
237+
use std::net::{Ipv4Addr, Ipv6Addr};
238+
224239
use x509_cert::{
225240
certificate::TbsCertificateInner, der::Decode, ext::pkix::ID_CE_SUBJECT_ALT_NAME,
226241
};
@@ -247,6 +262,7 @@ mod tests {
247262
&certificate.certificate.tbs_certificate,
248263
"CN=trino-coordinator-default-0",
249264
&[],
265+
&[],
250266
DEFAULT_CERTIFICATE_VALIDITY,
251267
None,
252268
);
@@ -262,10 +278,12 @@ mod tests {
262278
"trino-coordinator-default-0.trino-coordinator-default.default.svc.cluster-local",
263279
"trino-coordinator-default.default.svc.cluster-local",
264280
];
281+
let san_ips = ["10.0.0.1".parse().unwrap(), "fe80::42".parse().unwrap()];
265282

266283
let certificate = CertificateBuilder::builder()
267284
.subject("CN=trino-coordinator-default-0")
268285
.subject_alterative_dns_names(&sans)
286+
.subject_alterative_ip_addresses(&san_ips)
269287
.serial_number(08121997)
270288
.validity(Duration::from_days_unchecked(42))
271289
.key_pair(rsa::SigningKey::new().unwrap())
@@ -277,6 +295,7 @@ mod tests {
277295
&certificate.certificate.tbs_certificate,
278296
"CN=trino-coordinator-default-0",
279297
&sans,
298+
&san_ips,
280299
Duration::from_days_unchecked(42),
281300
Some(08121997),
282301
);
@@ -286,6 +305,7 @@ mod tests {
286305
certificate: &TbsCertificateInner,
287306
subject: &str,
288307
sans: &[&str],
308+
san_ips: &[IpAddr],
289309
validity: Duration,
290310
serial_number: Option<u64>,
291311
) {
@@ -300,17 +320,25 @@ mod tests {
300320
.find(|ext| ext.extn_id == ID_CE_SUBJECT_ALT_NAME)
301321
.expect("cert had no SAN extension");
302322

303-
let san_extension = SubjectAltName::from_der(san_extension.extn_value.as_bytes())
304-
.expect("failed to parse SAN");
305-
let actual_sans = san_extension
306-
.0
323+
let san_entries = SubjectAltName::from_der(san_extension.extn_value.as_bytes())
324+
.expect("failed to parse SAN")
325+
.0;
326+
let actual_sans = san_entries
307327
.iter()
308328
.filter_map(|san| match san {
309329
GeneralName::DnsName(dns_name) => Some(dns_name.as_str()),
310330
_ => None,
311331
})
312332
.collect::<Vec<_>>();
313333
assert_eq!(actual_sans, sans);
334+
let actual_san_ips = san_entries
335+
.iter()
336+
.filter_map(|san| match san {
337+
GeneralName::IpAddress(ip) => Some(bytes_to_ip_addr(ip.as_bytes())),
338+
_ => None,
339+
})
340+
.collect::<Vec<_>>();
341+
assert_eq!(actual_san_ips, san_ips);
314342

315343
let not_before = certificate.validity.not_before.to_system_time();
316344
let not_after = certificate.validity.not_after.to_system_time();
@@ -327,4 +355,23 @@ mod tests {
327355
assert_ne!(certificate.serial_number, SerialNumber::from(0_u64))
328356
}
329357
}
358+
359+
fn bytes_to_ip_addr(bytes: &[u8]) -> IpAddr {
360+
match bytes.len() {
361+
4 => {
362+
let mut array = [0u8; 4];
363+
array.copy_from_slice(bytes);
364+
IpAddr::V4(Ipv4Addr::from(array))
365+
}
366+
16 => {
367+
let mut array = [0u8; 16];
368+
array.copy_from_slice(bytes);
369+
IpAddr::V6(Ipv6Addr::from(array))
370+
}
371+
_ => panic!(
372+
"Invalid IP byte length: expected 4 or 16, got {}",
373+
bytes.len()
374+
),
375+
}
376+
}
330377
}

0 commit comments

Comments
 (0)