@@ -256,6 +256,86 @@ impl From<Encryption> for EncryptionRaw {
256256 }
257257}
258258
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+
259339/// Application secrets
260340#[ serde_as]
261341#[ derive( Debug , Clone , Serialize , Deserialize , JsonSchema ) ]
@@ -267,8 +347,10 @@ pub struct SecretsConfig {
267347 encryption : Encryption ,
268348
269349 /// 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 ,
272354}
273355
274356impl SecretsConfig {
@@ -279,7 +361,8 @@ impl SecretsConfig {
279361 /// Returns an error when a key could not be imported
280362 #[ tracing:: instrument( name = "secrets.load" , skip_all) ]
281363 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 ?;
283366
284367 Ok ( Keystore :: new ( JsonWebKeySet :: new ( web_keys) ) )
285368 }
@@ -394,7 +477,7 @@ impl SecretsConfig {
394477
395478 Ok ( Self {
396479 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] ) ,
398481 } )
399482 }
400483
@@ -435,7 +518,7 @@ impl SecretsConfig {
435518
436519 Self {
437520 encryption : Encryption :: Value ( [ 0xEA ; 32 ] ) ,
438- keys : vec ! [ rsa_key, ecdsa_key] ,
521+ keys : Keys :: Values ( vec ! [ rsa_key, ecdsa_key] ) ,
439522 }
440523 }
441524}
@@ -451,6 +534,129 @@ mod tests {
451534
452535 use super :: * ;
453536
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+
454660 #[ tokio:: test]
455661 async fn load_config_inline_secrets ( ) {
456662 task:: spawn_blocking ( || {
0 commit comments