@@ -244,6 +244,86 @@ impl From<Encryption> for EncryptionRaw {
244244 }
245245}
246246
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+
247327/// Application secrets
248328#[ serde_as]
249329#[ derive( Debug , Clone , Serialize , Deserialize , JsonSchema ) ]
@@ -255,8 +335,10 @@ pub struct SecretsConfig {
255335 encryption : Encryption ,
256336
257337 /// 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 ,
260342}
261343
262344impl SecretsConfig {
@@ -267,7 +349,8 @@ impl SecretsConfig {
267349 /// Returns an error when a key could not be imported
268350 #[ tracing:: instrument( name = "secrets.load" , skip_all) ]
269351 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 ?;
271354
272355 Ok ( Keystore :: new ( JsonWebKeySet :: new ( web_keys) ) )
273356 }
@@ -382,7 +465,7 @@ impl SecretsConfig {
382465
383466 Ok ( Self {
384467 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] ) ,
386469 } )
387470 }
388471
@@ -423,7 +506,7 @@ impl SecretsConfig {
423506
424507 Self {
425508 encryption : Encryption :: Value ( [ 0xEA ; 32 ] ) ,
426- keys : vec ! [ rsa_key, ecdsa_key] ,
509+ keys : Keys :: Values ( vec ! [ rsa_key, ecdsa_key] ) ,
427510 }
428511 }
429512}
@@ -439,6 +522,129 @@ mod tests {
439522
440523 use super :: * ;
441524
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+
442648 #[ tokio:: test]
443649 async fn load_config_inline_secrets ( ) {
444650 task:: spawn_blocking ( || {
0 commit comments