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:
Jakub Konka
2025-05-28 06:51:18 +02:00
committed by GitHub
parent 31def9ea81
commit b6845dfc5c
18 changed files with 810 additions and 246 deletions

69
Cargo.lock generated
View File

@@ -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"

View File

@@ -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,

View File

@@ -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 }

View File

@@ -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.

View File

@@ -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

View File

@@ -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))
}

View 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()
))
)
}
}

View 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();
}
}

View File

@@ -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 {

View File

@@ -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};

View File

@@ -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> {

View File

@@ -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)),
);

View File

@@ -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,
}
}

View File

@@ -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 }

View File

@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View 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-----

View File

@@ -43,4 +43,4 @@ log:
auth:
enabled: false
whitelist_path: "../auth/whitelist.csv"
whitelist: "../auth/whitelist.csv"

View File

@@ -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()));