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
55use const_oid:: db:: rfc5280:: { ID_KP_CLIENT_AUTH , ID_KP_SERVER_AUTH } ;
66use k8s_openapi:: api:: core:: v1:: Secret ;
@@ -9,9 +9,10 @@ use snafu::{OptionExt, ResultExt, Snafu};
99use stackable_operator:: { client:: Client , commons:: secret:: SecretReference , time:: Duration } ;
1010use tracing:: { debug, instrument} ;
1111use 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,22 @@ pub enum Error {
6667
6768 #[ snafu( display( "failed to parse AuthorityKeyIdentifier" ) ) ]
6869 ParseAuthorityKeyIdentifier { source : x509_cert:: der:: Error } ,
70+
71+ #[ snafu( display(
72+ "failed to parse subject alternative DNS name {subject_alternative_dns_name:?} as a Ia5 string"
73+ ) ) ]
74+ ParseSubjectAlternativeDnsName {
75+ subject_alternative_dns_name : String ,
76+ source : x509_cert:: der:: Error ,
77+ } ,
6978}
7079
7180/// Custom implementation of [`std::cmp::PartialEq`] because some inner types
7281/// don't implement it.
7382///
74- /// Note that this implementation is restritced to testing because there is a
83+ /// Note that this implementation is restricted to testing because there is a
7584/// variant that is impossible to compare, and will cause a panic if it is
76- /// attemped .
85+ /// attempted .
7786#[ cfg( test) ]
7887impl PartialEq for Error {
7988 fn eq ( & self , other : & Self ) -> bool {
@@ -170,7 +179,7 @@ where
170179 /// These parameters include:
171180 ///
172181 /// - a randomly generated serial number
173- /// - a default validity of one hour (see [`DEFAULT_CA_VALIDITY_SECONDS `])
182+ /// - a default validity of one hour (see [`DEFAULT_CA_VALIDITY `])
174183 ///
175184 /// The CA contains the public half of the provided `signing_key` and is
176185 /// signed by the private half of said key.
@@ -181,9 +190,8 @@ where
181190 #[ instrument( name = "create_certificate_authority" , skip( signing_key_pair) ) ]
182191 pub fn new ( signing_key_pair : S ) -> Result < Self > {
183192 let serial_number = rand:: random :: < u64 > ( ) ;
184- let validity = Duration :: from_secs ( DEFAULT_CA_VALIDITY_SECONDS ) ;
185193
186- Self :: new_with ( signing_key_pair, serial_number, validity )
194+ Self :: new_with ( signing_key_pair, serial_number, DEFAULT_CA_VALIDITY )
187195 }
188196
189197 /// Creates a new CA certificate.
@@ -200,9 +208,8 @@ where
200208 // We don't allow customization of the CA subject by callers. Every CA
201209 // created by us should contain the same subject consisting a common set
202210 // of distinguished names (DNs).
203- let subject = Name :: from_str ( ROOT_CA_SUBJECT ) . context ( ParseSubjectSnafu {
204- subject : ROOT_CA_SUBJECT ,
205- } ) ?;
211+ let subject = Name :: from_str ( SDP_ROOT_CA_SUBJECT )
212+ . expect ( "the SDP_ROOT_CA_SUBJECT must be a valid subject" ) ;
206213
207214 let spki_pem = signing_key_pair
208215 . verifying_key ( )
@@ -267,15 +274,16 @@ where
267274 /// authentication, because they include [`ID_KP_CLIENT_AUTH`] and
268275 /// [`ID_KP_SERVER_AUTH`] in the extended key usage extension.
269276 ///
270- /// It is also possible to directly greate RSA or ECDSA-based leaf
277+ /// It is also possible to directly create RSA or ECDSA-based leaf
271278 /// certificates using [`CertificateAuthority::generate_rsa_leaf_certificate`]
272279 /// and [`CertificateAuthority::generate_ecdsa_leaf_certificate`].
273280 #[ instrument( skip( self , key_pair) ) ]
274- pub fn generate_leaf_certificate < T > (
281+ pub fn generate_leaf_certificate < ' a , T > (
275282 & mut self ,
276283 key_pair : T ,
277284 name : & str ,
278285 scope : & str ,
286+ subject_alterative_dns_names : impl IntoIterator < Item = & ' a str > + Debug ,
279287 validity : Duration ,
280288 ) -> Result < CertificatePair < T > >
281289 where
@@ -301,10 +309,6 @@ where
301309 let spki = SubjectPublicKeyInfoOwned :: from_pem ( spki_pem. as_bytes ( ) )
302310 . context ( DecodeSpkiFromPemSnafu ) ?;
303311
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-
308312 let signer = self . certificate_pair . key_pair . signing_key ( ) ;
309313 let mut builder = CertificateBuilder :: new (
310314 Profile :: Leaf {
@@ -325,9 +329,27 @@ where
325329 )
326330 . context ( CreateCertificateBuilderSnafu ) ?;
327331
328- // Again, add the extension created above.
332+ // The leaf certificate can be used for WWW client and server
333+ // authentication. This is a base requirement for TLS certs.
334+ builder
335+ . add_extension ( & ExtendedKeyUsage ( vec ! [
336+ ID_KP_CLIENT_AUTH ,
337+ ID_KP_SERVER_AUTH ,
338+ ] ) )
339+ . context ( AddCertificateExtensionSnafu ) ?;
340+
341+ let sans = subject_alterative_dns_names
342+ . into_iter ( )
343+ . map ( |dns_name| {
344+ let ia5_dns_name =
345+ Ia5String :: new ( dns_name) . context ( ParseSubjectAlternativeDnsNameSnafu {
346+ subject_alternative_dns_name : dns_name. to_string ( ) ,
347+ } ) ?;
348+ Ok ( GeneralName :: DnsName ( ia5_dns_name) )
349+ } )
350+ . collect :: < Result < Vec < _ > , Error > > ( ) ?;
329351 builder
330- . add_extension ( & eku )
352+ . add_extension ( & SubjectAltName ( sans ) )
331353 . context ( AddCertificateExtensionSnafu ) ?;
332354
333355 debug ! ( "create and sign leaf certificate" ) ;
@@ -344,29 +366,31 @@ where
344366 /// See [`CertificateAuthority::generate_leaf_certificate`] for more
345367 /// information.
346368 #[ instrument( skip( self ) ) ]
347- pub fn generate_rsa_leaf_certificate (
369+ pub fn generate_rsa_leaf_certificate < ' a > (
348370 & mut self ,
349371 name : & str ,
350372 scope : & str ,
373+ subject_alterative_dns_names : impl IntoIterator < Item = & ' a str > + Debug ,
351374 validity : Duration ,
352375 ) -> Result < CertificatePair < rsa:: SigningKey > > {
353376 let key = rsa:: SigningKey :: new ( ) . context ( GenerateRsaSigningKeySnafu ) ?;
354- self . generate_leaf_certificate ( key, name, scope, validity)
377+ self . generate_leaf_certificate ( key, name, scope, subject_alterative_dns_names , validity)
355378 }
356379
357380 /// Generates an ECDSAasync -based leaf certificate which is signed by this CA.
358381 ///
359382 /// See [`CertificateAuthority::generate_leaf_certificate`] for more
360383 /// information.
361384 #[ instrument( skip( self ) ) ]
362- pub fn generate_ecdsa_leaf_certificate (
385+ pub fn generate_ecdsa_leaf_certificate < ' a > (
363386 & mut self ,
364387 name : & str ,
365388 scope : & str ,
389+ subject_alterative_dns_names : impl IntoIterator < Item = & ' a str > + Debug ,
366390 validity : Duration ,
367391 ) -> Result < CertificatePair < ecdsa:: SigningKey > > {
368392 let key = ecdsa:: SigningKey :: new ( ) . context ( GenerateEcdsaSigningKeySnafu ) ?;
369- self . generate_leaf_certificate ( key, name, scope, validity)
393+ self . generate_leaf_certificate ( key, name, scope, subject_alterative_dns_names , validity)
370394 }
371395
372396 /// Create a [`CertificateAuthority`] from a Kubernetes [`Secret`].
@@ -443,6 +467,11 @@ where
443467
444468 Self :: from_secret ( secret, key_certificate, key_private_key)
445469 }
470+
471+ /// Returns the ca certificate.
472+ pub fn ca_cert ( & self ) -> & Certificate {
473+ & self . certificate_pair . certificate
474+ }
446475}
447476
448477impl CertificateAuthority < rsa:: SigningKey > {
@@ -468,19 +497,61 @@ fn format_leaf_certificate_subject(name: &str, scope: &str) -> Result<Name> {
468497
469498#[ cfg( test) ]
470499mod tests {
500+ use const_oid:: ObjectIdentifier ;
501+
471502 use super :: * ;
472503
504+ const TEST_CERT_LIFETIME : Duration = Duration :: from_hours_unchecked ( 1 ) ;
505+ const TEST_SAN : & str = "product-0.product.default.svc.cluster.local" ;
506+
473507 #[ tokio:: test]
474508 async fn rsa_key_generation ( ) {
475509 let mut ca = CertificateAuthority :: new_rsa ( ) . unwrap ( ) ;
476- ca. generate_rsa_leaf_certificate ( "Airflow" , "pod" , Duration :: from_secs ( 3600 ) )
477- . unwrap ( ) ;
510+ let cert = ca
511+ . generate_rsa_leaf_certificate ( "Product" , "pod" , [ TEST_SAN ] , TEST_CERT_LIFETIME )
512+ . expect (
513+ "Must be able to generate an RSA certificate. Perhaps there was an RNG failure" ,
514+ ) ;
515+
516+ assert_cert_attributes ( cert. certificate ( ) ) ;
478517 }
479518
480519 #[ tokio:: test]
481520 async fn ecdsa_key_generation ( ) {
482521 let mut ca = CertificateAuthority :: new_ecdsa ( ) . unwrap ( ) ;
483- ca. generate_ecdsa_leaf_certificate ( "Airflow" , "pod" , Duration :: from_secs ( 3600 ) )
484- . unwrap ( ) ;
522+ let cert = ca
523+ . generate_ecdsa_leaf_certificate ( "Product" , "pod" , [ TEST_SAN ] , TEST_CERT_LIFETIME )
524+ . expect (
525+ "Must be able to generate an ECDSA certificate. Perhaps there was an RNG failure" ,
526+ ) ;
527+
528+ assert_cert_attributes ( cert. certificate ( ) ) ;
529+ }
530+
531+ fn assert_cert_attributes ( cert : & Certificate ) {
532+ let cert = & cert. tbs_certificate ;
533+ // Test subject
534+ assert_eq ! (
535+ cert. subject,
536+ Name :: from_str( "CN=Product Certificate for pod" ) . unwrap( )
537+ ) ;
538+
539+ // Test SAN extension is present
540+ let extensions = cert. extensions . as_ref ( ) . expect ( "cert must have extensions" ) ;
541+ assert ! (
542+ extensions
543+ . iter( )
544+ . any( |ext| ext. extn_id == ObjectIdentifier :: new_unwrap( "2.5.29.17" ) )
545+ ) ;
546+
547+ // Test lifetime
548+ let not_before = cert. validity . not_before . to_system_time ( ) ;
549+ let not_after = cert. validity . not_after . to_system_time ( ) ;
550+ assert_eq ! (
551+ not_after
552+ . duration_since( not_before)
553+ . expect( "notBefore must be before notAfter" ) ,
554+ * TEST_CERT_LIFETIME
555+ ) ;
485556 }
486557}
0 commit comments