@@ -256,6 +256,86 @@ impl From<Encryption> for EncryptionRaw {
256
256
}
257
257
}
258
258
259
+ /// Description of signing keys.
260
+ ///
261
+ /// It either holds the key config values directly or references a directory
262
+ /// where each file contains a key.
263
+ #[ derive( Debug , Clone ) ]
264
+ pub enum Keys {
265
+ Values ( Vec < KeyConfig > ) ,
266
+ Directory ( Utf8PathBuf ) ,
267
+ }
268
+
269
+ impl Keys {
270
+ /// Returns a list of key configs.
271
+ ///
272
+ /// If `keys_dir` was given, the keys are read from file.
273
+ async fn key_configs ( & self ) -> anyhow:: Result < Vec < KeyConfig > > {
274
+ match self {
275
+ Keys :: Values ( key_configs) => Ok ( key_configs. clone ( ) ) ,
276
+ Keys :: Directory ( path) => key_configs_from_path ( path) . await ,
277
+ }
278
+ }
279
+ }
280
+
281
+ /// Reads all keys from the given directory.
282
+ async fn key_configs_from_path ( path : & Utf8PathBuf ) -> anyhow:: Result < Vec < KeyConfig > > {
283
+ let mut result = vec ! [ ] ;
284
+ let mut read_dir = tokio:: fs:: read_dir ( path) . await ?;
285
+ while let Some ( dir_entry) = read_dir. next_entry ( ) . await ? {
286
+ if !dir_entry. path ( ) . is_file ( ) {
287
+ continue ;
288
+ }
289
+ result. push ( KeyConfig {
290
+ kid : None ,
291
+ password : None ,
292
+ key : Key :: File ( dir_entry. path ( ) . try_into ( ) ?) ,
293
+ } ) ;
294
+ }
295
+ Ok ( result)
296
+ }
297
+
298
+ #[ serde_as]
299
+ #[ derive( JsonSchema , Serialize , Deserialize , Debug , Clone ) ]
300
+ struct KeysRaw {
301
+ /// List of private keys to use for signing and encrypting payloads.
302
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
303
+ keys : Option < Vec < KeyConfig > > ,
304
+
305
+ /// Directory of private keys to use for signing and encrypting payloads.
306
+ #[ schemars( with = "Option<String>" ) ]
307
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
308
+ keys_dir : Option < Utf8PathBuf > ,
309
+ }
310
+
311
+ impl TryFrom < KeysRaw > for Keys {
312
+ type Error = anyhow:: Error ;
313
+
314
+ fn try_from ( value : KeysRaw ) -> Result < Keys , Self :: Error > {
315
+ match ( value. keys , value. keys_dir ) {
316
+ ( None , None ) => bail ! ( "Missing `keys` or `keys_dir`" ) ,
317
+ ( None , Some ( path) ) => Ok ( Keys :: Directory ( path) ) ,
318
+ ( Some ( keys) , None ) => Ok ( Keys :: Values ( keys) ) ,
319
+ ( Some ( _) , Some ( _) ) => bail ! ( "Cannot specify both `keys` and `keys_dir`" ) ,
320
+ }
321
+ }
322
+ }
323
+
324
+ impl From < Keys > for KeysRaw {
325
+ fn from ( value : Keys ) -> Self {
326
+ match value {
327
+ Keys :: Directory ( path) => KeysRaw {
328
+ keys_dir : Some ( path) ,
329
+ keys : None ,
330
+ } ,
331
+ Keys :: Values ( keys) => KeysRaw {
332
+ keys_dir : None ,
333
+ keys : Some ( keys) ,
334
+ } ,
335
+ }
336
+ }
337
+ }
338
+
259
339
/// Application secrets
260
340
#[ serde_as]
261
341
#[ derive( Debug , Clone , Serialize , Deserialize , JsonSchema ) ]
@@ -267,8 +347,10 @@ pub struct SecretsConfig {
267
347
encryption : Encryption ,
268
348
269
349
/// List of private keys to use for signing and encrypting payloads
270
- #[ serde( default ) ]
271
- keys : Vec < KeyConfig > ,
350
+ #[ schemars( with = "KeysRaw" ) ]
351
+ #[ serde_as( as = "serde_with::TryFromInto<KeysRaw>" ) ]
352
+ #[ serde( flatten) ]
353
+ keys : Keys ,
272
354
}
273
355
274
356
impl SecretsConfig {
@@ -279,7 +361,8 @@ impl SecretsConfig {
279
361
/// Returns an error when a key could not be imported
280
362
#[ tracing:: instrument( name = "secrets.load" , skip_all) ]
281
363
pub async fn key_store ( & self ) -> anyhow:: Result < Keystore > {
282
- let web_keys = try_join_all ( self . keys . iter ( ) . map ( KeyConfig :: json_web_key) ) . await ?;
364
+ let key_configs = self . keys . key_configs ( ) . await ?;
365
+ let web_keys = try_join_all ( key_configs. iter ( ) . map ( KeyConfig :: json_web_key) ) . await ?;
283
366
284
367
Ok ( Keystore :: new ( JsonWebKeySet :: new ( web_keys) ) )
285
368
}
@@ -394,7 +477,7 @@ impl SecretsConfig {
394
477
395
478
Ok ( Self {
396
479
encryption : Encryption :: Value ( Standard . sample ( & mut rng) ) ,
397
- keys : vec ! [ rsa_key, ec_p256_key, ec_p384_key, ec_k256_key] ,
480
+ keys : Keys :: Values ( vec ! [ rsa_key, ec_p256_key, ec_p384_key, ec_k256_key] ) ,
398
481
} )
399
482
}
400
483
@@ -435,7 +518,7 @@ impl SecretsConfig {
435
518
436
519
Self {
437
520
encryption : Encryption :: Value ( [ 0xEA ; 32 ] ) ,
438
- keys : vec ! [ rsa_key, ecdsa_key] ,
521
+ keys : Keys :: Values ( vec ! [ rsa_key, ecdsa_key] ) ,
439
522
}
440
523
}
441
524
}
@@ -451,6 +534,129 @@ mod tests {
451
534
452
535
use super :: * ;
453
536
537
+ #[ tokio:: test]
538
+ async fn load_config ( ) {
539
+ task:: spawn_blocking ( || {
540
+ Jail :: expect_with ( |jail| {
541
+ jail. create_file (
542
+ "config.yaml" ,
543
+ indoc:: indoc! { r"
544
+ secrets:
545
+ encryption_file: encryption
546
+ keys_dir: keys
547
+ " } ,
548
+ ) ?;
549
+ jail. create_file ( "encryption" , example_secret ( ) ) ?;
550
+ jail. create_dir ( "keys" ) ?;
551
+ jail. create_file (
552
+ "keys/key1" ,
553
+ indoc:: indoc! { r"
554
+ -----BEGIN RSA PRIVATE KEY-----
555
+ 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
604
+ -----END RSA PRIVATE KEY-----
605
+ " } ,
606
+ ) ?;
607
+ jail. create_file (
608
+ "keys/key2" ,
609
+ indoc:: indoc! { r"
610
+ -----BEGIN EC PRIVATE KEY-----
611
+ MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
612
+ AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
613
+ h27LAir5RqxByHvua2XsP46rSTChof78uw==
614
+ -----END EC PRIVATE KEY-----
615
+ " } ,
616
+ ) ?;
617
+
618
+ let config = Figment :: new ( )
619
+ . merge ( Yaml :: file ( "config.yaml" ) )
620
+ . extract_inner :: < SecretsConfig > ( "secrets" ) ?;
621
+
622
+ Handle :: current ( ) . block_on ( async move {
623
+ assert ! (
624
+ matches!( config. encryption, Encryption :: File ( ref p) if p == "encryption" )
625
+ ) ;
626
+ assert_eq ! (
627
+ config. encryption( ) . await . unwrap( ) ,
628
+ [
629
+ 0 , 0 , 17 , 17 , 34 , 34 , 51 , 51 , 68 , 68 , 85 , 85 , 102 , 102 , 119 , 119 , 136 ,
630
+ 136 , 153 , 153 , 170 , 170 , 187 , 187 , 204 , 204 , 221 , 221 , 238 , 238 , 255 ,
631
+ 255
632
+ ]
633
+ ) ;
634
+
635
+ let mut key_config = config. keys . key_configs ( ) . await . unwrap ( ) ;
636
+ key_config. sort_by_key ( |a| {
637
+ if let Key :: File ( p) = & a. key {
638
+ Some ( p. clone ( ) )
639
+ } else {
640
+ None
641
+ }
642
+ } ) ;
643
+ let key_store = config. key_store ( ) . await . unwrap ( ) ;
644
+
645
+ assert ! ( key_config[ 0 ] . kid. is_none( ) ) ;
646
+ assert ! ( matches!( & key_config[ 0 ] . key, Key :: File ( p) if p == "keys/key1" ) ) ;
647
+ assert ! ( key_store. iter( ) . any( |k| k. kid( ) == Some ( "040b0ab8" ) ) ) ;
648
+ assert ! ( key_config[ 1 ] . kid. is_none( ) ) ;
649
+ assert ! ( matches!( & key_config[ 1 ] . key, Key :: File ( p) if p == "keys/key2" ) ) ;
650
+ assert ! ( key_store. iter( ) . any( |k| k. kid( ) == Some ( "7a0a3fc2" ) ) ) ;
651
+ } ) ;
652
+
653
+ Ok ( ( ) )
654
+ } ) ;
655
+ } )
656
+ . await
657
+ . unwrap ( ) ;
658
+ }
659
+
454
660
#[ tokio:: test]
455
661
async fn load_config_inline_secrets ( ) {
456
662
task:: spawn_blocking ( || {
0 commit comments