Skip to content

Commit 1c82ff8

Browse files
author
Jakub Konka
committed
Address review comments
1 parent 593d130 commit 1c82ff8

File tree

12 files changed

+171
-136
lines changed

12 files changed

+171
-136
lines changed

Cargo.lock

Lines changed: 26 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/notary/client/src/client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ pub struct NotaryClient {
138138
/// in notary server.
139139
#[builder(setter(into, strip_option), default)]
140140
api_key: Option<String>,
141-
/// JWT token used to callnotary server endpoints if JWT authorization is
141+
/// JWT token used to call notary server endpoints if JWT authorization is
142142
/// enabled in notary server.
143143
#[builder(setter(into, strip_option), default)]
144144
jwt: Option<String>,

crates/notary/server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ serde_json = { workspace = true }
4848
serde_yaml = { version = "0.9" }
4949
sha1 = { version = "0.10" }
5050
structopt = { version = "0.3" }
51+
strum = { version = "0.27", features = ["derive"] }
5152
thiserror = { workspace = true }
5253
tokio = { workspace = true, features = ["full"] }
5354
tokio-rustls = { workspace = true }

crates/notary/server/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ An optional authorization module is available to only allow requests with a vali
213213
Please note that only *one* mode can be active at any one time.
214214

215215
##### Whitelist mode
216-
In whitelist mode, an API key is attached in the custom HTTP header `X-API-Key`. The API key whitelist path (as well as the flag to enable/disable this module) can be changed in the config (`authorization` field).
216+
In whitelist mode, a valid API key needs to be attached in the custom HTTP header `X-API-Key`. The path of the API key whitelist, path (as well as the flag to enable/disable this module), can be changed in the config (`authorization` field).
217217

218218
Hot reloading of the whitelist is supported, i.e. modification of the whitelist file will be automatically applied without needing to restart the server. Please take note of the following
219219
- Avoid using auto save mode when editing the whitelist to prevent spamming hot reloads
@@ -223,6 +223,10 @@ Hot reloading of the whitelist is supported, i.e. modification of the whitelist
223223
In JWT mode, JSON Web Token is attached in the standard `Authorization` HTTP header as a bearer token. The path to decoding key as well as custom user claims can be changed in the
224224
config (`authorization` field).
225225

226+
Care should be taken when defining custom user claims as the middleware will:
227+
- accept any claim if no custom claim is defined,
228+
- as long as user defined claims are found, other unknown claims will be ignored.
229+
226230
An example JWT config may look something like this:
227231

228232
```yaml

crates/notary/server/src/auth.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,31 @@ pub(crate) mod whitelist;
44
use eyre::{eyre, Result};
55
use jwt::load_jwt_key;
66
use std::{
7-
collections::HashMap,
7+
str::FromStr,
88
sync::{Arc, Mutex},
99
};
1010
use tracing::debug;
1111
use whitelist::load_authorization_whitelist;
1212

13-
pub use jwt::{Jwt, JwtError};
14-
pub use whitelist::{watch_and_reload_authorization_whitelist, AuthorizationWhitelistRecord};
13+
pub use jwt::{Algorithm, Jwt, JwtValidationError};
14+
pub use whitelist::{
15+
watch_and_reload_authorization_whitelist, AuthorizationWhitelistRecord, Whitelist,
16+
};
1517

1618
use crate::{AuthorizationModeProperties, NotaryServerProperties};
1719

1820
/// Supported authorization modes.
1921
#[derive(Clone)]
2022
pub enum AuthorizationMode {
2123
Jwt(Jwt),
22-
Whitelist(Arc<Mutex<HashMap<String, AuthorizationWhitelistRecord>>>),
24+
Whitelist(Whitelist),
2325
}
2426

2527
impl AuthorizationMode {
26-
pub fn as_whitelist(
27-
&self,
28-
) -> Option<Arc<Mutex<HashMap<String, AuthorizationWhitelistRecord>>>> {
28+
pub fn as_whitelist(&self) -> Option<&Whitelist> {
2929
match self {
3030
Self::Jwt(..) => None,
31-
Self::Whitelist(whitelist) => Some(whitelist.clone()),
31+
Self::Whitelist(whitelist) => Some(whitelist),
3232
}
3333
}
3434
}
@@ -44,11 +44,16 @@ pub async fn load_authorization_mode(
4444

4545
let auth_mode = match config.auth.mode.as_ref().ok_or_else(|| {
4646
eyre!(
47-
"Authorization enabled but neither whitelist nor jwt properties provided in the config"
47+
"Authorization enabled but failed to load either whitelist or jwt properties. They are either absent or malformed."
4848
)
4949
})? {
5050
AuthorizationModeProperties::Jwt(jwt_opts) => {
51-
let algorithm = jwt_opts.algorithm;
51+
let algorithm = Algorithm::from_str(&jwt_opts.algorithm).map_err(|_| {
52+
eyre!(
53+
"Unexpected JWT signing algorithm specified: '{}'",
54+
jwt_opts.algorithm
55+
)
56+
})?;
5257
let claims = jwt_opts.claims.clone();
5358
let key = load_jwt_key(&jwt_opts.public_key_path, algorithm).await?;
5459
AuthorizationMode::Jwt(Jwt {
@@ -58,8 +63,11 @@ pub async fn load_authorization_mode(
5863
})
5964
}
6065
AuthorizationModeProperties::Whitelist(whitelist_csv_path) => {
61-
let whitelist = load_authorization_whitelist(whitelist_csv_path)?;
62-
AuthorizationMode::Whitelist(Arc::new(Mutex::new(whitelist)))
66+
let entries = load_authorization_whitelist(whitelist_csv_path)?;
67+
AuthorizationMode::Whitelist(Whitelist {
68+
entries: Arc::new(Mutex::new(entries)),
69+
csv_path: whitelist_csv_path.clone(),
70+
})
6371
}
6472
};
6573

crates/notary/server/src/auth/jwt.rs

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
use eyre::Result;
2-
use jsonwebtoken::{Algorithm, DecodingKey};
2+
use jsonwebtoken::{Algorithm as JwtAlgorithm, DecodingKey};
33
use serde_json::Value;
4-
use std::io::Read;
4+
use strum::EnumString;
55
use tracing::error;
66

7-
use crate::{read_pem_file, JwtClaim, JwtClaimValueType};
7+
use crate::{JwtClaim, JwtClaimValueType};
88

99
/// Custom error for JWT handling
1010
#[derive(Debug, thiserror::Error, PartialEq)]
11-
pub enum JwtError {
12-
#[error("unsupported algorithm: {0:?}")]
13-
UnsupportedAlgorithm(Algorithm),
14-
#[error("JWT validation error: {0}")]
15-
Validation(String),
16-
}
11+
#[error("JWT validation error: {0}")]
12+
pub struct JwtValidationError(String);
1713

18-
type JwtResult<T> = std::result::Result<T, JwtError>;
14+
type JwtResult<T> = std::result::Result<T, JwtValidationError>;
1915

2016
/// JWT config which also encapsulates claims validation logic.
2117
#[derive(Clone)]
@@ -26,32 +22,33 @@ pub struct Jwt {
2622
}
2723

2824
impl Jwt {
29-
pub fn validate(&self, claims: Value) -> JwtResult<()> {
25+
pub fn validate(&self, claims: &Value) -> JwtResult<()> {
3026
Jwt::validate_claims(&self.claims, claims)
3127
}
3228

33-
fn validate_claims(expected: &[JwtClaim], claims: Value) -> JwtResult<()> {
29+
fn validate_claims(expected: &[JwtClaim], claims: &Value) -> JwtResult<()> {
3430
expected
3531
.iter()
36-
.try_for_each(|expected| Self::validate_claim(expected, claims.clone()))
32+
.try_for_each(|expected| Self::validate_claim(expected, claims))
3733
}
3834

39-
fn validate_claim(expected: &JwtClaim, given: Value) -> JwtResult<()> {
40-
let field = Jwt::get_field(&expected.name, &given).ok_or(JwtError::Validation(format!(
35+
fn validate_claim(expected: &JwtClaim, given: &Value) -> JwtResult<()> {
36+
let pointer = format!("/{}", expected.name.replace(".", "/"));
37+
let field = given.pointer(&pointer).ok_or(JwtValidationError(format!(
4138
"missing claim '{}'",
4239
expected.name
4340
)))?;
4441

4542
match expected.value_type {
4643
JwtClaimValueType::String => {
47-
let field_typed = field.as_str().ok_or(JwtError::Validation(format!(
44+
let field_typed = field.as_str().ok_or(JwtValidationError(format!(
4845
"unexpected type for claim '{}': expected '{:?}'",
4946
expected.name, expected.value_type
5047
)))?;
5148
if !expected.values.is_empty() {
5249
expected.values.iter().any(|exp| exp == field_typed).then_some(()).ok_or_else(|| {
5350
let expected_values = expected.values.iter().map(|x| format!("'{x}'")).collect::<Vec<String>>().join(", ");
54-
JwtError::Validation(format!(
51+
JwtValidationError(format!(
5552
"unexpected value for claim '{}': expected one of [ {expected_values} ], received '{field_typed}'", expected.name
5653
))
5754
})?;
@@ -61,19 +58,44 @@ impl Jwt {
6158

6259
Ok(())
6360
}
61+
}
6462

65-
fn get_field<'a>(path: &'a str, value: &'a Value) -> Option<&'a Value> {
66-
let (field, path) = match path.split_once('.') {
67-
Some((field, path)) => (field, Some(path)),
68-
None => (path, None),
69-
};
70-
if let Some(value) = value.get(field) {
71-
match path {
72-
Some(path) => Jwt::get_field(path, value),
73-
None => Some(value),
74-
}
75-
} else {
76-
None
63+
#[derive(EnumString, Debug, Clone, Copy, PartialEq, Eq)]
64+
#[strum(ascii_case_insensitive)]
65+
/// Supported JWT signing algorithms
66+
pub enum Algorithm {
67+
/// RSASSA-PSS using SHA-512
68+
RS256,
69+
/// RSASSA-PKCS1-v1_5 using SHA-384
70+
RS384,
71+
/// RSASSA-PKCS1-v1_5 using SHA-384
72+
RS512,
73+
/// RSASSA-PSS using SHA-256
74+
PS256,
75+
/// RSASSA-PSS using SHA-384
76+
PS384,
77+
/// RSASSA-PSS using SHA-512
78+
PS512,
79+
/// ECDSA using SHA-256
80+
ES256,
81+
/// ECDSA using SHA-384
82+
ES384,
83+
/// Edwards-curve Digital Signature Algorithm (EdDSA)
84+
EdDSA,
85+
}
86+
87+
impl From<Algorithm> for JwtAlgorithm {
88+
fn from(value: Algorithm) -> Self {
89+
match value {
90+
Algorithm::RS256 => Self::RS256,
91+
Algorithm::RS384 => Self::RS384,
92+
Algorithm::RS512 => Self::RS512,
93+
Algorithm::PS256 => Self::PS256,
94+
Algorithm::PS384 => Self::PS384,
95+
Algorithm::PS512 => Self::PS512,
96+
Algorithm::ES256 => Self::ES256,
97+
Algorithm::ES384 => Self::ES384,
98+
Algorithm::EdDSA => Self::EdDSA,
7799
}
78100
}
79101
}
@@ -83,9 +105,7 @@ pub(super) async fn load_jwt_key(
83105
public_key_pem_path: &str,
84106
algorithm: Algorithm,
85107
) -> Result<DecodingKey> {
86-
let mut reader = read_pem_file(public_key_pem_path).await?;
87-
let mut key_pem_bytes: Vec<u8> = Vec::new();
88-
reader.read_to_end(&mut key_pem_bytes)?;
108+
let key_pem_bytes = tokio::fs::read(public_key_pem_path).await?;
89109
let key = match algorithm {
90110
Algorithm::RS256
91111
| Algorithm::RS384
@@ -95,7 +115,6 @@ pub(super) async fn load_jwt_key(
95115
| Algorithm::PS512 => DecodingKey::from_rsa_pem(&key_pem_bytes)?,
96116
Algorithm::ES256 | Algorithm::ES384 => DecodingKey::from_ec_pem(&key_pem_bytes)?,
97117
Algorithm::EdDSA => DecodingKey::from_ed_pem(&key_pem_bytes)?,
98-
_ => return Err(JwtError::UnsupportedAlgorithm(algorithm).into()),
99118
};
100119
Ok(key)
101120
}
@@ -116,7 +135,7 @@ mod test {
116135
"exp": 12345,
117136
"sub": "test",
118137
});
119-
assert!(Jwt::validate_claim(&expected, given).is_ok());
138+
assert!(Jwt::validate_claim(&expected, &given).is_ok());
120139
}
121140

122141
#[test]
@@ -132,7 +151,7 @@ mod test {
132151
"host": "api.tlsn.com",
133152
},
134153
});
135-
assert!(Jwt::validate_claim(&expected, given).is_ok())
154+
assert!(Jwt::validate_claim(&expected, &given).is_ok())
136155
}
137156

138157
#[test]
@@ -142,7 +161,7 @@ mod test {
142161
"sub": "test",
143162
"what": "is_this",
144163
});
145-
assert!(Jwt::validate_claims(&[], given).is_ok())
164+
assert!(Jwt::validate_claims(&[], &given).is_ok())
146165
}
147166

148167
#[test]
@@ -156,8 +175,8 @@ mod test {
156175
"host": "localhost",
157176
});
158177
assert_eq!(
159-
Jwt::validate_claim(&expected, given),
160-
Err(JwtError::Validation("missing claim 'sub'".to_string()))
178+
Jwt::validate_claim(&expected, &given),
179+
Err(JwtValidationError("missing claim 'sub'".to_string()))
161180
)
162181
}
163182

@@ -172,8 +191,8 @@ mod test {
172191
"sub": "tlsn",
173192
});
174193
assert_eq!(
175-
Jwt::validate_claim(&expected, given),
176-
Err(JwtError::Validation("unexpected value for claim 'sub': expected one of [ 'tlsn_prod', 'tlsn_test' ], received 'tlsn'".to_string()))
194+
Jwt::validate_claim(&expected, &given),
195+
Err(JwtValidationError("unexpected value for claim 'sub': expected one of [ 'tlsn_prod', 'tlsn_test' ], received 'tlsn'".to_string()))
177196
)
178197
}
179198
}

0 commit comments

Comments
 (0)