Skip to content

Commit 651c78a

Browse files
committed
feat!(stackable-certs): Support adding SAN entries
1 parent 41f9832 commit 651c78a

File tree

3 files changed

+102
-31
lines changed

3 files changed

+102
-31
lines changed
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use stackable_operator::time::Duration;
2+
13
/// The default CA validity time span of one hour (3600 seconds).
2-
pub const DEFAULT_CA_VALIDITY_SECONDS: u64 = 3600;
4+
pub const DEFAULT_CA_VALIDITY: Duration = Duration::from_hours_unchecked(1);
35

46
/// The root CA subject name containing only the common name.
5-
pub const ROOT_CA_SUBJECT: &str = "CN=Stackable Data Platform Internal CA";
7+
pub const SDP_ROOT_CA_SUBJECT: &str = "CN=Stackable Data Platform Internal CA";

crates/stackable-certs/src/ca/mod.rs

Lines changed: 92 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Contains types and functions to generate and sign certificate authorities
22
//! (CAs).
3-
use std::str::FromStr;
3+
use std::{fmt::Debug, str::FromStr};
44

55
use const_oid::db::rfc5280::{ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH};
66
use k8s_openapi::api::core::v1::Secret;
@@ -9,9 +9,10 @@ use snafu::{OptionExt, ResultExt, Snafu};
99
use stackable_operator::{client::Client, commons::secret::SecretReference, time::Duration};
1010
use tracing::{debug, instrument};
1111
use x509_cert::{
12+
Certificate,
1213
builder::{Builder, CertificateBuilder, Profile},
13-
der::{DecodePem, pem::LineEnding, referenced::OwnedToRef},
14-
ext::pkix::{AuthorityKeyIdentifier, ExtendedKeyUsage},
14+
der::{DecodePem, asn1::Ia5String, pem::LineEnding, referenced::OwnedToRef},
15+
ext::pkix::{AuthorityKeyIdentifier, ExtendedKeyUsage, SubjectAltName, name::GeneralName},
1516
name::Name,
1617
serial_number::SerialNumber,
1718
spki::{EncodePublicKey, SubjectPublicKeyInfoOwned},
@@ -66,14 +67,20 @@ pub enum Error {
6667

6768
#[snafu(display("failed to parse AuthorityKeyIdentifier"))]
6869
ParseAuthorityKeyIdentifier { source: x509_cert::der::Error },
70+
71+
#[snafu(display("The subject alternative DNS name \"{dns_name}\" is not a Ia5String"))]
72+
SaDnsNameNotAIa5String {
73+
dns_name: String,
74+
source: x509_cert::der::Error,
75+
},
6976
}
7077

7178
/// Custom implementation of [`std::cmp::PartialEq`] because some inner types
7279
/// don't implement it.
7380
///
74-
/// Note that this implementation is restritced to testing because there is a
81+
/// Note that this implementation is restricted to testing because there is a
7582
/// variant that is impossible to compare, and will cause a panic if it is
76-
/// attemped.
83+
/// attempted.
7784
#[cfg(test)]
7885
impl PartialEq for Error {
7986
fn eq(&self, other: &Self) -> bool {
@@ -170,7 +177,7 @@ where
170177
/// These parameters include:
171178
///
172179
/// - a randomly generated serial number
173-
/// - a default validity of one hour (see [`DEFAULT_CA_VALIDITY_SECONDS`])
180+
/// - a default validity of one hour (see [`DEFAULT_CA_VALIDITY`])
174181
///
175182
/// The CA contains the public half of the provided `signing_key` and is
176183
/// signed by the private half of said key.
@@ -181,9 +188,8 @@ where
181188
#[instrument(name = "create_certificate_authority", skip(signing_key_pair))]
182189
pub fn new(signing_key_pair: S) -> Result<Self> {
183190
let serial_number = rand::random::<u64>();
184-
let validity = Duration::from_secs(DEFAULT_CA_VALIDITY_SECONDS);
185191

186-
Self::new_with(signing_key_pair, serial_number, validity)
192+
Self::new_with(signing_key_pair, serial_number, DEFAULT_CA_VALIDITY)
187193
}
188194

189195
/// Creates a new CA certificate.
@@ -200,8 +206,8 @@ where
200206
// We don't allow customization of the CA subject by callers. Every CA
201207
// created by us should contain the same subject consisting a common set
202208
// of distinguished names (DNs).
203-
let subject = Name::from_str(ROOT_CA_SUBJECT).context(ParseSubjectSnafu {
204-
subject: ROOT_CA_SUBJECT,
209+
let subject = Name::from_str(SDP_ROOT_CA_SUBJECT).context(ParseSubjectSnafu {
210+
subject: SDP_ROOT_CA_SUBJECT,
205211
})?;
206212

207213
let spki_pem = signing_key_pair
@@ -267,15 +273,16 @@ where
267273
/// authentication, because they include [`ID_KP_CLIENT_AUTH`] and
268274
/// [`ID_KP_SERVER_AUTH`] in the extended key usage extension.
269275
///
270-
/// It is also possible to directly greate RSA or ECDSA-based leaf
276+
/// It is also possible to directly create RSA or ECDSA-based leaf
271277
/// certificates using [`CertificateAuthority::generate_rsa_leaf_certificate`]
272278
/// and [`CertificateAuthority::generate_ecdsa_leaf_certificate`].
273279
#[instrument(skip(self, key_pair))]
274-
pub fn generate_leaf_certificate<T>(
280+
pub fn generate_leaf_certificate<'a, T>(
275281
&mut self,
276282
key_pair: T,
277283
name: &str,
278284
scope: &str,
285+
subject_alterative_dns_names: impl IntoIterator<Item = &'a str> + Debug,
279286
validity: Duration,
280287
) -> Result<CertificatePair<T>>
281288
where
@@ -301,10 +308,6 @@ where
301308
let spki = SubjectPublicKeyInfoOwned::from_pem(spki_pem.as_bytes())
302309
.context(DecodeSpkiFromPemSnafu)?;
303310

304-
// The leaf certificate can be used for WWW client and server
305-
// authentication. This is a base requirement for TLS certs.
306-
let eku = ExtendedKeyUsage(vec![ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH]);
307-
308311
let signer = self.certificate_pair.key_pair.signing_key();
309312
let mut builder = CertificateBuilder::new(
310313
Profile::Leaf {
@@ -325,9 +328,27 @@ where
325328
)
326329
.context(CreateCertificateBuilderSnafu)?;
327330

328-
// Again, add the extension created above.
331+
// The leaf certificate can be used for WWW client and server
332+
// authentication. This is a base requirement for TLS certs.
329333
builder
330-
.add_extension(&eku)
334+
.add_extension(&ExtendedKeyUsage(vec![
335+
ID_KP_CLIENT_AUTH,
336+
ID_KP_SERVER_AUTH,
337+
]))
338+
.context(AddCertificateExtensionSnafu)?;
339+
340+
let sans = subject_alterative_dns_names
341+
.into_iter()
342+
.map(|dns_name| {
343+
Ok(GeneralName::DnsName(Ia5String::new(dns_name).context(
344+
SaDnsNameNotAIa5StringSnafu {
345+
dns_name: dns_name.to_string(),
346+
},
347+
)?))
348+
})
349+
.collect::<Result<Vec<_>, Error>>()?;
350+
builder
351+
.add_extension(&SubjectAltName(sans))
331352
.context(AddCertificateExtensionSnafu)?;
332353

333354
debug!("create and sign leaf certificate");
@@ -344,29 +365,31 @@ where
344365
/// See [`CertificateAuthority::generate_leaf_certificate`] for more
345366
/// information.
346367
#[instrument(skip(self))]
347-
pub fn generate_rsa_leaf_certificate(
368+
pub fn generate_rsa_leaf_certificate<'a>(
348369
&mut self,
349370
name: &str,
350371
scope: &str,
372+
subject_alterative_dns_names: impl IntoIterator<Item = &'a str> + Debug,
351373
validity: Duration,
352374
) -> Result<CertificatePair<rsa::SigningKey>> {
353375
let key = rsa::SigningKey::new().context(GenerateRsaSigningKeySnafu)?;
354-
self.generate_leaf_certificate(key, name, scope, validity)
376+
self.generate_leaf_certificate(key, name, scope, subject_alterative_dns_names, validity)
355377
}
356378

357379
/// Generates an ECDSAasync -based leaf certificate which is signed by this CA.
358380
///
359381
/// See [`CertificateAuthority::generate_leaf_certificate`] for more
360382
/// information.
361383
#[instrument(skip(self))]
362-
pub fn generate_ecdsa_leaf_certificate(
384+
pub fn generate_ecdsa_leaf_certificate<'a>(
363385
&mut self,
364386
name: &str,
365387
scope: &str,
388+
subject_alterative_dns_names: impl IntoIterator<Item = &'a str> + Debug,
366389
validity: Duration,
367390
) -> Result<CertificatePair<ecdsa::SigningKey>> {
368391
let key = ecdsa::SigningKey::new().context(GenerateEcdsaSigningKeySnafu)?;
369-
self.generate_leaf_certificate(key, name, scope, validity)
392+
self.generate_leaf_certificate(key, name, scope, subject_alterative_dns_names, validity)
370393
}
371394

372395
/// Create a [`CertificateAuthority`] from a Kubernetes [`Secret`].
@@ -443,6 +466,11 @@ where
443466

444467
Self::from_secret(secret, key_certificate, key_private_key)
445468
}
469+
470+
/// Returns the ca certificate.
471+
pub fn ca_cert(&self) -> &Certificate {
472+
&self.certificate_pair.certificate
473+
}
446474
}
447475

448476
impl CertificateAuthority<rsa::SigningKey> {
@@ -468,19 +496,57 @@ fn format_leaf_certificate_subject(name: &str, scope: &str) -> Result<Name> {
468496

469497
#[cfg(test)]
470498
mod tests {
499+
use const_oid::ObjectIdentifier;
500+
471501
use super::*;
472502

503+
const TEST_CERT_LIFETIME: Duration = Duration::from_hours_unchecked(1);
504+
const TEST_SAN: &str = "airflow-0.airflow.default.svc.cluster.local";
505+
473506
#[tokio::test]
474507
async fn rsa_key_generation() {
475508
let mut ca = CertificateAuthority::new_rsa().unwrap();
476-
ca.generate_rsa_leaf_certificate("Airflow", "pod", Duration::from_secs(3600))
477-
.unwrap();
509+
let cert = ca
510+
.generate_rsa_leaf_certificate("Airflow", "pod", [TEST_SAN], TEST_CERT_LIFETIME)
511+
.expect("RSA certificate generation failed");
512+
513+
assert_cert_attributes(cert.certificate());
478514
}
479515

480516
#[tokio::test]
481517
async fn ecdsa_key_generation() {
482518
let mut ca = CertificateAuthority::new_ecdsa().unwrap();
483-
ca.generate_ecdsa_leaf_certificate("Airflow", "pod", Duration::from_secs(3600))
484-
.unwrap();
519+
let cert = ca
520+
.generate_ecdsa_leaf_certificate("Airflow", "pod", [TEST_SAN], TEST_CERT_LIFETIME)
521+
.expect("ecdsa certificate generation failed");
522+
523+
assert_cert_attributes(cert.certificate());
524+
}
525+
526+
fn assert_cert_attributes(cert: &Certificate) {
527+
let cert = &cert.tbs_certificate;
528+
// Test subject
529+
assert_eq!(
530+
cert.subject,
531+
Name::from_str("CN=Airflow Certificate for pod").unwrap()
532+
);
533+
534+
// Test SAN extension is present
535+
let extensions = cert.extensions.as_ref().expect("cert had no extension");
536+
assert!(
537+
extensions
538+
.iter()
539+
.any(|ext| ext.extn_id == ObjectIdentifier::new_unwrap("2.5.29.17"))
540+
);
541+
542+
// Test lifetime
543+
let not_before = cert.validity.not_before.to_system_time();
544+
let not_after = cert.validity.not_after.to_system_time();
545+
assert_eq!(
546+
not_after
547+
.duration_since(not_before)
548+
.expect("Failed to calculate duration between notBefore and notAfter"),
549+
*TEST_CERT_LIFETIME
550+
);
485551
}
486552
}

crates/stackable-webhook/src/tls.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ use hyper::{body::Incoming, service::service_fn};
88
use hyper_util::rt::{TokioExecutor, TokioIo};
99
use opentelemetry::trace::{FutureExt, SpanKind};
1010
use snafu::{ResultExt, Snafu};
11-
use stackable_certs::{CertificatePairError, ca::CertificateAuthority, keys::rsa};
12-
use stackable_operator::time::Duration;
11+
use stackable_certs::{
12+
CertificatePairError,
13+
ca::{CertificateAuthority, DEFAULT_CA_VALIDITY},
14+
keys::rsa,
15+
};
1316
use tokio::net::TcpListener;
1417
use tokio_rustls::{
1518
TlsAcceptor,
@@ -106,7 +109,7 @@ impl TlsServer {
106109
CertificateAuthority::new_rsa().context(CreateCertificateAuthoritySnafu)?;
107110

108111
let leaf_certificate = certificate_authority
109-
.generate_rsa_leaf_certificate("Leaf", "webhook", Duration::from_secs(3600))
112+
.generate_rsa_leaf_certificate("Leaf", "webhook", [], DEFAULT_CA_VALIDITY)
110113
.context(GenerateLeafCertificateSnafu)?;
111114

112115
let certificate_der = leaf_certificate

0 commit comments

Comments
 (0)