@@ -244,6 +244,86 @@ impl From<Encryption> for EncryptionRaw {
244
244
}
245
245
}
246
246
247
+ /// Description of signing keys.
248
+ ///
249
+ /// It either holds the key config values directly or references a directory
250
+ /// where each file contains a key.
251
+ #[ derive( Debug , Clone ) ]
252
+ pub enum Keys {
253
+ Values ( Vec < KeyConfig > ) ,
254
+ Directory ( Utf8PathBuf ) ,
255
+ }
256
+
257
+ impl Keys {
258
+ /// Returns a list of key configs.
259
+ ///
260
+ /// If `keys_dir` was given, the keys are read from file.
261
+ async fn key_configs ( & self ) -> anyhow:: Result < Vec < KeyConfig > > {
262
+ match self {
263
+ Keys :: Values ( key_configs) => Ok ( key_configs. clone ( ) ) ,
264
+ Keys :: Directory ( path) => key_configs_from_path ( path) . await ,
265
+ }
266
+ }
267
+ }
268
+
269
+ /// Reads all keys from the given directory.
270
+ async fn key_configs_from_path ( path : & Utf8PathBuf ) -> anyhow:: Result < Vec < KeyConfig > > {
271
+ let mut result = vec ! [ ] ;
272
+ let mut read_dir = tokio:: fs:: read_dir ( path) . await ?;
273
+ while let Some ( dir_entry) = read_dir. next_entry ( ) . await ? {
274
+ if !dir_entry. path ( ) . is_file ( ) {
275
+ continue ;
276
+ }
277
+ result. push ( KeyConfig {
278
+ kid : None ,
279
+ password : None ,
280
+ key : Key :: File ( dir_entry. path ( ) . try_into ( ) ?) ,
281
+ } ) ;
282
+ }
283
+ Ok ( result)
284
+ }
285
+
286
+ #[ serde_as]
287
+ #[ derive( JsonSchema , Serialize , Deserialize , Debug , Clone ) ]
288
+ struct KeysRaw {
289
+ /// List of private keys to use for signing and encrypting payloads.
290
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
291
+ keys : Option < Vec < KeyConfig > > ,
292
+
293
+ /// Directory of private keys to use for signing and encrypting payloads.
294
+ #[ schemars( with = "Option<String>" ) ]
295
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
296
+ keys_dir : Option < Utf8PathBuf > ,
297
+ }
298
+
299
+ impl TryFrom < KeysRaw > for Keys {
300
+ type Error = anyhow:: Error ;
301
+
302
+ fn try_from ( value : KeysRaw ) -> Result < Keys , Self :: Error > {
303
+ match ( value. keys , value. keys_dir ) {
304
+ ( None , None ) => bail ! ( "Missing `keys` or `keys_dir`" ) ,
305
+ ( None , Some ( path) ) => Ok ( Keys :: Directory ( path) ) ,
306
+ ( Some ( keys) , None ) => Ok ( Keys :: Values ( keys) ) ,
307
+ ( Some ( _) , Some ( _) ) => bail ! ( "Cannot specify both `keys` and `keys_dir`" ) ,
308
+ }
309
+ }
310
+ }
311
+
312
+ impl From < Keys > for KeysRaw {
313
+ fn from ( value : Keys ) -> Self {
314
+ match value {
315
+ Keys :: Directory ( path) => KeysRaw {
316
+ keys_dir : Some ( path) ,
317
+ keys : None ,
318
+ } ,
319
+ Keys :: Values ( keys) => KeysRaw {
320
+ keys_dir : None ,
321
+ keys : Some ( keys) ,
322
+ } ,
323
+ }
324
+ }
325
+ }
326
+
247
327
/// Application secrets
248
328
#[ serde_as]
249
329
#[ derive( Debug , Clone , Serialize , Deserialize , JsonSchema ) ]
@@ -255,8 +335,10 @@ pub struct SecretsConfig {
255
335
encryption : Encryption ,
256
336
257
337
/// List of private keys to use for signing and encrypting payloads
258
- #[ serde( default ) ]
259
- keys : Vec < KeyConfig > ,
338
+ #[ schemars( with = "KeysRaw" ) ]
339
+ #[ serde_as( as = "serde_with::TryFromInto<KeysRaw>" ) ]
340
+ #[ serde( flatten) ]
341
+ keys : Keys ,
260
342
}
261
343
262
344
impl SecretsConfig {
@@ -267,7 +349,8 @@ impl SecretsConfig {
267
349
/// Returns an error when a key could not be imported
268
350
#[ tracing:: instrument( name = "secrets.load" , skip_all) ]
269
351
pub async fn key_store ( & self ) -> anyhow:: Result < Keystore > {
270
- let web_keys = try_join_all ( self . keys . iter ( ) . map ( KeyConfig :: json_web_key) ) . await ?;
352
+ let key_configs = self . keys . key_configs ( ) . await ?;
353
+ let web_keys = try_join_all ( key_configs. iter ( ) . map ( KeyConfig :: json_web_key) ) . await ?;
271
354
272
355
Ok ( Keystore :: new ( JsonWebKeySet :: new ( web_keys) ) )
273
356
}
@@ -382,7 +465,7 @@ impl SecretsConfig {
382
465
383
466
Ok ( Self {
384
467
encryption : Encryption :: Value ( Standard . sample ( & mut rng) ) ,
385
- keys : vec ! [ rsa_key, ec_p256_key, ec_p384_key, ec_k256_key] ,
468
+ keys : Keys :: Values ( vec ! [ rsa_key, ec_p256_key, ec_p384_key, ec_k256_key] ) ,
386
469
} )
387
470
}
388
471
@@ -423,7 +506,7 @@ impl SecretsConfig {
423
506
424
507
Self {
425
508
encryption : Encryption :: Value ( [ 0xEA ; 32 ] ) ,
426
- keys : vec ! [ rsa_key, ecdsa_key] ,
509
+ keys : Keys :: Values ( vec ! [ rsa_key, ecdsa_key] ) ,
427
510
}
428
511
}
429
512
}
@@ -439,6 +522,129 @@ mod tests {
439
522
440
523
use super :: * ;
441
524
525
+ #[ tokio:: test]
526
+ async fn load_config ( ) {
527
+ task:: spawn_blocking ( || {
528
+ Jail :: expect_with ( |jail| {
529
+ jail. create_file (
530
+ "config.yaml" ,
531
+ indoc:: indoc! { r"
532
+ secrets:
533
+ encryption_file: encryption
534
+ keys_dir: keys
535
+ " } ,
536
+ ) ?;
537
+ jail. create_file ( "encryption" , example_secret ( ) ) ?;
538
+ jail. create_dir ( "keys" ) ?;
539
+ jail. create_file (
540
+ "keys/key1" ,
541
+ indoc:: indoc! { r"
542
+ -----BEGIN RSA PRIVATE KEY-----
543
+ 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
592
+ -----END RSA PRIVATE KEY-----
593
+ " } ,
594
+ ) ?;
595
+ jail. create_file (
596
+ "keys/key2" ,
597
+ indoc:: indoc! { r"
598
+ -----BEGIN EC PRIVATE KEY-----
599
+ MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
600
+ AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
601
+ h27LAir5RqxByHvua2XsP46rSTChof78uw==
602
+ -----END EC PRIVATE KEY-----
603
+ " } ,
604
+ ) ?;
605
+
606
+ let config = Figment :: new ( )
607
+ . merge ( Yaml :: file ( "config.yaml" ) )
608
+ . extract_inner :: < SecretsConfig > ( "secrets" ) ?;
609
+
610
+ Handle :: current ( ) . block_on ( async move {
611
+ assert ! (
612
+ matches!( config. encryption, Encryption :: File ( ref p) if p == "encryption" )
613
+ ) ;
614
+ assert_eq ! (
615
+ config. encryption( ) . await . unwrap( ) ,
616
+ [
617
+ 0 , 0 , 17 , 17 , 34 , 34 , 51 , 51 , 68 , 68 , 85 , 85 , 102 , 102 , 119 , 119 , 136 ,
618
+ 136 , 153 , 153 , 170 , 170 , 187 , 187 , 204 , 204 , 221 , 221 , 238 , 238 , 255 ,
619
+ 255
620
+ ]
621
+ ) ;
622
+
623
+ let mut key_config = config. keys . key_configs ( ) . await . unwrap ( ) ;
624
+ key_config. sort_by_key ( |a| {
625
+ if let Key :: File ( p) = & a. key {
626
+ Some ( p. clone ( ) )
627
+ } else {
628
+ None
629
+ }
630
+ } ) ;
631
+ let key_store = config. key_store ( ) . await . unwrap ( ) ;
632
+
633
+ assert ! ( key_config[ 0 ] . kid. is_none( ) ) ;
634
+ assert ! ( matches!( & key_config[ 0 ] . key, Key :: File ( p) if p == "keys/key1" ) ) ;
635
+ assert ! ( key_store. iter( ) . any( |k| k. kid( ) == Some ( "xmgGCzGtQFmhEOP0YAqBt-oZyVauSVMXcf4kwcgGZLc" ) ) ) ;
636
+ assert ! ( key_config[ 1 ] . kid. is_none( ) ) ;
637
+ assert ! ( matches!( & key_config[ 1 ] . key, Key :: File ( p) if p == "keys/key2" ) ) ;
638
+ assert ! ( key_store. iter( ) . any( |k| k. kid( ) == Some ( "ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o" ) ) ) ;
639
+ } ) ;
640
+
641
+ Ok ( ( ) )
642
+ } ) ;
643
+ } )
644
+ . await
645
+ . unwrap ( ) ;
646
+ }
647
+
442
648
#[ tokio:: test]
443
649
async fn load_config_inline_secrets ( ) {
444
650
task:: spawn_blocking ( || {
0 commit comments