@@ -8,6 +8,7 @@ use std::borrow::Cow;
88
99use anyhow:: { Context , bail} ;
1010use camino:: Utf8PathBuf ;
11+ use futures_util:: future:: { try_join, try_join_all} ;
1112use mas_jose:: jwk:: { JsonWebKey , JsonWebKeySet } ;
1213use mas_keystore:: { Encrypter , Keystore , PrivateKey } ;
1314use rand:: {
@@ -27,23 +28,160 @@ fn example_secret() -> &'static str {
2728 "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
2829}
2930
30- #[ derive( JsonSchema , Serialize , Deserialize , Clone , Debug ) ]
31- pub struct KeyConfig {
32- kid : String ,
31+ /// Password config option.
32+ ///
33+ /// It either holds the password value directly or references a file where the
34+ /// password is stored.
35+ #[ derive( Clone , Debug ) ]
36+ pub enum Password {
37+ File ( Utf8PathBuf ) ,
38+ Value ( String ) ,
39+ }
3340
41+ /// Password fields as serialized in JSON.
42+ #[ derive( JsonSchema , Serialize , Deserialize , Clone , Debug ) ]
43+ struct PasswordRaw {
44+ #[ schemars( with = "Option<String>" ) ]
45+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
46+ password_file : Option < Utf8PathBuf > ,
3447 #[ serde( skip_serializing_if = "Option::is_none" ) ]
3548 password : Option < String > ,
49+ }
3650
37- #[ serde( skip_serializing_if = "Option::is_none" ) ]
38- #[ schemars( with = "Option<String>" ) ]
39- password_file : Option < Utf8PathBuf > ,
51+ impl TryFrom < PasswordRaw > for Option < Password > {
52+ type Error = anyhow:: Error ;
4053
41- #[ serde( skip_serializing_if = "Option::is_none" ) ]
42- key : Option < String > ,
54+ fn try_from ( value : PasswordRaw ) -> Result < Self , Self :: Error > {
55+ match ( value. password , value. password_file ) {
56+ ( None , None ) => Ok ( None ) ,
57+ ( None , Some ( path) ) => Ok ( Some ( Password :: File ( path) ) ) ,
58+ ( Some ( password) , None ) => Ok ( Some ( Password :: Value ( password) ) ) ,
59+ ( Some ( _) , Some ( _) ) => bail ! ( "Cannot specify both `password` and `password_file`" ) ,
60+ }
61+ }
62+ }
4363
44- #[ serde( skip_serializing_if = "Option::is_none" ) ]
64+ impl From < Option < Password > > for PasswordRaw {
65+ fn from ( value : Option < Password > ) -> Self {
66+ match value {
67+ Some ( Password :: File ( path) ) => PasswordRaw {
68+ password_file : Some ( path) ,
69+ password : None ,
70+ } ,
71+ Some ( Password :: Value ( password) ) => PasswordRaw {
72+ password_file : None ,
73+ password : Some ( password) ,
74+ } ,
75+ None => PasswordRaw {
76+ password_file : None ,
77+ password : None ,
78+ } ,
79+ }
80+ }
81+ }
82+
83+ /// Key config option.
84+ ///
85+ /// It either holds the key value directly or references a file where the key is
86+ /// stored.
87+ #[ derive( Clone , Debug ) ]
88+ pub enum Key {
89+ File ( Utf8PathBuf ) ,
90+ Value ( String ) ,
91+ }
92+
93+ /// Key fields as serialized in JSON.
94+ #[ derive( JsonSchema , Serialize , Deserialize , Clone , Debug ) ]
95+ struct KeyRaw {
4596 #[ schemars( with = "Option<String>" ) ]
97+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
4698 key_file : Option < Utf8PathBuf > ,
99+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
100+ key : Option < String > ,
101+ }
102+
103+ impl TryFrom < KeyRaw > for Key {
104+ type Error = anyhow:: Error ;
105+
106+ fn try_from ( value : KeyRaw ) -> Result < Key , Self :: Error > {
107+ match ( value. key , value. key_file ) {
108+ ( None , None ) => bail ! ( "Missing `key` or `key_file`" ) ,
109+ ( None , Some ( path) ) => Ok ( Key :: File ( path) ) ,
110+ ( Some ( key) , None ) => Ok ( Key :: Value ( key) ) ,
111+ ( Some ( _) , Some ( _) ) => bail ! ( "Cannot specify both `key` and `key_file`" ) ,
112+ }
113+ }
114+ }
115+
116+ impl From < Key > for KeyRaw {
117+ fn from ( value : Key ) -> Self {
118+ match value {
119+ Key :: File ( path) => KeyRaw {
120+ key_file : Some ( path) ,
121+ key : None ,
122+ } ,
123+ Key :: Value ( key) => KeyRaw {
124+ key_file : None ,
125+ key : Some ( key) ,
126+ } ,
127+ }
128+ }
129+ }
130+
131+ /// A single key with its key ID and optional password.
132+ #[ serde_as]
133+ #[ derive( JsonSchema , Serialize , Deserialize , Clone , Debug ) ]
134+ pub struct KeyConfig {
135+ kid : String ,
136+
137+ #[ schemars( with = "PasswordRaw" ) ]
138+ #[ serde_as( as = "serde_with::TryFromInto<PasswordRaw>" ) ]
139+ #[ serde( flatten) ]
140+ password : Option < Password > ,
141+
142+ #[ schemars( with = "KeyRaw" ) ]
143+ #[ serde_as( as = "serde_with::TryFromInto<KeyRaw>" ) ]
144+ #[ serde( flatten) ]
145+ key : Key ,
146+ }
147+
148+ impl KeyConfig {
149+ /// Returns the password in case any is provided.
150+ ///
151+ /// If `password_file` was given, the password is read from that file.
152+ async fn password ( & self ) -> anyhow:: Result < Option < Cow < String > > > {
153+ Ok ( match & self . password {
154+ Some ( Password :: File ( path) ) => Some ( Cow :: Owned ( tokio:: fs:: read_to_string ( path) . await ?) ) ,
155+ Some ( Password :: Value ( password) ) => Some ( Cow :: Borrowed ( password) ) ,
156+ None => None ,
157+ } )
158+ }
159+
160+ /// Returns the key.
161+ ///
162+ /// If `key_file` was given, the key is read from that file.
163+ async fn key ( & self ) -> anyhow:: Result < Cow < String > > {
164+ Ok ( match & self . key {
165+ Key :: File ( path) => Cow :: Owned ( tokio:: fs:: read_to_string ( path) . await ?) ,
166+ Key :: Value ( key) => Cow :: Borrowed ( key) ,
167+ } )
168+ }
169+
170+ /// Returns the JSON Web Key derived from this key config.
171+ ///
172+ /// Password and/or key are read from file if they’re given as path.
173+ async fn json_web_key ( & self ) -> anyhow:: Result < JsonWebKey < mas_keystore:: PrivateKey > > {
174+ let ( key, password) = try_join ( self . key ( ) , self . password ( ) ) . await ?;
175+
176+ let private_key = match password {
177+ Some ( password) => PrivateKey :: load_encrypted ( key. as_bytes ( ) , password. as_bytes ( ) ) ?,
178+ None => PrivateKey :: load ( key. as_bytes ( ) ) ?,
179+ } ;
180+
181+ Ok ( JsonWebKey :: new ( private_key)
182+ . with_kid ( self . kid . clone ( ) )
183+ . with_use ( mas_iana:: jose:: JsonWebKeyUse :: Sig ) )
184+ }
47185}
48186
49187/// Application secrets
@@ -72,49 +210,9 @@ impl SecretsConfig {
72210 /// Returns an error when a key could not be imported
73211 #[ tracing:: instrument( name = "secrets.load" , skip_all) ]
74212 pub async fn key_store ( & self ) -> anyhow:: Result < Keystore > {
75- let mut keys = Vec :: with_capacity ( self . keys . len ( ) ) ;
76- for item in & self . keys {
77- let password = match ( & item. password , & item. password_file ) {
78- ( None , None ) => None ,
79- ( Some ( _) , Some ( _) ) => {
80- bail ! ( "Cannot specify both `password` and `password_file`" )
81- }
82- ( Some ( password) , None ) => Some ( Cow :: Borrowed ( password) ) ,
83- ( None , Some ( path) ) => Some ( Cow :: Owned ( tokio:: fs:: read_to_string ( path) . await ?) ) ,
84- } ;
85-
86- // Read the key either embedded in the config file or on disk
87- let key = match ( & item. key , & item. key_file ) {
88- ( None , None ) => bail ! ( "Missing `key` or `key_file`" ) ,
89- ( Some ( _) , Some ( _) ) => bail ! ( "Cannot specify both `key` and `key_file`" ) ,
90- ( Some ( key) , None ) => {
91- // If the key was embedded in the config file, assume it is formatted as PEM
92- if let Some ( password) = password {
93- PrivateKey :: load_encrypted_pem ( key, password. as_bytes ( ) ) ?
94- } else {
95- PrivateKey :: load_pem ( key) ?
96- }
97- }
98- ( None , Some ( path) ) => {
99- // When reading from disk, it might be either PEM or DER. `PrivateKey::load*`
100- // will try both.
101- let key = tokio:: fs:: read ( path) . await ?;
102- if let Some ( password) = password {
103- PrivateKey :: load_encrypted ( & key, password. as_bytes ( ) ) ?
104- } else {
105- PrivateKey :: load ( & key) ?
106- }
107- }
108- } ;
109-
110- let key = JsonWebKey :: new ( key)
111- . with_kid ( item. kid . clone ( ) )
112- . with_use ( mas_iana:: jose:: JsonWebKeyUse :: Sig ) ;
113- keys. push ( key) ;
114- }
213+ let web_keys = try_join_all ( self . keys . iter ( ) . map ( KeyConfig :: json_web_key) ) . await ?;
115214
116- let keys = JsonWebKeySet :: new ( keys) ;
117- Ok ( Keystore :: new ( keys) )
215+ Ok ( Keystore :: new ( JsonWebKeySet :: new ( web_keys) ) )
118216 }
119217
120218 /// Derive an [`Encrypter`] out of the config
@@ -126,43 +224,6 @@ impl SecretsConfig {
126224
127225impl ConfigurationSection for SecretsConfig {
128226 const PATH : Option < & ' static str > = Some ( "secrets" ) ;
129-
130- fn validate ( & self , figment : & figment:: Figment ) -> Result < ( ) , figment:: Error > {
131- for ( index, key) in self . keys . iter ( ) . enumerate ( ) {
132- let annotate = |mut error : figment:: Error | {
133- error. metadata = figment
134- . find_metadata ( & format ! ( "{root}.keys" , root = Self :: PATH . unwrap( ) ) )
135- . cloned ( ) ;
136- error. profile = Some ( figment:: Profile :: Default ) ;
137- error. path = vec ! [
138- Self :: PATH . unwrap( ) . to_owned( ) ,
139- "keys" . to_owned( ) ,
140- index. to_string( ) ,
141- ] ;
142- Err ( error)
143- } ;
144-
145- if key. key . is_none ( ) && key. key_file . is_none ( ) {
146- return annotate ( figment:: Error :: from (
147- "Missing `key` or `key_file`" . to_owned ( ) ,
148- ) ) ;
149- }
150-
151- if key. key . is_some ( ) && key. key_file . is_some ( ) {
152- return annotate ( figment:: Error :: from (
153- "Cannot specify both `key` and `key_file`" . to_owned ( ) ,
154- ) ) ;
155- }
156-
157- if key. password . is_some ( ) && key. password_file . is_some ( ) {
158- return annotate ( figment:: Error :: from (
159- "Cannot specify both `password` and `password_file`" . to_owned ( ) ,
160- ) ) ;
161- }
162- }
163-
164- Ok ( ( ) )
165- }
166227}
167228
168229impl SecretsConfig {
@@ -186,9 +247,7 @@ impl SecretsConfig {
186247 let rsa_key = KeyConfig {
187248 kid : Alphanumeric . sample_string ( & mut rng, 10 ) ,
188249 password : None ,
189- password_file : None ,
190- key : Some ( rsa_key. to_pem ( pem_rfc7468:: LineEnding :: LF ) ?. to_string ( ) ) ,
191- key_file : None ,
250+ key : Key :: Value ( rsa_key. to_pem ( pem_rfc7468:: LineEnding :: LF ) ?. to_string ( ) ) ,
192251 } ;
193252
194253 let span = tracing:: info_span!( "ec_p256" ) ;
@@ -204,9 +263,7 @@ impl SecretsConfig {
204263 let ec_p256_key = KeyConfig {
205264 kid : Alphanumeric . sample_string ( & mut rng, 10 ) ,
206265 password : None ,
207- password_file : None ,
208- key : Some ( ec_p256_key. to_pem ( pem_rfc7468:: LineEnding :: LF ) ?. to_string ( ) ) ,
209- key_file : None ,
266+ key : Key :: Value ( ec_p256_key. to_pem ( pem_rfc7468:: LineEnding :: LF ) ?. to_string ( ) ) ,
210267 } ;
211268
212269 let span = tracing:: info_span!( "ec_p384" ) ;
@@ -222,9 +279,7 @@ impl SecretsConfig {
222279 let ec_p384_key = KeyConfig {
223280 kid : Alphanumeric . sample_string ( & mut rng, 10 ) ,
224281 password : None ,
225- password_file : None ,
226- key : Some ( ec_p384_key. to_pem ( pem_rfc7468:: LineEnding :: LF ) ?. to_string ( ) ) ,
227- key_file : None ,
282+ key : Key :: Value ( ec_p384_key. to_pem ( pem_rfc7468:: LineEnding :: LF ) ?. to_string ( ) ) ,
228283 } ;
229284
230285 let span = tracing:: info_span!( "ec_k256" ) ;
@@ -240,9 +295,7 @@ impl SecretsConfig {
240295 let ec_k256_key = KeyConfig {
241296 kid : Alphanumeric . sample_string ( & mut rng, 10 ) ,
242297 password : None ,
243- password_file : None ,
244- key : Some ( ec_k256_key. to_pem ( pem_rfc7468:: LineEnding :: LF ) ?. to_string ( ) ) ,
245- key_file : None ,
298+ key : Key :: Value ( ec_k256_key. to_pem ( pem_rfc7468:: LineEnding :: LF ) ?. to_string ( ) ) ,
246299 } ;
247300
248301 Ok ( Self {
@@ -255,8 +308,7 @@ impl SecretsConfig {
255308 let rsa_key = KeyConfig {
256309 kid : "abcdef" . to_owned ( ) ,
257310 password : None ,
258- password_file : None ,
259- key : Some (
311+ key : Key :: Value (
260312 indoc:: indoc! { r"
261313 -----BEGIN PRIVATE KEY-----
262314 MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN
@@ -271,13 +323,11 @@ impl SecretsConfig {
271323 " }
272324 . to_owned ( ) ,
273325 ) ,
274- key_file : None ,
275326 } ;
276327 let ecdsa_key = KeyConfig {
277328 kid : "ghijkl" . to_owned ( ) ,
278329 password : None ,
279- password_file : None ,
280- key : Some (
330+ key : Key :: Value (
281331 indoc:: indoc! { r"
282332 -----BEGIN PRIVATE KEY-----
283333 MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA
@@ -287,7 +337,6 @@ impl SecretsConfig {
287337 " }
288338 . to_owned ( ) ,
289339 ) ,
290- key_file : None ,
291340 } ;
292341
293342 Self {
0 commit comments