Skip to content

Commit 44cf890

Browse files
fix(auth): Require EdDSA and fix verification (#232)
There are many different functions you are to use for different types of keys. We are planning to require EdDSA, so we validate the algorithm and switch to using `from_ed_pem()`. --------- Co-authored-by: Jan Michael Auer <mail@jauer.org>
1 parent 268b7ae commit 44cf890

File tree

2 files changed

+53
-35
lines changed

2 files changed

+53
-35
lines changed

objectstore-server/src/auth/context.rs

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use 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};
44
use objectstore_service::id::ObjectId;
55
use secrecy::ExposeSecret;
66
use 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());

objectstore-server/src/auth/service.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ impl FromRequestParts<ServiceState> for AuthAwareService {
9494

9595
let context = AuthContext::from_encoded_jwt(encoded_token, &state.config.auth);
9696
if context.is_err() && state.config.auth.enforce {
97-
tracing::debug!("Authorization failed when enforcement is enabled");
97+
tracing::debug!(
98+
"Authorization failed and enforcement is enabled: `{:?}`",
99+
context
100+
);
98101
return Err(StatusCode::UNAUTHORIZED);
99102
}
100103

0 commit comments

Comments
 (0)