1
1
//! Contains types and functions to generate and sign certificate authorities
2
2
//! (CAs).
3
- use std:: str:: FromStr ;
3
+ use std:: { fmt :: Debug , str:: FromStr } ;
4
4
5
5
use const_oid:: db:: rfc5280:: { ID_KP_CLIENT_AUTH , ID_KP_SERVER_AUTH } ;
6
6
use k8s_openapi:: api:: core:: v1:: Secret ;
@@ -9,9 +9,10 @@ use snafu::{OptionExt, ResultExt, Snafu};
9
9
use stackable_operator:: { client:: Client , commons:: secret:: SecretReference , time:: Duration } ;
10
10
use tracing:: { debug, instrument} ;
11
11
use x509_cert:: {
12
+ Certificate ,
12
13
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 } ,
15
16
name:: Name ,
16
17
serial_number:: SerialNumber ,
17
18
spki:: { EncodePublicKey , SubjectPublicKeyInfoOwned } ,
@@ -66,14 +67,22 @@ pub enum Error {
66
67
67
68
#[ snafu( display( "failed to parse AuthorityKeyIdentifier" ) ) ]
68
69
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
+ } ,
69
78
}
70
79
71
80
/// Custom implementation of [`std::cmp::PartialEq`] because some inner types
72
81
/// don't implement it.
73
82
///
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
75
84
/// variant that is impossible to compare, and will cause a panic if it is
76
- /// attemped .
85
+ /// attempted .
77
86
#[ cfg( test) ]
78
87
impl PartialEq for Error {
79
88
fn eq ( & self , other : & Self ) -> bool {
@@ -170,7 +179,7 @@ where
170
179
/// These parameters include:
171
180
///
172
181
/// - 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 `])
174
183
///
175
184
/// The CA contains the public half of the provided `signing_key` and is
176
185
/// signed by the private half of said key.
@@ -181,9 +190,8 @@ where
181
190
#[ instrument( name = "create_certificate_authority" , skip( signing_key_pair) ) ]
182
191
pub fn new ( signing_key_pair : S ) -> Result < Self > {
183
192
let serial_number = rand:: random :: < u64 > ( ) ;
184
- let validity = Duration :: from_secs ( DEFAULT_CA_VALIDITY_SECONDS ) ;
185
193
186
- Self :: new_with ( signing_key_pair, serial_number, validity )
194
+ Self :: new_with ( signing_key_pair, serial_number, DEFAULT_CA_VALIDITY )
187
195
}
188
196
189
197
/// Creates a new CA certificate.
@@ -200,9 +208,8 @@ where
200
208
// We don't allow customization of the CA subject by callers. Every CA
201
209
// created by us should contain the same subject consisting a common set
202
210
// 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" ) ;
206
213
207
214
let spki_pem = signing_key_pair
208
215
. verifying_key ( )
@@ -267,15 +274,16 @@ where
267
274
/// authentication, because they include [`ID_KP_CLIENT_AUTH`] and
268
275
/// [`ID_KP_SERVER_AUTH`] in the extended key usage extension.
269
276
///
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
271
278
/// certificates using [`CertificateAuthority::generate_rsa_leaf_certificate`]
272
279
/// and [`CertificateAuthority::generate_ecdsa_leaf_certificate`].
273
280
#[ instrument( skip( self , key_pair) ) ]
274
- pub fn generate_leaf_certificate < T > (
281
+ pub fn generate_leaf_certificate < ' a , T > (
275
282
& mut self ,
276
283
key_pair : T ,
277
284
name : & str ,
278
285
scope : & str ,
286
+ subject_alterative_dns_names : impl IntoIterator < Item = & ' a str > + Debug ,
279
287
validity : Duration ,
280
288
) -> Result < CertificatePair < T > >
281
289
where
@@ -301,10 +309,6 @@ where
301
309
let spki = SubjectPublicKeyInfoOwned :: from_pem ( spki_pem. as_bytes ( ) )
302
310
. context ( DecodeSpkiFromPemSnafu ) ?;
303
311
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
-
308
312
let signer = self . certificate_pair . key_pair . signing_key ( ) ;
309
313
let mut builder = CertificateBuilder :: new (
310
314
Profile :: Leaf {
@@ -325,9 +329,27 @@ where
325
329
)
326
330
. context ( CreateCertificateBuilderSnafu ) ?;
327
331
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 > > ( ) ?;
329
351
builder
330
- . add_extension ( & eku )
352
+ . add_extension ( & SubjectAltName ( sans ) )
331
353
. context ( AddCertificateExtensionSnafu ) ?;
332
354
333
355
debug ! ( "create and sign leaf certificate" ) ;
@@ -344,29 +366,31 @@ where
344
366
/// See [`CertificateAuthority::generate_leaf_certificate`] for more
345
367
/// information.
346
368
#[ instrument( skip( self ) ) ]
347
- pub fn generate_rsa_leaf_certificate (
369
+ pub fn generate_rsa_leaf_certificate < ' a > (
348
370
& mut self ,
349
371
name : & str ,
350
372
scope : & str ,
373
+ subject_alterative_dns_names : impl IntoIterator < Item = & ' a str > + Debug ,
351
374
validity : Duration ,
352
375
) -> Result < CertificatePair < rsa:: SigningKey > > {
353
376
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)
355
378
}
356
379
357
380
/// Generates an ECDSAasync -based leaf certificate which is signed by this CA.
358
381
///
359
382
/// See [`CertificateAuthority::generate_leaf_certificate`] for more
360
383
/// information.
361
384
#[ instrument( skip( self ) ) ]
362
- pub fn generate_ecdsa_leaf_certificate (
385
+ pub fn generate_ecdsa_leaf_certificate < ' a > (
363
386
& mut self ,
364
387
name : & str ,
365
388
scope : & str ,
389
+ subject_alterative_dns_names : impl IntoIterator < Item = & ' a str > + Debug ,
366
390
validity : Duration ,
367
391
) -> Result < CertificatePair < ecdsa:: SigningKey > > {
368
392
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)
370
394
}
371
395
372
396
/// Create a [`CertificateAuthority`] from a Kubernetes [`Secret`].
@@ -443,6 +467,11 @@ where
443
467
444
468
Self :: from_secret ( secret, key_certificate, key_private_key)
445
469
}
470
+
471
+ /// Returns the ca certificate.
472
+ pub fn ca_cert ( & self ) -> & Certificate {
473
+ & self . certificate_pair . certificate
474
+ }
446
475
}
447
476
448
477
impl CertificateAuthority < rsa:: SigningKey > {
@@ -468,19 +497,61 @@ fn format_leaf_certificate_subject(name: &str, scope: &str) -> Result<Name> {
468
497
469
498
#[ cfg( test) ]
470
499
mod tests {
500
+ use const_oid:: ObjectIdentifier ;
501
+
471
502
use super :: * ;
472
503
504
+ const TEST_CERT_LIFETIME : Duration = Duration :: from_hours_unchecked ( 1 ) ;
505
+ const TEST_SAN : & str = "product-0.product.default.svc.cluster.local" ;
506
+
473
507
#[ tokio:: test]
474
508
async fn rsa_key_generation ( ) {
475
509
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 ( ) ) ;
478
517
}
479
518
480
519
#[ tokio:: test]
481
520
async fn ecdsa_key_generation ( ) {
482
521
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
+ ) ;
485
556
}
486
557
}
0 commit comments