@@ -8,6 +8,7 @@ use std::borrow::Cow;
8
8
9
9
use anyhow:: { Context , bail} ;
10
10
use camino:: Utf8PathBuf ;
11
+ use futures_util:: future:: { try_join, try_join_all} ;
11
12
use mas_jose:: jwk:: { JsonWebKey , JsonWebKeySet } ;
12
13
use mas_keystore:: { Encrypter , Keystore , PrivateKey } ;
13
14
use rand:: {
@@ -27,23 +28,160 @@ fn example_secret() -> &'static str {
27
28
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
28
29
}
29
30
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
+ }
33
40
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 > ,
34
47
#[ serde( skip_serializing_if = "Option::is_none" ) ]
35
48
password : Option < String > ,
49
+ }
36
50
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 ;
40
53
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
+ }
43
63
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 {
45
96
#[ schemars( with = "Option<String>" ) ]
97
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
46
98
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
+ }
47
185
}
48
186
49
187
/// Application secrets
@@ -72,49 +210,9 @@ impl SecretsConfig {
72
210
/// Returns an error when a key could not be imported
73
211
#[ tracing:: instrument( name = "secrets.load" , skip_all) ]
74
212
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 ?;
115
214
116
- let keys = JsonWebKeySet :: new ( keys) ;
117
- Ok ( Keystore :: new ( keys) )
215
+ Ok ( Keystore :: new ( JsonWebKeySet :: new ( web_keys) ) )
118
216
}
119
217
120
218
/// Derive an [`Encrypter`] out of the config
@@ -126,43 +224,6 @@ impl SecretsConfig {
126
224
127
225
impl ConfigurationSection for SecretsConfig {
128
226
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
- }
166
227
}
167
228
168
229
impl SecretsConfig {
@@ -186,9 +247,7 @@ impl SecretsConfig {
186
247
let rsa_key = KeyConfig {
187
248
kid : Alphanumeric . sample_string ( & mut rng, 10 ) ,
188
249
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 ( ) ) ,
192
251
} ;
193
252
194
253
let span = tracing:: info_span!( "ec_p256" ) ;
@@ -204,9 +263,7 @@ impl SecretsConfig {
204
263
let ec_p256_key = KeyConfig {
205
264
kid : Alphanumeric . sample_string ( & mut rng, 10 ) ,
206
265
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 ( ) ) ,
210
267
} ;
211
268
212
269
let span = tracing:: info_span!( "ec_p384" ) ;
@@ -222,9 +279,7 @@ impl SecretsConfig {
222
279
let ec_p384_key = KeyConfig {
223
280
kid : Alphanumeric . sample_string ( & mut rng, 10 ) ,
224
281
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 ( ) ) ,
228
283
} ;
229
284
230
285
let span = tracing:: info_span!( "ec_k256" ) ;
@@ -240,9 +295,7 @@ impl SecretsConfig {
240
295
let ec_k256_key = KeyConfig {
241
296
kid : Alphanumeric . sample_string ( & mut rng, 10 ) ,
242
297
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 ( ) ) ,
246
299
} ;
247
300
248
301
Ok ( Self {
@@ -255,8 +308,7 @@ impl SecretsConfig {
255
308
let rsa_key = KeyConfig {
256
309
kid : "abcdef" . to_owned ( ) ,
257
310
password : None ,
258
- password_file : None ,
259
- key : Some (
311
+ key : Key :: Value (
260
312
indoc:: indoc! { r"
261
313
-----BEGIN PRIVATE KEY-----
262
314
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN
@@ -271,13 +323,11 @@ impl SecretsConfig {
271
323
" }
272
324
. to_owned ( ) ,
273
325
) ,
274
- key_file : None ,
275
326
} ;
276
327
let ecdsa_key = KeyConfig {
277
328
kid : "ghijkl" . to_owned ( ) ,
278
329
password : None ,
279
- password_file : None ,
280
- key : Some (
330
+ key : Key :: Value (
281
331
indoc:: indoc! { r"
282
332
-----BEGIN PRIVATE KEY-----
283
333
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA
@@ -287,7 +337,6 @@ impl SecretsConfig {
287
337
" }
288
338
. to_owned ( ) ,
289
339
) ,
290
- key_file : None ,
291
340
} ;
292
341
293
342
Self {
0 commit comments