mirror of
https://github.com/tlsnotary/tlsn.git
synced 2026-01-08 21:08:04 -05:00
feat(notary): add JWT-based authorization mode (#817)
* feat(server): add JWT-based authorization mode This mode is an alternative to whitelist authorization mode. It extracts the JWT from the authorization header (bearer token), validates token's signature, claimed expiry times and additional (user-configurable) claims. * Fix formatting and lints * Address review comments * feat(server): remove JwtClaimType config property * Fix missing README comments * Address review comments * Address review comments --------- Co-authored-by: yuroitaki <25913766+yuroitaki@users.noreply.github.com>
This commit is contained in:
69
Cargo.lock
generated
69
Cargo.lock
generated
@@ -2255,8 +2255,8 @@ dependencies = [
|
||||
"proc-macro-rules",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"strum 0.25.0",
|
||||
"strum_macros 0.25.3",
|
||||
"syn 2.0.101",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
@@ -3828,6 +3828,21 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"js-sys",
|
||||
"pem",
|
||||
"ring 0.17.14",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.13.4"
|
||||
@@ -4697,6 +4712,7 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"jsonwebtoken",
|
||||
"k256",
|
||||
"mc-sgx-dcap-types",
|
||||
"notary-common",
|
||||
@@ -4708,9 +4724,11 @@ dependencies = [
|
||||
"rustls 0.21.12",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha1",
|
||||
"structopt",
|
||||
"strum 0.27.1",
|
||||
"thiserror 1.0.69",
|
||||
"tlsn-common",
|
||||
"tlsn-core",
|
||||
@@ -4739,6 +4757,7 @@ dependencies = [
|
||||
"hyper 1.6.0",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"jsonwebtoken",
|
||||
"notary-client",
|
||||
"notary-common",
|
||||
"notary-server",
|
||||
@@ -5043,6 +5062,16 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -6528,6 +6557,18 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplecss"
|
||||
version = "0.2.2"
|
||||
@@ -6702,7 +6743,16 @@ version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
"strum_macros 0.25.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
dependencies = [
|
||||
"strum_macros 0.27.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6718,6 +6768,19 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
||||
@@ -7,6 +7,7 @@ use http_body_util::{BodyExt as _, Either, Empty, Full};
|
||||
use hyper::{
|
||||
body::{Bytes, Incoming},
|
||||
client::conn::http1::Parts,
|
||||
header::AUTHORIZATION,
|
||||
Request, Response, StatusCode,
|
||||
};
|
||||
use hyper_util::rt::TokioIo;
|
||||
@@ -137,6 +138,10 @@ pub struct NotaryClient {
|
||||
/// in notary server.
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
api_key: Option<String>,
|
||||
/// JWT token used to call notary server endpoints if JWT authorization is
|
||||
/// enabled in notary server.
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
jwt: Option<String>,
|
||||
/// The duration of notarization request timeout in seconds.
|
||||
#[builder(default = "60")]
|
||||
request_timeout: usize,
|
||||
@@ -291,6 +296,11 @@ impl NotaryClient {
|
||||
configuration_request_builder.header(X_API_KEY_HEADER, api_key);
|
||||
}
|
||||
|
||||
if let Some(jwt) = &self.jwt {
|
||||
configuration_request_builder =
|
||||
configuration_request_builder.header(AUTHORIZATION, format!("Bearer {jwt}"));
|
||||
}
|
||||
|
||||
let configuration_request = configuration_request_builder
|
||||
.body(Either::Left(Full::new(Bytes::from(
|
||||
configuration_request_payload,
|
||||
|
||||
@@ -32,6 +32,7 @@ http = { workspace = true }
|
||||
http-body-util = { workspace = true }
|
||||
hyper = { workspace = true, features = ["client", "http1", "server"] }
|
||||
hyper-util = { workspace = true, features = ["full"] }
|
||||
jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
|
||||
k256 = { workspace = true }
|
||||
notify = { version = "6.1.1", default-features = false, features = [
|
||||
"macos_kqueue",
|
||||
@@ -43,9 +44,11 @@ rand06-compat = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
rustls-pemfile = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { version = "0.9" }
|
||||
sha1 = { version = "0.10" }
|
||||
structopt = { version = "0.3" }
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-rustls = { workspace = true }
|
||||
|
||||
@@ -90,7 +90,7 @@ log:
|
||||
|
||||
auth:
|
||||
enabled: false
|
||||
whitelist_path: null
|
||||
whitelist: null
|
||||
```
|
||||
⚠️ By default, `notarization.private_key_path` is `null`, which means a **random, ephemeral** signing key will be generated at runtime (see [Signing](#signing) for more details).
|
||||
|
||||
@@ -168,10 +168,35 @@ TLS needs to be turned on between the prover and the notary for security purpose
|
||||
The toggle to turn on TLS, as well as paths to the TLS private key and certificate can be defined in the config (`tls` field).
|
||||
|
||||
### Authorization
|
||||
An optional authorization module is available to only allow requests with a valid API key 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).
|
||||
An optional authorization module is available to only allow requests with a valid credential attached. Currently, two modes are supported: whitelist and JWT.
|
||||
|
||||
Please note that only *one* mode can be active at any one time.
|
||||
|
||||
#### Whitelist mode
|
||||
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, as well as the flag to enable/disable this module, can be changed in the config (`auth` field).
|
||||
|
||||
Hot reloading of the whitelist is supported, i.e. changes to the whitelist file are automatically applied without needing to restart the server.
|
||||
|
||||
#### JWT mode
|
||||
In JWT mode, JSON Web Token is attached in the standard `Authorization` HTTP header as a bearer token. The algorithm, the path to verifying key, as well as custom user claims, can be changed in the config (`auth` field).
|
||||
|
||||
Care should be taken when defining custom user claims as the middleware will:
|
||||
- accept any claim if no custom claim is defined,
|
||||
- as long as user defined claims are found, other unknown claims will be ignored.
|
||||
|
||||
An example JWT config may look something like this:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
enabled: true
|
||||
jwt:
|
||||
algorithm: "RS256"
|
||||
public_key_path: "./fixture/auth/jwt.key.pub"
|
||||
claims:
|
||||
- name: sub
|
||||
values: ["tlsnotary"]
|
||||
```
|
||||
|
||||
### Logging
|
||||
The default logging strategy of this server is set to `DEBUG` verbosity level for the crates that are useful for most debugging scenarios, i.e. using the following filtering logic.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ paths:
|
||||
security:
|
||||
- {} # make security optional
|
||||
- ApiKeyAuth: []
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Ok response from server
|
||||
@@ -38,6 +39,7 @@ paths:
|
||||
security:
|
||||
- {} # make security optional
|
||||
- ApiKeyAuth: []
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Info response from server
|
||||
@@ -60,6 +62,7 @@ paths:
|
||||
security:
|
||||
- {} # make security optional
|
||||
- ApiKeyAuth: []
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- in: header
|
||||
name: Content-Type
|
||||
@@ -212,4 +215,9 @@ components:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
description: Whitelisted API key if auth module is turned on
|
||||
description: Whitelisted API key if auth module is turned on and in whitelist mode
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: JSON Web Token if auth module is turned on and in JWT mode
|
||||
|
||||
@@ -1,189 +1,81 @@
|
||||
pub(crate) mod jwt;
|
||||
pub(crate) mod whitelist;
|
||||
|
||||
use eyre::{eyre, Result};
|
||||
use notify::{
|
||||
event::ModifyKind, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use jwt::load_jwt_key;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
use strum::VariantNames;
|
||||
use tracing::debug;
|
||||
use whitelist::load_authorization_whitelist;
|
||||
|
||||
use crate::{util::parse_csv_file, NotaryServerProperties};
|
||||
pub use jwt::{Algorithm, Jwt};
|
||||
pub use whitelist::{
|
||||
watch_and_reload_authorization_whitelist, AuthorizationWhitelistRecord, Whitelist,
|
||||
};
|
||||
|
||||
/// Structure of each whitelisted record of the API key whitelist for
|
||||
/// authorization purpose
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct AuthorizationWhitelistRecord {
|
||||
pub name: String,
|
||||
pub api_key: String,
|
||||
pub created_at: String,
|
||||
use crate::{AuthorizationModeProperties, NotaryServerProperties};
|
||||
|
||||
/// Supported authorization modes.
|
||||
#[derive(Clone)]
|
||||
pub enum AuthorizationMode {
|
||||
Jwt(Jwt),
|
||||
Whitelist(Whitelist),
|
||||
}
|
||||
|
||||
/// Convert whitelist data structure from vector to hashmap using api_key as the
|
||||
/// key to speed up lookup
|
||||
pub fn authorization_whitelist_vec_into_hashmap(
|
||||
authorization_whitelist: Vec<AuthorizationWhitelistRecord>,
|
||||
) -> HashMap<String, AuthorizationWhitelistRecord> {
|
||||
let mut hashmap = HashMap::new();
|
||||
authorization_whitelist.iter().for_each(|record| {
|
||||
hashmap.insert(record.api_key.clone(), record.to_owned());
|
||||
});
|
||||
hashmap
|
||||
}
|
||||
|
||||
/// Load authorization whitelist if it is enabled
|
||||
pub fn load_authorization_whitelist(
|
||||
config: &NotaryServerProperties,
|
||||
) -> Result<Option<HashMap<String, AuthorizationWhitelistRecord>>> {
|
||||
let authorization_whitelist = if !config.auth.enabled {
|
||||
debug!("Skipping authorization as it is turned off.");
|
||||
None
|
||||
} else {
|
||||
// Check if whitelist_csv_path is Some and convert to &str
|
||||
let whitelist_csv_path = config.auth.whitelist_path.as_deref().ok_or_else(|| {
|
||||
eyre!("Authorization whitelist csv path is not provided in the config")
|
||||
})?;
|
||||
// Load the csv
|
||||
let whitelist_csv = parse_csv_file::<AuthorizationWhitelistRecord>(whitelist_csv_path)
|
||||
.map_err(|err| eyre!("Failed to parse authorization whitelist csv: {:?}", err))?;
|
||||
// Convert the whitelist record into hashmap for faster lookup
|
||||
let whitelist_hashmap = authorization_whitelist_vec_into_hashmap(whitelist_csv);
|
||||
Some(whitelist_hashmap)
|
||||
};
|
||||
Ok(authorization_whitelist)
|
||||
}
|
||||
|
||||
// Setup a watcher to detect any changes to authorization whitelist
|
||||
// When the list file is modified, the watcher thread will reload the whitelist
|
||||
// The watcher is setup in a separate thread by the notify library which is
|
||||
// synchronous
|
||||
pub fn watch_and_reload_authorization_whitelist(
|
||||
config: NotaryServerProperties,
|
||||
authorization_whitelist: Option<Arc<Mutex<HashMap<String, AuthorizationWhitelistRecord>>>>,
|
||||
) -> Result<Option<RecommendedWatcher>> {
|
||||
// Only setup the watcher if auth whitelist is loaded
|
||||
let watcher = if let Some(authorization_whitelist) = authorization_whitelist {
|
||||
let cloned_config = config.clone();
|
||||
// Setup watcher by giving it a function that will be triggered when an event is
|
||||
// detected
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |event: Result<Event, Error>| {
|
||||
match event {
|
||||
Ok(event) => {
|
||||
// Only reload whitelist if it's an event that modified the file data
|
||||
if let EventKind::Modify(ModifyKind::Data(_)) = event.kind {
|
||||
debug!("Authorization whitelist is modified");
|
||||
match load_authorization_whitelist(&cloned_config) {
|
||||
Ok(Some(new_authorization_whitelist)) => {
|
||||
*authorization_whitelist.lock().unwrap() = new_authorization_whitelist;
|
||||
info!("Successfully reloaded authorization whitelist!");
|
||||
}
|
||||
Ok(None) => unreachable!(
|
||||
"Authorization whitelist will never be None as the auth module is enabled"
|
||||
),
|
||||
// Ensure that error from reloading doesn't bring the server down
|
||||
Err(err) => error!("{err}"),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error occured when watcher detected an event: {err}")
|
||||
}
|
||||
}
|
||||
},
|
||||
notify::Config::default(),
|
||||
)
|
||||
.map_err(|err| eyre!("Error occured when setting up watcher for hot reload: {err}"))?;
|
||||
|
||||
// Check if whitelist_csv_path is Some and convert to &str
|
||||
let whitelist_csv_path = config.auth.whitelist_path.as_deref().ok_or_else(|| {
|
||||
eyre!("Authorization whitelist csv path is not provided in the config")
|
||||
})?;
|
||||
|
||||
// Start watcher to listen to any changes on the whitelist file
|
||||
watcher
|
||||
.watch(Path::new(whitelist_csv_path), RecursiveMode::Recursive)
|
||||
.map_err(|err| eyre!("Error occured when starting up watcher for hot reload: {err}"))?;
|
||||
|
||||
Some(watcher)
|
||||
} else {
|
||||
// Skip setup the watcher if auth whitelist is not loaded
|
||||
None
|
||||
};
|
||||
// Need to return the watcher to parent function, else it will be dropped and
|
||||
// stop listening
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs::OpenOptions, time::Duration};
|
||||
|
||||
use csv::WriterBuilder;
|
||||
|
||||
use crate::AuthorizationProperties;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_watch_and_reload_authorization_whitelist() {
|
||||
// Clone fixture auth whitelist for testing
|
||||
let original_whitelist_csv_path = "../tests-integration/fixture/auth/whitelist.csv";
|
||||
let whitelist_csv_path =
|
||||
"../tests-integration/fixture/auth/whitelist_copied.csv".to_string();
|
||||
std::fs::copy(original_whitelist_csv_path, &whitelist_csv_path).unwrap();
|
||||
|
||||
// Setup watcher
|
||||
let config = NotaryServerProperties {
|
||||
auth: AuthorizationProperties {
|
||||
enabled: true,
|
||||
whitelist_path: Some(whitelist_csv_path.clone()),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let authorization_whitelist = load_authorization_whitelist(&config)
|
||||
.expect("Authorization whitelist csv from fixture should be able to be loaded")
|
||||
.as_ref()
|
||||
.map(|whitelist| Arc::new(Mutex::new(whitelist.clone())));
|
||||
let _watcher = watch_and_reload_authorization_whitelist(
|
||||
config.clone(),
|
||||
authorization_whitelist.as_ref().map(Arc::clone),
|
||||
)
|
||||
.expect("Watcher should be able to be setup successfully")
|
||||
.expect("Watcher should be set up and not None");
|
||||
|
||||
// Sleep to buy a bit of time for hot reload task and watcher thread to run
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Write a new record to the whitelist to trigger modify event
|
||||
let new_record = AuthorizationWhitelistRecord {
|
||||
name: "unit-test-name".to_string(),
|
||||
api_key: "unit-test-api-key".to_string(),
|
||||
created_at: "unit-test-created-at".to_string(),
|
||||
};
|
||||
if let Some(ref path) = config.auth.whitelist_path {
|
||||
let file = OpenOptions::new().append(true).open(path).unwrap();
|
||||
let mut wtr = WriterBuilder::new()
|
||||
.has_headers(false) // Set to false to avoid writing header again
|
||||
.from_writer(file);
|
||||
wtr.serialize(new_record).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
} else {
|
||||
panic!("Whitelist CSV path should be provided in the config");
|
||||
impl AuthorizationMode {
|
||||
pub fn as_whitelist(&self) -> Option<&Whitelist> {
|
||||
match self {
|
||||
Self::Jwt(..) => None,
|
||||
Self::Whitelist(whitelist) => Some(whitelist),
|
||||
}
|
||||
// Sleep to buy a bit of time for updated whitelist to be hot reloaded
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert!(authorization_whitelist
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key("unit-test-api-key"));
|
||||
|
||||
// Delete the cloned whitelist
|
||||
std::fs::remove_file(&whitelist_csv_path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load authorization mode if it is enabled
|
||||
pub async fn load_authorization_mode(
|
||||
config: &NotaryServerProperties,
|
||||
) -> Result<Option<AuthorizationMode>> {
|
||||
if !config.auth.enabled {
|
||||
debug!("Skipping authorization as it is turned off.");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let auth_mode = match config.auth.mode.as_ref().ok_or_else(|| {
|
||||
eyre!(
|
||||
"Authorization enabled but failed to load either whitelist or jwt properties. They are either absent or malformed."
|
||||
)
|
||||
})? {
|
||||
AuthorizationModeProperties::Jwt(jwt_opts) => {
|
||||
debug!("Using JWT for authorization");
|
||||
let algorithm = Algorithm::from_str(&jwt_opts.algorithm).map_err(|_| {
|
||||
eyre!(
|
||||
"Unexpected JWT signing algorithm specified: '{}'. Possible values are: {:?}",
|
||||
jwt_opts.algorithm,
|
||||
Algorithm::VARIANTS,
|
||||
)
|
||||
})?;
|
||||
let claims = jwt_opts.claims.clone();
|
||||
let key = load_jwt_key(&jwt_opts.public_key_path, algorithm)
|
||||
.await
|
||||
.map_err(|err| eyre!("Failed to parse JWT public key: {:?}", err))?;
|
||||
AuthorizationMode::Jwt(Jwt {
|
||||
key,
|
||||
claims,
|
||||
algorithm,
|
||||
})
|
||||
}
|
||||
AuthorizationModeProperties::Whitelist(whitelist_csv_path) => {
|
||||
debug!("Using whitelist for authorization");
|
||||
let entries = load_authorization_whitelist(whitelist_csv_path)?;
|
||||
AuthorizationMode::Whitelist(Whitelist {
|
||||
entries: Arc::new(Mutex::new(entries)),
|
||||
csv_path: whitelist_csv_path.clone(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(auth_mode))
|
||||
}
|
||||
|
||||
210
crates/notary/server/src/auth/jwt.rs
Normal file
210
crates/notary/server/src/auth/jwt.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
use eyre::Result;
|
||||
use jsonwebtoken::{Algorithm as JwtAlgorithm, DecodingKey};
|
||||
use serde_json::Value;
|
||||
use strum::{EnumString, VariantNames};
|
||||
use tracing::error;
|
||||
|
||||
use crate::JwtClaim;
|
||||
|
||||
/// Custom error for JWT handling
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
#[error("JWT validation error: {0}")]
|
||||
pub struct JwtValidationError(String);
|
||||
|
||||
type JwtResult<T> = std::result::Result<T, JwtValidationError>;
|
||||
|
||||
/// JWT config which also encapsulates claims validation logic.
|
||||
#[derive(Clone)]
|
||||
pub struct Jwt {
|
||||
pub algorithm: Algorithm,
|
||||
pub key: DecodingKey,
|
||||
pub claims: Vec<JwtClaim>,
|
||||
}
|
||||
|
||||
impl Jwt {
|
||||
pub fn validate(&self, claims: &Value) -> JwtResult<()> {
|
||||
Jwt::validate_claims(&self.claims, claims)
|
||||
}
|
||||
|
||||
fn validate_claims(expected: &[JwtClaim], claims: &Value) -> JwtResult<()> {
|
||||
expected
|
||||
.iter()
|
||||
.try_for_each(|expected| Self::validate_claim(expected, claims))
|
||||
}
|
||||
|
||||
fn validate_claim(expected: &JwtClaim, given: &Value) -> JwtResult<()> {
|
||||
let pointer = format!("/{}", expected.name.replace(".", "/"));
|
||||
let field = given.pointer(&pointer).ok_or(JwtValidationError(format!(
|
||||
"missing claim '{}'",
|
||||
expected.name
|
||||
)))?;
|
||||
|
||||
let field_typed = field.as_str().ok_or(JwtValidationError(format!(
|
||||
"unexpected type for claim '{}': only strings are supported for claim values",
|
||||
expected.name,
|
||||
)))?;
|
||||
if !expected.values.is_empty() {
|
||||
expected.values.iter().any(|exp| exp == field_typed).then_some(()).ok_or_else(|| {
|
||||
let expected_values = expected.values.iter().map(|x| format!("'{x}'")).collect::<Vec<String>>().join(", ");
|
||||
JwtValidationError(format!(
|
||||
"unexpected value for claim '{}': expected one of [ {expected_values} ], received '{field_typed}'", expected.name
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(EnumString, Debug, Clone, Copy, PartialEq, Eq, VariantNames)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
/// Supported JWT signing algorithms
|
||||
pub enum Algorithm {
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-256
|
||||
RS256,
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-384
|
||||
RS384,
|
||||
/// RSASSA-PKCS1-v1_5 using SHA-512
|
||||
RS512,
|
||||
/// RSASSA-PSS using SHA-256
|
||||
PS256,
|
||||
/// RSASSA-PSS using SHA-384
|
||||
PS384,
|
||||
/// RSASSA-PSS using SHA-512
|
||||
PS512,
|
||||
/// ECDSA using SHA-256
|
||||
ES256,
|
||||
/// ECDSA using SHA-384
|
||||
ES384,
|
||||
/// Edwards-curve Digital Signature Algorithm (EdDSA)
|
||||
EdDSA,
|
||||
}
|
||||
|
||||
impl From<Algorithm> for JwtAlgorithm {
|
||||
fn from(value: Algorithm) -> Self {
|
||||
match value {
|
||||
Algorithm::RS256 => Self::RS256,
|
||||
Algorithm::RS384 => Self::RS384,
|
||||
Algorithm::RS512 => Self::RS512,
|
||||
Algorithm::PS256 => Self::PS256,
|
||||
Algorithm::PS384 => Self::PS384,
|
||||
Algorithm::PS512 => Self::PS512,
|
||||
Algorithm::ES256 => Self::ES256,
|
||||
Algorithm::ES384 => Self::ES384,
|
||||
Algorithm::EdDSA => Self::EdDSA,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load JWT public key
|
||||
pub(super) async fn load_jwt_key(
|
||||
public_key_pem_path: &str,
|
||||
algorithm: Algorithm,
|
||||
) -> Result<DecodingKey> {
|
||||
let key_pem_bytes = tokio::fs::read(public_key_pem_path).await?;
|
||||
let key = match algorithm {
|
||||
Algorithm::RS256
|
||||
| Algorithm::RS384
|
||||
| Algorithm::RS512
|
||||
| Algorithm::PS256
|
||||
| Algorithm::PS384
|
||||
| Algorithm::PS512 => DecodingKey::from_rsa_pem(&key_pem_bytes)?,
|
||||
Algorithm::ES256 | Algorithm::ES384 => DecodingKey::from_ec_pem(&key_pem_bytes)?,
|
||||
Algorithm::EdDSA => DecodingKey::from_ed_pem(&key_pem_bytes)?,
|
||||
};
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn validates_presence() {
|
||||
let expected = JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let given = json!({
|
||||
"exp": 12345,
|
||||
"sub": "test",
|
||||
});
|
||||
Jwt::validate_claim(&expected, &given).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_expected_value() {
|
||||
let expected = JwtClaim {
|
||||
name: "custom.host".to_string(),
|
||||
values: vec!["tlsn.com".to_string(), "api.tlsn.com".to_string()],
|
||||
};
|
||||
let given = json!({
|
||||
"exp": 12345,
|
||||
"custom": {
|
||||
"host": "api.tlsn.com",
|
||||
},
|
||||
});
|
||||
Jwt::validate_claim(&expected, &given).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_with_unknown_claims() {
|
||||
let given = json!({
|
||||
"exp": 12345,
|
||||
"sub": "test",
|
||||
"what": "is_this",
|
||||
});
|
||||
Jwt::validate_claims(&[], &given).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_if_claim_missing() {
|
||||
let expected = JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let given = json!({
|
||||
"exp": 12345,
|
||||
"host": "localhost",
|
||||
});
|
||||
assert_eq!(
|
||||
Jwt::validate_claim(&expected, &given),
|
||||
Err(JwtValidationError("missing claim 'sub'".to_string()))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_if_claim_has_unknown_value() {
|
||||
let expected = JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
values: vec!["tlsn_prod".to_string(), "tlsn_test".to_string()],
|
||||
};
|
||||
let given = json!({
|
||||
"sub": "tlsn",
|
||||
});
|
||||
assert_eq!(
|
||||
Jwt::validate_claim(&expected, &given),
|
||||
Err(JwtValidationError("unexpected value for claim 'sub': expected one of [ 'tlsn_prod', 'tlsn_test' ], received 'tlsn'".to_string()))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_if_claim_has_invalid_value_type() {
|
||||
let expected = JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let given = json!({
|
||||
"sub": { "name": "john" }
|
||||
});
|
||||
assert_eq!(
|
||||
Jwt::validate_claim(&expected, &given),
|
||||
Err(JwtValidationError(
|
||||
"unexpected type for claim 'sub': only strings are supported for claim values"
|
||||
.to_string()
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
161
crates/notary/server/src/auth/whitelist.rs
Normal file
161
crates/notary/server/src/auth/whitelist.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use eyre::{eyre, Result};
|
||||
use notify::{
|
||||
event::ModifyKind, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::util::parse_csv_file;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Whitelist {
|
||||
pub entries: Arc<Mutex<HashMap<String, AuthorizationWhitelistRecord>>>,
|
||||
pub csv_path: String,
|
||||
}
|
||||
|
||||
/// Structure of each whitelisted record of the API key whitelist for
|
||||
/// authorization purpose
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct AuthorizationWhitelistRecord {
|
||||
pub name: String,
|
||||
pub api_key: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// Convert whitelist data structure from vector to hashmap using api_key as the
|
||||
/// key to speed up lookup
|
||||
pub(crate) fn authorization_whitelist_vec_into_hashmap(
|
||||
authorization_whitelist: Vec<AuthorizationWhitelistRecord>,
|
||||
) -> HashMap<String, AuthorizationWhitelistRecord> {
|
||||
let mut hashmap = HashMap::new();
|
||||
authorization_whitelist.iter().for_each(|record| {
|
||||
hashmap.insert(record.api_key.clone(), record.to_owned());
|
||||
});
|
||||
hashmap
|
||||
}
|
||||
|
||||
/// Load authorization whitelist
|
||||
pub(super) fn load_authorization_whitelist(
|
||||
whitelist_csv_path: &str,
|
||||
) -> Result<HashMap<String, AuthorizationWhitelistRecord>> {
|
||||
// Load the csv
|
||||
let whitelist_csv = parse_csv_file::<AuthorizationWhitelistRecord>(whitelist_csv_path)
|
||||
.map_err(|err| eyre!("Failed to parse authorization whitelist csv: {:?}", err))?;
|
||||
// Convert the whitelist record into hashmap for faster lookup
|
||||
let whitelist_hashmap = authorization_whitelist_vec_into_hashmap(whitelist_csv);
|
||||
Ok(whitelist_hashmap)
|
||||
}
|
||||
|
||||
// Setup a watcher to detect any changes to authorization whitelist
|
||||
// When the list file is modified, the watcher thread will reload the whitelist
|
||||
// The watcher is setup in a separate thread by the notify library which is
|
||||
// synchronous
|
||||
pub fn watch_and_reload_authorization_whitelist(
|
||||
whitelist: &Whitelist,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
let whitelist_csv_path_cloned = whitelist.csv_path.clone();
|
||||
let entries = whitelist.entries.clone();
|
||||
// Setup watcher by giving it a function that will be triggered when an event is
|
||||
// detected
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |event: Result<Event, Error>| {
|
||||
match event {
|
||||
Ok(event) => {
|
||||
// Only reload whitelist if it's an event that modified the file data
|
||||
if let EventKind::Modify(ModifyKind::Data(_)) = event.kind {
|
||||
debug!("Authorization whitelist is modified");
|
||||
match load_authorization_whitelist(&whitelist_csv_path_cloned) {
|
||||
Ok(new_authorization_whitelist) => {
|
||||
*entries.lock().unwrap() = new_authorization_whitelist;
|
||||
info!("Successfully reloaded authorization whitelist!");
|
||||
}
|
||||
// Ensure that error from reloading doesn't bring the server down
|
||||
Err(err) => error!("{err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error occured when watcher detected an event: {err}")
|
||||
}
|
||||
}
|
||||
},
|
||||
notify::Config::default(),
|
||||
)
|
||||
.map_err(|err| eyre!("Error occured when setting up watcher for hot reload: {err}"))?;
|
||||
|
||||
// Start watcher to listen to any changes on the whitelist file
|
||||
watcher
|
||||
.watch(Path::new(&whitelist.csv_path), RecursiveMode::Recursive)
|
||||
.map_err(|err| eyre!("Error occured when starting up watcher for hot reload: {err}"))?;
|
||||
|
||||
// Need to return the watcher to parent function, else it will be dropped and
|
||||
// stop listening
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs::OpenOptions, time::Duration};
|
||||
|
||||
use csv::WriterBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_watch_and_reload_authorization_whitelist() {
|
||||
// Clone fixture auth whitelist for testing
|
||||
let original_whitelist_csv_path = "../tests-integration/fixture/auth/whitelist.csv";
|
||||
let whitelist_csv_path =
|
||||
"../tests-integration/fixture/auth/whitelist_copied.csv".to_string();
|
||||
std::fs::copy(original_whitelist_csv_path, &whitelist_csv_path).unwrap();
|
||||
|
||||
// Setup watcher
|
||||
let entries = load_authorization_whitelist(&whitelist_csv_path).expect(
|
||||
"Authorization whitelist csv from fixture should be able
|
||||
to be loaded",
|
||||
);
|
||||
let whitelist = Whitelist {
|
||||
entries: Arc::new(Mutex::new(entries)),
|
||||
csv_path: whitelist_csv_path.clone(),
|
||||
};
|
||||
let _watcher = watch_and_reload_authorization_whitelist(&whitelist)
|
||||
.expect("Watcher should be able to be setup successfully");
|
||||
|
||||
// Sleep to buy a bit of time for hot reload task and watcher thread to run
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Write a new record to the whitelist to trigger modify event
|
||||
let new_record = AuthorizationWhitelistRecord {
|
||||
name: "unit-test-name".to_string(),
|
||||
api_key: "unit-test-api-key".to_string(),
|
||||
created_at: "unit-test-created-at".to_string(),
|
||||
};
|
||||
let file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&whitelist_csv_path)
|
||||
.unwrap();
|
||||
let mut wtr = WriterBuilder::new()
|
||||
.has_headers(false) // Set to false to avoid writing header again
|
||||
.from_writer(file);
|
||||
wtr.serialize(new_record).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
|
||||
// Sleep to buy a bit of time for updated whitelist to be hot reloaded
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert!(whitelist
|
||||
.entries
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key("unit-test-api-key"));
|
||||
|
||||
// Delete the cloned whitelist
|
||||
std::fs::remove_file(&whitelist_csv_path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -42,19 +42,35 @@ impl NotaryServerProperties {
|
||||
.to_string();
|
||||
|
||||
// Prepend notarization key path.
|
||||
if let Some(path) = &config.notarization.private_key_path {
|
||||
config.notarization.private_key_path = Some(prepend_file_path(path, &parent_dir)?);
|
||||
if let Some(path) = config.notarization.private_key_path {
|
||||
config.notarization.private_key_path = Some(prepend_file_path(&path, &parent_dir)?);
|
||||
}
|
||||
// Prepend TLS key paths.
|
||||
if let Some(path) = &config.tls.private_key_path {
|
||||
config.tls.private_key_path = Some(prepend_file_path(path, &parent_dir)?);
|
||||
if let Some(path) = config.tls.private_key_path {
|
||||
config.tls.private_key_path = Some(prepend_file_path(&path, &parent_dir)?);
|
||||
}
|
||||
if let Some(path) = &config.tls.certificate_path {
|
||||
config.tls.certificate_path = Some(prepend_file_path(path, &parent_dir)?);
|
||||
if let Some(path) = config.tls.certificate_path {
|
||||
config.tls.certificate_path = Some(prepend_file_path(&path, &parent_dir)?);
|
||||
}
|
||||
// Prepend auth whitelist path.
|
||||
if let Some(path) = &config.auth.whitelist_path {
|
||||
config.auth.whitelist_path = Some(prepend_file_path(path, &parent_dir)?);
|
||||
// Prepend auth file path.
|
||||
if let Some(mode) = config.auth.mode {
|
||||
config.auth.mode = Some(match mode {
|
||||
AuthorizationModeProperties::Jwt(JwtAuthorizationProperties {
|
||||
algorithm,
|
||||
public_key_path,
|
||||
claims,
|
||||
}) => AuthorizationModeProperties::Jwt(JwtAuthorizationProperties {
|
||||
algorithm,
|
||||
public_key_path: prepend_file_path(&public_key_path, &parent_dir)?,
|
||||
claims,
|
||||
}),
|
||||
AuthorizationModeProperties::Whitelist(path) => {
|
||||
AuthorizationModeProperties::Whitelist(prepend_file_path(
|
||||
&path,
|
||||
&parent_dir,
|
||||
)?)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
@@ -134,8 +150,39 @@ pub struct LogProperties {
|
||||
pub struct AuthorizationProperties {
|
||||
/// Flag to turn on or off auth middleware
|
||||
pub enabled: bool,
|
||||
/// Authorization mode to use: JWT or Whitelist
|
||||
#[serde(flatten)]
|
||||
pub mode: Option<AuthorizationModeProperties>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum AuthorizationModeProperties {
|
||||
/// JWT authorization properties
|
||||
Jwt(JwtAuthorizationProperties),
|
||||
/// File path of the API key whitelist (in CSV format)
|
||||
pub whitelist_path: Option<String>,
|
||||
Whitelist(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct JwtAuthorizationProperties {
|
||||
/// Algorithm used for signing the JWT
|
||||
pub algorithm: String,
|
||||
/// File path to JWT public key (in PEM format) for verifying token
|
||||
/// signatures
|
||||
pub public_key_path: String,
|
||||
/// Optional set of required JWT claims
|
||||
#[serde(default)]
|
||||
pub claims: Vec<JwtClaim>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct JwtClaim {
|
||||
/// Name of the claim
|
||||
pub name: String,
|
||||
/// Optional set of expected values for the claim
|
||||
#[serde(default)]
|
||||
pub values: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for NotaryServerProperties {
|
||||
|
||||
@@ -14,8 +14,8 @@ mod util;
|
||||
|
||||
pub use cli::CliFields;
|
||||
pub use config::{
|
||||
AuthorizationProperties, LogProperties, NotarizationProperties, NotaryServerProperties,
|
||||
TLSProperties,
|
||||
AuthorizationModeProperties, AuthorizationProperties, JwtAuthorizationProperties, JwtClaim,
|
||||
LogProperties, NotarizationProperties, NotaryServerProperties, TLSProperties,
|
||||
};
|
||||
pub use error::NotaryServerError;
|
||||
pub use server::{read_pem_file, run_server};
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use axum::http::request::Parts;
|
||||
use axum::http::{header, request::Parts};
|
||||
use axum_core::extract::{FromRef, FromRequestParts};
|
||||
use jsonwebtoken::{decode, TokenData, Validation};
|
||||
use notary_common::X_API_KEY_HEADER;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tracing::{error, trace};
|
||||
|
||||
use crate::{auth::AuthorizationWhitelistRecord, types::NotaryGlobals, NotaryServerError};
|
||||
use crate::{
|
||||
auth::{AuthorizationMode, AuthorizationWhitelistRecord},
|
||||
types::NotaryGlobals,
|
||||
NotaryServerError,
|
||||
};
|
||||
|
||||
/// Auth middleware to prevent DOS
|
||||
pub struct AuthorizationMiddleware;
|
||||
@@ -18,36 +24,64 @@ where
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let notary_globals = NotaryGlobals::from_ref(state);
|
||||
let Some(whitelist) = notary_globals.authorization_whitelist else {
|
||||
trace!("Skipping authorization as whitelist is not set.");
|
||||
let Some(mode) = notary_globals.authorization_mode else {
|
||||
trace!("Skipping authorization as it's not enabled.");
|
||||
return Ok(Self);
|
||||
};
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(X_API_KEY_HEADER)
|
||||
.and_then(|value| std::str::from_utf8(value.as_bytes()).ok());
|
||||
|
||||
match auth_header {
|
||||
Some(auth_header) => {
|
||||
let whitelist = whitelist.lock().unwrap();
|
||||
if api_key_is_valid(auth_header, &whitelist) {
|
||||
match mode {
|
||||
AuthorizationMode::Whitelist(whitelist) => {
|
||||
let Some(auth_header) = parts
|
||||
.headers
|
||||
.get(X_API_KEY_HEADER)
|
||||
.and_then(|value| std::str::from_utf8(value.as_bytes()).ok())
|
||||
else {
|
||||
return Err(unauthorized("Missing API key"));
|
||||
};
|
||||
let entries = whitelist.entries.lock().unwrap();
|
||||
if api_key_is_valid(auth_header, &entries) {
|
||||
trace!("Request authorized.");
|
||||
Ok(Self)
|
||||
} else {
|
||||
let err_msg = "Invalid API key.".to_string();
|
||||
error!(err_msg);
|
||||
Err(NotaryServerError::UnauthorizedProverRequest(err_msg))
|
||||
Err(unauthorized("Invalid API key"))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let err_msg = "Missing API key.".to_string();
|
||||
error!(err_msg);
|
||||
Err(NotaryServerError::UnauthorizedProverRequest(err_msg))
|
||||
AuthorizationMode::Jwt(jwt_config) => {
|
||||
let Some(auth_header) = parts
|
||||
.headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| std::str::from_utf8(value.as_bytes()).ok())
|
||||
else {
|
||||
return Err(unauthorized("Missing JWT token"));
|
||||
};
|
||||
let raw_token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
|
||||
unauthorized("Invalid Authorization header: expected 'Bearer <token>'")
|
||||
})?;
|
||||
let validation = Validation::new(jwt_config.algorithm.into());
|
||||
let claims = match decode::<Value>(raw_token, &jwt_config.key, &validation) {
|
||||
Ok(TokenData { claims, .. }) => claims,
|
||||
Err(err) => {
|
||||
error!("Decoding JWT failed with error: {err:?}");
|
||||
return Err(unauthorized("Invalid JWT token"));
|
||||
}
|
||||
};
|
||||
if let Err(err) = jwt_config.validate(&claims) {
|
||||
error!("Validating JWT failed with error: {err:?}");
|
||||
return Err(unauthorized("Invalid JWT token"));
|
||||
};
|
||||
trace!("Request authorized.");
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unauthorized(err_msg: impl ToString) -> NotaryServerError {
|
||||
let err_msg = err_msg.to_string();
|
||||
error!(err_msg);
|
||||
NotaryServerError::UnauthorizedProverRequest(err_msg)
|
||||
}
|
||||
|
||||
/// Helper function to check if an API key is in whitelist
|
||||
fn api_key_is_valid(
|
||||
api_key: &str,
|
||||
@@ -59,7 +93,9 @@ fn api_key_is_valid(
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{api_key_is_valid, HashMap};
|
||||
use crate::auth::{authorization_whitelist_vec_into_hashmap, AuthorizationWhitelistRecord};
|
||||
use crate::auth::{
|
||||
whitelist::authorization_whitelist_vec_into_hashmap, AuthorizationWhitelistRecord,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn get_whitelist_fixture() -> HashMap<String, AuthorizationWhitelistRecord> {
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::{
|
||||
io::BufReader,
|
||||
net::{IpAddr, SocketAddr},
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
sync::Arc,
|
||||
};
|
||||
use tlsn_core::CryptoProvider;
|
||||
use tokio::{fs::File, io::AsyncReadExt, net::TcpListener};
|
||||
@@ -28,7 +28,7 @@ use tracing::{debug, error, info, warn};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
auth::{load_authorization_whitelist, watch_and_reload_authorization_whitelist},
|
||||
auth::{load_authorization_mode, watch_and_reload_authorization_whitelist, AuthorizationMode},
|
||||
config::{NotarizationProperties, NotaryServerProperties},
|
||||
error::NotaryServerError,
|
||||
middleware::AuthorizationMiddleware,
|
||||
@@ -87,12 +87,14 @@ pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotarySer
|
||||
Some(TlsAcceptor::from(tls_config))
|
||||
};
|
||||
|
||||
// Load the authorization whitelist csv if it is turned on
|
||||
let authorization_whitelist =
|
||||
load_authorization_whitelist(config)?.map(|whitelist| Arc::new(Mutex::new(whitelist)));
|
||||
// Set up authorization if it is turned on
|
||||
let authorization_mode = load_authorization_mode(config).await?;
|
||||
// Enable hot reload if authorization whitelist is available
|
||||
let watcher =
|
||||
watch_and_reload_authorization_whitelist(config.clone(), authorization_whitelist.clone())?;
|
||||
let watcher = authorization_mode
|
||||
.as_ref()
|
||||
.and_then(AuthorizationMode::as_whitelist)
|
||||
.map(watch_and_reload_authorization_whitelist)
|
||||
.transpose()?;
|
||||
if watcher.is_some() {
|
||||
debug!("Successfully setup watcher for hot reload of authorization whitelist!");
|
||||
}
|
||||
@@ -113,7 +115,7 @@ pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotarySer
|
||||
let notary_globals = NotaryGlobals::new(
|
||||
Arc::new(crypto_provider),
|
||||
config.notarization.clone(),
|
||||
authorization_whitelist,
|
||||
authorization_mode,
|
||||
Arc::new(Semaphore::new(config.concurrency)),
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::sync::Semaphore;
|
||||
|
||||
#[cfg(feature = "tee_quote")]
|
||||
use crate::tee::Quote;
|
||||
use crate::{auth::AuthorizationWhitelistRecord, config::NotarizationProperties};
|
||||
use crate::{auth::AuthorizationMode, config::NotarizationProperties};
|
||||
|
||||
/// Response object of the /info API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -34,14 +34,14 @@ pub struct NotarizationRequestQuery {
|
||||
}
|
||||
|
||||
/// Global data that needs to be shared with the axum handlers
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct NotaryGlobals {
|
||||
pub crypto_provider: Arc<CryptoProvider>,
|
||||
pub notarization_config: NotarizationProperties,
|
||||
/// A temporary storage to store session_id
|
||||
pub store: Arc<Mutex<HashMap<String, ()>>>,
|
||||
/// Whitelist of API keys for authorization purpose
|
||||
pub authorization_whitelist: Option<Arc<Mutex<HashMap<String, AuthorizationWhitelistRecord>>>>,
|
||||
/// Selected authorization mode if any
|
||||
pub authorization_mode: Option<AuthorizationMode>,
|
||||
/// A semaphore to acquire a permit for notarization
|
||||
pub semaphore: Arc<Semaphore>,
|
||||
}
|
||||
@@ -50,14 +50,14 @@ impl NotaryGlobals {
|
||||
pub fn new(
|
||||
crypto_provider: Arc<CryptoProvider>,
|
||||
notarization_config: NotarizationProperties,
|
||||
authorization_whitelist: Option<Arc<Mutex<HashMap<String, AuthorizationWhitelistRecord>>>>,
|
||||
authorization_mode: Option<AuthorizationMode>,
|
||||
semaphore: Arc<Semaphore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
crypto_provider,
|
||||
notarization_config,
|
||||
store: Default::default(),
|
||||
authorization_whitelist,
|
||||
authorization_mode,
|
||||
semaphore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ hyper-tls = { version = "0.6", features = [
|
||||
"vendored",
|
||||
] } # specify vendored feature to use statically linked copy of OpenSSL
|
||||
hyper-util = { workspace = true, features = ["full"] }
|
||||
jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
|
||||
rstest = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
rustls-pemfile = { workspace = true }
|
||||
|
||||
51
crates/notary/tests-integration/fixture/auth/jwt.key
Normal file
51
crates/notary/tests-integration/fixture/auth/jwt.key
Normal file
@@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
14
crates/notary/tests-integration/fixture/auth/jwt.key.pub
Normal file
14
crates/notary/tests-integration/fixture/auth/jwt.key.pub
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxkfGQo2iyUK6sV84rvsb
|
||||
6d4IlorFaX4WDwDnEP/zU2Pduwf7kV39x6oqJzNjmfXm/RkcaAZXQdbvjBA9uwM7
|
||||
cd2Z7hMWLT3oix66Qv9d3+PWcdJtTUVK710+QflZJqOEFOt30eNHm/8pN6z1P6YS
|
||||
ZvYpHMVlCC7tL8OLWcMH9gYmUH6cWOdzCaCigdQD1CqE9TG7jZlIepiWI7QMD7v/
|
||||
yN8tZoV8EzW25JdGPe4gtetBl1O+QoAUpuQp7JSUxV4RR8HArGGSQ0OHHZMcfWx/
|
||||
CoLBUyXlmSCC21sSl634l7+HUM6U/dHo1b+XSMpjjVCTjd0lDFCU+kiLtapdcCiJ
|
||||
DijSj+a4xkJP+uvxpTZuB6A4W31eDeKPIeutPAwcnw8UktdX6eiAt1ONAxB+ytZN
|
||||
cdrAUbMdIcocrMxgyPtCWBgDX1LKfpPlRTBRI50mdFkIOp9dh02ijpWAYXaFA65a
|
||||
I8Tqh9j3wzbvFsCWpBfK7Zcbl7BZskAvXCjnPHDdup5sesnYrhOOG4jo2/nho7Am
|
||||
EEkvZVDyH9jDPrA6imljFX1gpKvWmtERY7eor3gPI6FFOUh8qwEOB4lTsj9DUd75
|
||||
vvRlaPU7ibvFTdLVoRiXkAraKR59WmcOpAfut9yOxNaE+M25jY/Jj+crptEt0Mme
|
||||
zfowRimt3wpQ0C7i6hCBgrMCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -43,4 +43,4 @@ log:
|
||||
|
||||
auth:
|
||||
enabled: false
|
||||
whitelist_path: "../auth/whitelist.csv"
|
||||
whitelist: "../auth/whitelist.csv"
|
||||
|
||||
@@ -9,6 +9,7 @@ use hyper_util::{
|
||||
client::legacy::{connect::HttpConnector, Builder},
|
||||
rt::{TokioExecutor, TokioIo},
|
||||
};
|
||||
use jsonwebtoken::{encode, get_current_timestamp, Algorithm, EncodingKey, Header};
|
||||
use notary_client::{Accepted, ClientError, NotarizationRequest, NotaryClient, NotaryConnection};
|
||||
use notary_common::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse};
|
||||
use rstest::rstest;
|
||||
@@ -29,8 +30,9 @@ use tracing_subscriber::EnvFilter;
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
|
||||
use notary_server::{
|
||||
read_pem_file, run_server, AuthorizationProperties, NotarizationProperties,
|
||||
NotaryServerProperties, TLSProperties,
|
||||
read_pem_file, run_server, AuthorizationModeProperties, AuthorizationProperties,
|
||||
JwtAuthorizationProperties, JwtClaim, NotarizationProperties, NotaryServerProperties,
|
||||
TLSProperties,
|
||||
};
|
||||
|
||||
const MAX_SENT_DATA: usize = 1 << 13;
|
||||
@@ -41,11 +43,28 @@ const NOTARY_DNS: &str = "tlsnotaryserver.io";
|
||||
const NOTARY_CA_CERT_PATH: &str = "./fixture/tls/rootCA.crt";
|
||||
const NOTARY_CA_CERT_BYTES: &[u8] = include_bytes!("../fixture/tls/rootCA.crt");
|
||||
const API_KEY: &str = "test_api_key_0";
|
||||
const JWT_PRIVATE_KEY: &[u8] = include_bytes!("../fixture/auth/jwt.key");
|
||||
|
||||
enum AuthMode {
|
||||
Jwt,
|
||||
Whitelist,
|
||||
}
|
||||
|
||||
fn get_jwt() -> String {
|
||||
let priv_key = EncodingKey::from_rsa_pem(JWT_PRIVATE_KEY).unwrap();
|
||||
let timestamp = get_current_timestamp() as i64 + 1000;
|
||||
encode(
|
||||
&Header::new(Algorithm::RS256),
|
||||
&serde_json::json!({ "exp": timestamp, "sub": "test"}),
|
||||
&priv_key,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn get_server_config(
|
||||
port: u16,
|
||||
tls_enabled: bool,
|
||||
auth_enabled: bool,
|
||||
auth: Option<AuthMode>,
|
||||
concurrency: usize,
|
||||
) -> NotaryServerProperties {
|
||||
NotaryServerProperties {
|
||||
@@ -63,8 +82,20 @@ fn get_server_config(
|
||||
certificate_path: Some("./fixture/tls/notary.crt".to_string()),
|
||||
},
|
||||
auth: AuthorizationProperties {
|
||||
enabled: auth_enabled,
|
||||
whitelist_path: Some("./fixture/auth/whitelist.csv".to_string()),
|
||||
enabled: auth.is_some(),
|
||||
mode: auth.map(|mode| match mode {
|
||||
AuthMode::Jwt => AuthorizationModeProperties::Jwt(JwtAuthorizationProperties {
|
||||
algorithm: "rs256".to_string(),
|
||||
public_key_path: "./fixture/auth/jwt.key.pub".to_string(),
|
||||
claims: vec![JwtClaim {
|
||||
name: "sub".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
}),
|
||||
AuthMode::Whitelist => AuthorizationModeProperties::Whitelist(
|
||||
"./fixture/auth/whitelist.csv".to_string(),
|
||||
),
|
||||
}),
|
||||
},
|
||||
concurrency,
|
||||
..Default::default()
|
||||
@@ -75,10 +106,10 @@ async fn setup_config_and_server(
|
||||
sleep_ms: u64,
|
||||
port: u16,
|
||||
tls_enabled: bool,
|
||||
auth_enabled: bool,
|
||||
auth: Option<AuthMode>,
|
||||
concurrency: usize,
|
||||
) -> NotaryServerProperties {
|
||||
let notary_config = get_server_config(port, tls_enabled, auth_enabled, concurrency);
|
||||
let notary_config = get_server_config(port, tls_enabled, auth, concurrency);
|
||||
|
||||
// Abruptly closed connections will cause the server to log errors. We
|
||||
// prevent that by excluding the noisy modules from logging.
|
||||
@@ -113,7 +144,14 @@ fn tcp_prover_client(notary_config: NotaryServerProperties) -> NotaryClient {
|
||||
.enable_tls(false);
|
||||
|
||||
if notary_config.auth.enabled {
|
||||
notary_client_builder.api_key(API_KEY);
|
||||
match notary_config.auth.mode.unwrap() {
|
||||
AuthorizationModeProperties::Jwt(..) => {
|
||||
notary_client_builder.jwt(get_jwt());
|
||||
}
|
||||
AuthorizationModeProperties::Whitelist(..) => {
|
||||
notary_client_builder.api_key(API_KEY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notary_client_builder.build().unwrap()
|
||||
@@ -164,13 +202,16 @@ async fn tls_prover(notary_config: NotaryServerProperties) -> (NotaryConnection,
|
||||
// For `tls_without_auth` test to pass, one needs to add "<NOTARY_HOST> <NOTARY_DNS>" in /etc/hosts
|
||||
// so that this test programme can resolve the self-named NOTARY_DNS to NOTARY_HOST IP successfully.
|
||||
#[case::tls_without_auth({
|
||||
tls_prover(setup_config_and_server(100, 7047, true, false, 100).await)
|
||||
tls_prover(setup_config_and_server(100, 7047, true, None, 100).await)
|
||||
})]
|
||||
#[case::tcp_with_auth({
|
||||
tcp_prover(setup_config_and_server(100, 7048, false, true, 100).await)
|
||||
#[case::tcp_with_whitelist_auth({
|
||||
tcp_prover(setup_config_and_server(100, 7048, false, Some(AuthMode::Whitelist), 100).await)
|
||||
})]
|
||||
#[case::tcp_with_jwt_auth({
|
||||
tcp_prover(setup_config_and_server(100, 7049, false, Some(AuthMode::Jwt), 100).await)
|
||||
})]
|
||||
#[case::tcp_without_auth({
|
||||
tcp_prover(setup_config_and_server(100, 7049, false, false, 100).await)
|
||||
tcp_prover(setup_config_and_server(100, 7050, false, None, 100).await)
|
||||
})]
|
||||
#[awt]
|
||||
#[tokio::test]
|
||||
@@ -278,7 +319,7 @@ async fn test_tcp_prover<S: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
#[ignore = "expensive"]
|
||||
async fn test_websocket_prover() {
|
||||
// Notary server configuration setup
|
||||
let notary_config = setup_config_and_server(100, 7050, true, false, 100).await;
|
||||
let notary_config = setup_config_and_server(100, 7051, true, None, 100).await;
|
||||
let notary_host = notary_config.host.clone();
|
||||
let notary_port = notary_config.port;
|
||||
|
||||
@@ -470,7 +511,7 @@ async fn test_websocket_prover() {
|
||||
async fn test_concurrency_limit() {
|
||||
const CONCURRENCY: usize = 5;
|
||||
|
||||
let notary_config = setup_config_and_server(100, 7051, false, false, CONCURRENCY).await;
|
||||
let notary_config = setup_config_and_server(100, 7052, false, None, CONCURRENCY).await;
|
||||
|
||||
async fn do_test(config: NotaryServerProperties) -> Vec<(NotaryConnection, String)> {
|
||||
// Start notarization requests in parallel.
|
||||
@@ -509,7 +550,7 @@ async fn test_concurrency_limit() {
|
||||
async fn test_notarization_request_retry() {
|
||||
const CONCURRENCY: usize = 5;
|
||||
|
||||
let config = setup_config_and_server(100, 7052, false, false, CONCURRENCY).await;
|
||||
let config = setup_config_and_server(100, 7053, false, None, CONCURRENCY).await;
|
||||
|
||||
// Max out the concurrency limit.
|
||||
let connections = (0..CONCURRENCY).map(|_| tcp_prover(config.clone()));
|
||||
|
||||
Reference in New Issue
Block a user