11use std:: collections:: { BTreeMap , HashSet } ;
22
3- use jsonwebtoken:: { DecodingKey , Header , TokenData , Validation , decode, decode_header} ;
3+ use jsonwebtoken:: { Algorithm , DecodingKey , Header , TokenData , Validation , decode, decode_header} ;
44use objectstore_service:: id:: ObjectId ;
55use secrecy:: ExposeSecret ;
66use serde:: { Deserialize , Serialize } ;
@@ -126,11 +126,31 @@ impl AuthContext {
126126 . get ( key_id)
127127 . ok_or_else ( || AuthError :: InternalError ( format ! ( "Key `{key_id}` not configured" ) ) ) ?;
128128
129+ if jwt_header. alg != Algorithm :: EdDSA {
130+ tracing:: warn!(
131+ algorithm = ?jwt_header. alg,
132+ "JWT signed with unexpected algorithm" ,
133+ ) ;
134+ let kind = jsonwebtoken:: errors:: ErrorKind :: InvalidAlgorithm ;
135+ return Err ( AuthError :: ValidationFailure ( kind. into ( ) ) ) ;
136+ }
137+
129138 let mut verified_claims: Option < TokenData < JwtClaims > > = None ;
130139 for key in & key_config. key_versions {
140+ let decoding_key = match DecodingKey :: from_ed_pem ( key. expose_secret ( ) . as_bytes ( ) ) {
141+ Ok ( key) => key,
142+ Err ( error) => {
143+ tracing:: error!(
144+ error = & error as & dyn std:: error:: Error ,
145+ "Failed to construct decoding key from PEM" ,
146+ ) ;
147+ continue ;
148+ }
149+ } ;
150+
131151 let decode_result = decode :: < JwtClaims > (
132152 encoded_token,
133- & DecodingKey :: from_secret ( key . expose_secret ( ) . as_bytes ( ) ) ,
153+ & decoding_key ,
134154 & jwt_validation_params ( & jwt_header) ,
135155 ) ;
136156
@@ -210,7 +230,16 @@ mod tests {
210230 use serde_json:: json;
211231
212232 const TEST_SIGNING_KID : & str = "test-key" ;
213- const TEST_SIGNING_SECRET : & str = "fa24f0a3ab08f9ff0d4b2183595a045c" ;
233+ // Private key generated with `openssl genpkey -algorithm Ed25519`
234+ const TEST_PRIVATE_KEY : & str = r#"-----BEGIN PRIVATE KEY-----
235+ MC4CAQAwBQYDK2VwBCIEIAZtPzCHjltjZSi3+THxP6Rh8vUM0LRNA/QDR8zJx0tB
236+ -----END PRIVATE KEY-----
237+ "# ;
238+ // Public key extracted with `openssl pkey -in private_key.pem -pubout`
239+ const TEST_PUBLIC_KEY : & str = r#"-----BEGIN PUBLIC KEY-----
240+ MCowBQYDK2VwAyEA/TOsO19FvHFTsZqcYiO8HGfm02Df5oWBXgzulxYPvSs=
241+ -----END PUBLIC KEY-----
242+ "# ;
214243
215244 #[ derive( Serialize , Deserialize ) ]
216245 struct TestJwtClaims {
@@ -228,7 +257,7 @@ mod tests {
228257 }
229258
230259 fn test_config ( max_permissions : HashSet < Permission > ) -> AuthZ {
231- let wrapped_key = SecretBox :: new ( Box :: new ( ConfigSecret :: from ( TEST_SIGNING_SECRET ) ) ) ;
260+ let wrapped_key = SecretBox :: new ( Box :: new ( ConfigSecret :: from ( TEST_PUBLIC_KEY ) ) ) ;
232261 let key_config = AuthZVerificationKey {
233262 key_versions : vec ! [ wrapped_key] ,
234263 max_permissions,
@@ -239,24 +268,20 @@ mod tests {
239268 }
240269 }
241270
242- fn sign_token ( claims : & JwtClaims , signing_secret : & str ) -> String {
271+ fn sign_token ( claims : & JwtClaims , signing_secret : & str , exp : Option < u64 > ) -> String {
243272 use jsonwebtoken:: { Algorithm , EncodingKey , Header , encode, get_current_timestamp} ;
244273
245- let mut header = Header :: new ( Algorithm :: HS256 ) ;
274+ let mut header = Header :: new ( Algorithm :: EdDSA ) ;
246275 header. kid = Some ( TEST_SIGNING_KID . into ( ) ) ;
247276 header. typ = Some ( "JWT" . into ( ) ) ;
248277
249278 let claims = TestJwtClaims {
250- exp : get_current_timestamp ( ) + 300 ,
279+ exp : exp . unwrap_or_else ( || get_current_timestamp ( ) + 300 ) ,
251280 claims : claims. clone ( ) ,
252281 } ;
253282
254- encode (
255- & header,
256- & claims,
257- & EncodingKey :: from_secret ( signing_secret. as_bytes ( ) ) ,
258- )
259- . unwrap ( )
283+ let key = EncodingKey :: from_ed_pem ( signing_secret. as_bytes ( ) ) . unwrap ( ) ;
284+ encode ( & header, & claims, & key) . unwrap ( )
260285 }
261286
262287 fn sample_claims (
@@ -289,7 +314,7 @@ mod tests {
289314 fn test_from_encoded_jwt_basic ( ) -> Result < ( ) , AuthError > {
290315 // Create a token with max permissions
291316 let claims = sample_claims ( "123" , "456" , "attachments" , max_permission ( ) ) ;
292- let encoded_token = sign_token ( & claims, TEST_SIGNING_SECRET ) ;
317+ let encoded_token = sign_token ( & claims, TEST_PRIVATE_KEY , None ) ;
293318
294319 // Create test config with max permissions
295320 let test_config = test_config ( max_permission ( ) ) ;
@@ -307,7 +332,7 @@ mod tests {
307332 fn test_from_encoded_jwt_max_permissions_limit ( ) -> Result < ( ) , AuthError > {
308333 // Create a token with max permissions
309334 let claims = sample_claims ( "123" , "456" , "attachments" , max_permission ( ) ) ;
310- let encoded_token = sign_token ( & claims, TEST_SIGNING_SECRET ) ;
335+ let encoded_token = sign_token ( & claims, TEST_PRIVATE_KEY , None ) ;
311336
312337 // Assign read-only permissions to the signing key in config
313338 let ro_permission = HashSet :: from ( [ Permission :: ObjectRead ] ) ;
@@ -339,9 +364,12 @@ mod tests {
339364
340365 #[ test]
341366 fn test_from_encoded_jwt_unknown_key_fails ( ) -> Result < ( ) , AuthError > {
342- // Create a token with max permissions
343367 let claims = sample_claims ( "123" , "456" , "attachments" , max_permission ( ) ) ;
344- let encoded_token = sign_token ( & claims, "unknown signing key" ) ;
368+ let unknown_key = r#"-----BEGIN PRIVATE KEY-----
369+ MC4CAQAwBQYDK2VwBCIEIKwVoE4TmTfWoqH3HgLVsEcHs9PHNe+ar/Hp6e4To8pK
370+ -----END PRIVATE KEY-----
371+ "# ;
372+ let encoded_token = sign_token ( & claims, unknown_key, None ) ;
345373
346374 // Create test config with max permissions
347375 let test_config = test_config ( max_permission ( ) ) ;
@@ -356,25 +384,12 @@ mod tests {
356384
357385 #[ test]
358386 fn test_from_encoded_jwt_expired ( ) -> Result < ( ) , AuthError > {
359- use jsonwebtoken:: { Algorithm , EncodingKey , Header , encode, get_current_timestamp} ;
360-
361387 let claims = sample_claims ( "123" , "456" , "attachments" , max_permission ( ) ) ;
362-
363- let mut header = Header :: new ( Algorithm :: HS256 ) ;
364- header. kid = Some ( TEST_SIGNING_KID . into ( ) ) ;
365- header. typ = Some ( "JWT" . into ( ) ) ;
366-
367- let claims = TestJwtClaims {
368- exp : get_current_timestamp ( ) - 100 , // NB: expired
369- claims : claims. clone ( ) ,
370- } ;
371-
372- let encoded_token = encode (
373- & header,
388+ let encoded_token = sign_token (
374389 & claims,
375- & EncodingKey :: from_secret ( TEST_SIGNING_SECRET . as_bytes ( ) ) ,
376- )
377- . unwrap ( ) ;
390+ TEST_PRIVATE_KEY ,
391+ Some ( jsonwebtoken :: get_current_timestamp ( ) - 100 ) ,
392+ ) ;
378393
379394 // Create test config with max permissions
380395 let test_config = test_config ( max_permission ( ) ) ;
0 commit comments