refactor(notary): default to ephemeral key, remove config file & fixtures (#818)

* Add default values, refactor.

* Prepend file paths.

* Remove config and refactor.

* Fix fmt, add missing export.

* Simplify error.

* Use serde to print.

* Update crates/notary/server/src/config.rs

Co-authored-by: dan <themighty1@users.noreply.github.com>

* fixture removal + generate signing key (#819)

* Default to ephemeral key gen, remove fixutres.

* Fix wording.

* Add configuring sig alg, comment fixes.

* Fix sig alg id parsing.

* Refactor pub key to pem.

* Return error, add test.

* Update crates/notary/server/src/signing.rs

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>

---------

Co-authored-by: yuroitaki <>
Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>

---------

Co-authored-by: yuroitaki <>
Co-authored-by: dan <themighty1@users.noreply.github.com>
Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
This commit is contained in:
yuroitaki
2025-05-16 19:02:20 +08:00
committed by GitHub
parent c2a6546deb
commit edc2a1783d
35 changed files with 658 additions and 571 deletions

View File

@@ -9,13 +9,7 @@ workspace = true
[features]
tee_quote = [
"dep:mc-sgx-dcap-types",
"dep:hex",
"dep:rand",
"dep:rand06-compat",
"dep:once_cell",
"dep:simple_asn1",
"dep:pem",
"dep:lazy_static",
"dep:hex",
]
[dependencies]
@@ -29,6 +23,7 @@ axum-core = { version = "0.5" }
axum-macros = { version = "0.5" }
base64 = { version = "0.21" }
config = { version = "0.14", features = ["yaml"] }
const-oid = { version = "0.9.6", features = ["db"] }
csv = { version = "1.3" }
eyre = { version = "0.6" }
futures-util = { workspace = true }
@@ -42,6 +37,8 @@ notify = { version = "6.1.1", default-features = false, features = [
] }
p256 = { workspace = true }
pkcs8 = { workspace = true, features = ["pem"] }
rand = { workspace = true }
rand06-compat = { workspace = true }
rustls = { workspace = true }
rustls-pemfile = { workspace = true }
serde = { workspace = true, features = ["derive"] }
@@ -61,14 +58,8 @@ uuid = { workspace = true, features = ["v4", "fast-rng"] }
ws_stream_tungstenite = { workspace = true, features = ["tokio_io"] }
zeroize = { workspace = true }
mc-sgx-dcap-types = { version = "0.11.0", optional = true }
hex = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
rand06-compat = { workspace = true, optional = true }
once_cell = { workspace = true, optional = true }
simple_asn1 = { version = "0.6.2", optional = true }
pem = { version = "1.1.0", optional = true }
lazy_static = { version = "1.4", optional = true }
mc-sgx-dcap-types = { version = "0.11.0", optional = true }
[build-dependencies]
git2 = "0.19.0"

View File

@@ -1,48 +0,0 @@
server:
name: "notary-server"
host: "0.0.0.0"
port: 7047
html_info: |
<head>
<meta charset="UTF-8">
<meta name="author" content="tlsnotary">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<svg width="86" height="88" viewBox="0 0 86 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5484 0.708986C25.5484 0.17436 26.1196 -0.167376 26.5923 0.0844205L33.6891 3.86446C33.9202 3.98756 34.0645 4.22766 34.0645 4.48902V9.44049H37.6129C38.0048 9.44049 38.3226 9.75747 38.3226 10.1485V21.4766L36.1936 20.0606V11.5645H34.0645V80.9919C34.0645 81.1134 34.0332 81.2328 33.9735 81.3388L30.4251 87.6388C30.1539 88.1204 29.459 88.1204 29.1878 87.6388L25.6394 81.3388C25.5797 81.2328 25.5484 81.1134 25.5484 80.9919V0.708986Z" fill="#243F5F"/>
<path d="M21.2903 25.7246V76.7012H12.7742V34.2207H0V25.7246H21.2903Z" fill="#243F5F"/>
<path d="M63.871 76.7012H72.3871V34.2207H76.6452V76.7012H85.1613V25.7246H63.871V76.7012Z" fill="#243F5F"/>
<path d="M38.3226 25.7246H59.6129V34.2207H46.8387V46.9649H59.6129V76.7012H38.3226V68.2051H51.0968V55.4609H38.3226V25.7246Z" fill="#243F5F"/>
</svg>
<h1>Notary Server {version}!</h1>
<ul>
<li>public key: <pre>{public_key}</pre></li>
<li>git commit hash: <a href="https://github.com/tlsnotary/tlsn/commit/{git_commit_hash}">{git_commit_hash}</a></li>
<li><a href="healthcheck">health check</a></li>
<li><a href="info">info</a></li>
</ul>
</body>
notarization:
max_sent_data: 4096
max_recv_data: 16384
timeout: 1800
tls:
enabled: true
private_key_pem_path: "./fixture/tls/notary.key"
certificate_pem_path: "./fixture/tls/notary.crt"
notary_key:
private_key_pem_path: "./fixture/notary/notary.key"
public_key_pem_path: "./fixture/notary/notary.pub"
logging:
level: DEBUG
authorization:
enabled: false
whitelist_csv_path: "./fixture/auth/whitelist.csv"
concurrency: 32

View File

@@ -0,0 +1,192 @@
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, NotaryServerProperties};
/// Custom HTTP header used for specifying a whitelisted API key
pub const X_API_KEY_HEADER: &str = "X-API-Key";
/// 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 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");
}
// 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();
}
}

View File

@@ -0,0 +1,10 @@
use structopt::StructOpt;
// Fields loaded from the command line when launching this server.
#[derive(Clone, Debug, StructOpt)]
#[structopt(name = "Notary Server")]
pub struct CliFields {
/// Configuration file location (optional).
#[structopt(long)]
pub config: Option<String>,
}

View File

@@ -1,46 +1,85 @@
use serde::Deserialize;
use config::{Config, Environment};
use eyre::{eyre, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Clone, Debug, Deserialize)]
use crate::{parse_config_file, util::prepend_file_path, CliFields};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NotaryServerProperties {
/// Name and address of the notary server
pub server: ServerProperties,
pub host: String,
pub port: u16,
/// Static html response returned from API root endpoint "/". Default html
/// response contains placeholder strings that will be replaced with
/// actual values in server.rs, e.g. {version}, {public_key}
pub html_info: String,
/// The maximum number of concurrent notarization sessions
pub concurrency: usize,
/// Setting for notarization
pub notarization: NotarizationProperties,
/// Setting for TLS connection between prover and notary
pub tls: TLSProperties,
/// File path of private key (in PEM format) used to sign the notarization
pub notary_key: NotarySigningKeyProperties,
/// Setting for logging
pub logging: LoggingProperties,
pub log: LogProperties,
/// Setting for authorization
pub authorization: AuthorizationProperties,
/// The maximum number of concurrent notarization sessions
pub concurrency: usize,
pub auth: AuthorizationProperties,
}
impl Default for NotaryServerProperties {
fn default() -> Self {
Self {
server: ServerProperties::default(),
notarization: NotarizationProperties::default(),
tls: TLSProperties::default(),
notary_key: NotarySigningKeyProperties::default(),
logging: LoggingProperties::default(),
authorization: AuthorizationProperties::default(),
concurrency: 32,
impl NotaryServerProperties {
pub fn new(cli_fields: &CliFields) -> Result<Self> {
// Uses config file if given.
if let Some(config_path) = &cli_fields.config {
let mut config: NotaryServerProperties = parse_config_file(config_path)?;
// Ensures all relative file paths in the config file are prepended with
// the config file's parent directory, so that server binary can be run from
// anywhere.
let parent_dir = Path::new(config_path)
.parent()
.ok_or(eyre!("Failed to get parent directory of config file"))?
.to_str()
.ok_or_else(|| eyre!("Failed to convert path to str"))?
.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)?);
}
// 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.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)?);
}
Ok(config)
} else {
let default_config = Config::try_from(&NotaryServerProperties::default())?;
let config = Config::builder()
.add_source(default_config)
// Add in settings from environment variables (with a prefix of NS and '_' as
// separator).
.add_source(
Environment::with_prefix("NS")
.try_parsing(true)
.prefix_separator("_")
.separator("__"),
)
.build()?
.try_deserialize()?;
Ok(config)
}
}
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct AuthorizationProperties {
/// Switch to turn on or off auth middleware
pub enabled: bool,
/// File path of the whitelist API key csv
pub whitelist_csv_path: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NotarizationProperties {
/// Global limit for maximum number of bytes that can be sent
pub max_sent_data: usize,
@@ -49,54 +88,108 @@ pub struct NotarizationProperties {
/// Number of seconds before notarization timeouts to prevent unreleased
/// memory
pub timeout: u64,
/// File path of private key (in PEM format) used to sign the notarization
pub private_key_path: Option<String>,
/// Signature algorithm used to generate a random private key when
/// private_key_path is not set
pub signature_algorithm: String,
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct ServerProperties {
/// Used for testing purpose
pub name: String,
pub host: String,
pub port: u16,
/// Static html response returned from API root endpoint "/". Default html
/// response contains placeholder strings that will be replaced with
/// actual values in server.rs, e.g. {version}, {public_key}
pub html_info: String,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct TLSProperties {
/// Flag to turn on/off TLS between prover and notary (should always be
/// turned on unless TLS is handled by external setup e.g. reverse proxy,
/// cloud)
/// Flag to turn on/off TLS between prover and notary should always be
/// turned on unless either
/// (1) TLS is handled by external setup e.g. reverse proxy, cloud; or
/// (2) For local testing
pub enabled: bool,
pub private_key_pem_path: Option<String>,
pub certificate_pem_path: Option<String>,
/// File path of TLS private key (in PEM format)
pub private_key_path: Option<String>,
/// File path of TLS cert (in PEM format)
pub certificate_path: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct NotarySigningKeyProperties {
pub private_key_pem_path: String,
pub public_key_pem_path: String,
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum LogFormat {
Compact,
Json,
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct LoggingProperties {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LogProperties {
/// Log verbosity level of the default filtering logic, which is
/// notary_server=<level>,tlsn_verifier=<level>,mpc_tls=<level> Must be either of <https://docs.rs/tracing/latest/tracing/struct.Level.html#implementations>
/// notary_server=<level>,tlsn_verifier=<level>,mpc_tls=<level>
/// Must be either of <https://docs.rs/tracing/latest/tracing/struct.Level.html#implementations>
pub level: String,
/// Custom filtering logic, refer to the syntax here https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax
/// This will override the default filtering logic above
pub filter: Option<String>,
/// Log format. Available options are "compact" and "json". Default is
/// "compact"
#[serde(default)]
/// Log format. Available options are "COMPACT" and "JSON"
pub format: LogFormat,
}
#[derive(Clone, Copy, Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum LogFormat {
#[default]
Compact,
Json,
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct AuthorizationProperties {
/// Flag to turn on or off auth middleware
pub enabled: bool,
/// File path of the API key whitelist (in CSV format)
pub whitelist_path: Option<String>,
}
impl Default for NotaryServerProperties {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 7047,
html_info: r#"
<head>
<meta charset='UTF-8'>
<meta name='author' content='tlsnotary'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
</head>
<body>
<svg width='86' height='88' viewBox='0 0 86 88' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M25.5484 0.708986C25.5484 0.17436 26.1196 -0.167376 26.5923 0.0844205L33.6891 3.86446C33.9202 3.98756 34.0645 4.22766 34.0645 4.48902V9.44049H37.6129C38.0048 9.44049 38.3226 9.75747 38.3226 10.1485V21.4766L36.1936 20.0606V11.5645H34.0645V80.9919C34.0645 81.1134 34.0332 81.2328 33.9735 81.3388L30.4251 87.6388C30.1539 88.1204 29.459 88.1204 29.1878 87.6388L25.6394 81.3388C25.5797 81.2328 25.5484 81.1134 25.5484 80.9919V0.708986Z' fill='#243F5F'/>
<path d='M21.2903 25.7246V76.7012H12.7742V34.2207H0V25.7246H21.2903Z' fill='#243F5F'/>
<path d='M63.871 76.7012H72.3871V34.2207H76.6452V76.7012H85.1613V25.7246H63.871V76.7012Z' fill='#243F5F'/>
<path d='M38.3226 25.7246H59.6129V34.2207H46.8387V46.9649H59.6129V76.7012H38.3226V68.2051H51.0968V55.4609H38.3226V25.7246Z' fill='#243F5F'/>
</svg>
<h1>Notary Server {version}!</h1>
<ul>
<li>public key: <pre>{public_key}</pre></li>
<li>git commit hash: <a href='https://github.com/tlsnotary/tlsn/commit/{git_commit_hash}'>{git_commit_hash}</a></li>
<li><a href='healthcheck'>health check</a></li>
<li><a href='info'>info</a></li>
</ul>
</body>
"#.to_string(),
concurrency: 32,
notarization: Default::default(),
tls: Default::default(),
log: Default::default(),
auth: Default::default(),
}
}
}
impl Default for NotarizationProperties {
fn default() -> Self {
Self {
max_sent_data: 4096,
max_recv_data: 16384,
timeout: 1800,
private_key_path: None,
signature_algorithm: "secp256k1".to_string(),
}
}
}
impl Default for LogProperties {
fn default() -> Self {
Self {
level: "DEBUG".to_string(),
filter: None,
format: LogFormat::Compact,
}
}
}

View File

@@ -1,21 +0,0 @@
pub mod auth;
pub mod cli;
pub mod notary;
#[cfg(feature = "tee_quote")]
use crate::tee::Quote;
use serde::{Deserialize, Serialize};
/// Response object of the /info API
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InfoResponse {
/// Current version of notary-server
pub version: String,
/// Public key of the notary signing key
pub public_key: String,
/// Current git commit hash of notary-server
pub git_commit_hash: String,
/// Hardware attestation
#[cfg(feature = "tee_quote")]
pub quote: Quote,
}

View File

@@ -1,27 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Custom HTTP header used for specifying a whitelisted API key
pub const X_API_KEY_HEADER: &str = "X-API-Key";
/// 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 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
}

View File

@@ -1,22 +0,0 @@
use structopt::StructOpt;
/// Fields loaded from the command line when launching this server.
#[derive(Clone, Debug, StructOpt)]
#[structopt(name = "Notary Server")]
pub struct CliFields {
/// Configuration file location
#[structopt(long, default_value = "./config/config.yaml")]
pub config_file: String,
/// Port of notary server
#[structopt(long)]
pub port: Option<u16>,
/// Flag to turn on/off TLS when connecting to prover
#[structopt(long)]
pub tls_enabled: Option<bool>,
/// Level of logging
#[structopt(long)]
pub log_level: Option<String>,
}

View File

@@ -1,27 +1,25 @@
mod auth;
mod cli;
mod config;
mod domain;
mod error;
mod middleware;
mod server;
mod server_tracing;
mod service;
mod settings;
mod signing;
#[cfg(feature = "tee_quote")]
mod tee;
mod types;
mod util;
pub use auth::X_API_KEY_HEADER;
pub use cli::CliFields;
pub use config::{
AuthorizationProperties, LoggingProperties, NotarizationProperties, NotaryServerProperties,
NotarySigningKeyProperties, ServerProperties, TLSProperties,
};
pub use domain::{
auth::X_API_KEY_HEADER,
cli::CliFields,
notary::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse},
AuthorizationProperties, LogProperties, NotarizationProperties, NotaryServerProperties,
TLSProperties,
};
pub use error::NotaryServerError;
pub use server::{read_pem_file, run_server};
pub use server_tracing::init_tracing;
pub use settings::Settings;
pub use types::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse};
pub use util::parse_config_file;

View File

@@ -1,5 +1,7 @@
use eyre::{eyre, Result};
use notary_server::{init_tracing, run_server, CliFields, NotaryServerError, Settings};
use notary_server::{
init_tracing, run_server, CliFields, NotaryServerError, NotaryServerProperties,
};
use structopt::StructOpt;
use tracing::debug;
@@ -8,16 +10,21 @@ async fn main() -> Result<(), NotaryServerError> {
// Load command line arguments
let cli_fields: CliFields = CliFields::from_args();
let settings =
Settings::new(&cli_fields).map_err(|err| eyre!("Failed to load settings: {}", err))?;
let config = NotaryServerProperties::new(&cli_fields)
.map_err(|err| eyre!("Failed to load config: {}", err))?;
// Set up tracing for logging
init_tracing(&settings.config).map_err(|err| eyre!("Failed to set up tracing: {err}"))?;
init_tracing(&config).map_err(|err| eyre!("Failed to set up tracing: {err}"))?;
debug!(?settings.config, "Server config loaded");
// debug!("Server config loaded: \n{}", config);
debug!(
"Server config loaded: \n{}",
serde_yaml::to_string(&config).map_err(|err| eyre!("Failed to print config: {err}"))?
);
// Run the server
run_server(&settings.config).await?;
run_server(&config).await?;
Ok(())
}

View File

@@ -4,10 +4,8 @@ use std::collections::HashMap;
use tracing::{error, trace};
use crate::{
domain::{
auth::{AuthorizationWhitelistRecord, X_API_KEY_HEADER},
notary::NotaryGlobals,
},
auth::{AuthorizationWhitelistRecord, X_API_KEY_HEADER},
types::NotaryGlobals,
NotaryServerError,
};
@@ -64,9 +62,7 @@ fn api_key_is_valid(
#[cfg(test)]
mod test {
use super::{api_key_is_valid, HashMap};
use crate::domain::auth::{
authorization_whitelist_vec_into_hashmap, AuthorizationWhitelistRecord,
};
use crate::auth::{authorization_whitelist_vec_into_hashmap, AuthorizationWhitelistRecord};
use std::sync::Arc;
fn get_whitelist_fixture() -> HashMap<String, AuthorizationWhitelistRecord> {

View File

@@ -10,17 +10,12 @@ use eyre::{ensure, eyre, Result};
use futures_util::future::poll_fn;
use hyper::{body::Incoming, server::conn::http1};
use hyper_util::rt::TokioIo;
use notify::{
event::ModifyKind, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
};
use pkcs8::DecodePrivateKey;
use rustls::{Certificate, PrivateKey, ServerConfig};
use std::{
collections::HashMap,
fs::File as StdFile,
io::BufReader,
net::{IpAddr, SocketAddr},
path::Path,
pin::Pin,
sync::{Arc, Mutex},
};
@@ -29,25 +24,21 @@ use tokio::{fs::File, io::AsyncReadExt, net::TcpListener};
use tokio_rustls::{rustls, TlsAcceptor};
use tower_http::cors::CorsLayer;
use tower_service::Service;
use tracing::{debug, error, info};
use tracing::{debug, error, info, warn};
use zeroize::Zeroize;
use crate::{
config::{NotaryServerProperties, NotarySigningKeyProperties},
domain::{
auth::{authorization_whitelist_vec_into_hashmap, AuthorizationWhitelistRecord},
notary::NotaryGlobals,
InfoResponse,
},
auth::{load_authorization_whitelist, watch_and_reload_authorization_whitelist},
config::{NotarizationProperties, NotaryServerProperties},
error::NotaryServerError,
middleware::AuthorizationMiddleware,
service::{initialize, upgrade_protocol},
signing::AttestationKey,
util::parse_csv_file,
types::{InfoResponse, NotaryGlobals},
};
#[cfg(feature = "tee_quote")]
use crate::tee::{generate_ephemeral_keypair, quote};
use crate::tee::quote;
use tokio::sync::Semaphore;
@@ -55,7 +46,14 @@ use tokio::sync::Semaphore;
/// both TCP and WebSocket clients
#[tracing::instrument(skip(config))]
pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotaryServerError> {
let attestation_key = load_attestation_key(&config.notary_key).await?;
let attestation_key = get_attestation_key(&config.notarization).await?;
let verifying_key_pem = attestation_key
.verifying_key_pem()
.map_err(|err| eyre!("Failed to get verifying key in PEM format: {err}"))?;
#[cfg(feature = "tee_quote")]
let verifying_key_bytes = attestation_key.verifying_key_bytes();
let crypto_provider = build_crypto_provider(attestation_key);
// Build TLS acceptor if it is turned on
@@ -65,12 +63,12 @@ pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotarySer
} else {
let private_key_pem_path = config
.tls
.private_key_pem_path
.private_key_path
.as_deref()
.ok_or_else(|| eyre!("TLS is enabled but private key PEM path is not set"))?;
let certificate_pem_path = config
.tls
.certificate_pem_path
.certificate_path
.as_deref()
.ok_or_else(|| eyre!("TLS is enabled but certificate PEM path is not set"))?;
@@ -100,10 +98,10 @@ pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotarySer
}
let notary_address = SocketAddr::new(
IpAddr::V4(config.server.host.parse().map_err(|err| {
IpAddr::V4(config.host.parse().map_err(|err| {
eyre!("Failed to parse notary host address from server config: {err}")
})?),
config.server.port,
config.port,
);
let mut listener = TcpListener::bind(notary_address)
.await
@@ -120,18 +118,16 @@ pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotarySer
);
// Parameters needed for the info endpoint
let public_key = std::fs::read_to_string(&config.notary_key.public_key_pem_path)
.map_err(|err| eyre!("Failed to load notary public signing key for notarization: {err}"))?;
let version = env!("CARGO_PKG_VERSION").to_string();
let git_commit_hash = env!("GIT_COMMIT_HASH").to_string();
// Parameters needed for the root / endpoint
let html_string = config.server.html_info.clone();
let html_string = config.html_info.clone();
let html_info = Html(
html_string
.replace("{version}", &version)
.replace("{git_commit_hash}", &git_commit_hash)
.replace("{public_key}", &public_key),
.replace("{public_key}", &verifying_key_pem),
);
let router = Router::new()
@@ -150,10 +146,10 @@ pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotarySer
StatusCode::OK,
Json(InfoResponse {
version,
public_key,
public_key: verifying_key_pem,
git_commit_hash,
#[cfg(feature = "tee_quote")]
quote: quote().await,
quote: quote(verifying_key_bytes).await,
}),
)
.into_response()
@@ -252,25 +248,30 @@ fn build_crypto_provider(attestation_key: AttestationKey) -> CryptoProvider {
provider
}
/// Load notary signing key for attestations from static file
async fn load_attestation_key(config: &NotarySigningKeyProperties) -> Result<AttestationKey> {
#[cfg(feature = "tee_quote")]
generate_ephemeral_keypair(&config.private_key_pem_path, &config.public_key_pem_path);
/// Get notary signing key for attestations.
/// Generate a random key if user does not provide a static key.
async fn get_attestation_key(config: &NotarizationProperties) -> Result<AttestationKey> {
let key = if let Some(private_key_path) = &config.private_key_path {
debug!("Loading notary server's signing key");
debug!("Loading notary server's signing key");
let mut file = File::open(private_key_path).await?;
let mut pem = String::new();
file.read_to_string(&mut pem)
.await
.map_err(|_| eyre!("pem file does not contain valid UTF-8"))?;
let mut file = File::open(&config.private_key_pem_path).await?;
let mut pem = String::new();
file.read_to_string(&mut pem)
.await
.map_err(|_| eyre!("pem file does not contain valid UTF-8"))?;
let key = AttestationKey::from_pkcs8_pem(&pem)
.map_err(|err| eyre!("Failed to load notary signing key for notarization: {err}"))?;
let key = AttestationKey::from_pkcs8_pem(&pem)
.map_err(|err| eyre!("Failed to load notary signing key for notarization: {err}"))?;
pem.zeroize();
pem.zeroize();
debug!("Successfully loaded notary server's signing key!");
key
} else {
warn!(
"⚠️ Using a random, ephemeral signing key because `notarization.private_key_path` is not set."
);
AttestationKey::random(&config.signature_algorithm)?
};
Ok(key)
}
@@ -306,112 +307,14 @@ async fn load_tls_key_and_cert(
Ok((private_key, certificates))
}
/// Load authorization whitelist if it is enabled
fn load_authorization_whitelist(
config: &NotaryServerProperties,
) -> Result<Option<HashMap<String, AuthorizationWhitelistRecord>>> {
let authorization_whitelist = if !config.authorization.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
.authorization
.whitelist_csv_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
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
.authorization
.whitelist_csv_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_load_notary_key_and_cert() {
let private_key_pem_path = "./fixture/tls/notary.key";
let certificate_pem_path = "./fixture/tls/notary.crt";
async fn test_load_tls_key_and_cert() {
let private_key_pem_path = "../tests-integration/fixture/tls/notary.key";
let certificate_pem_path = "../tests-integration/fixture/tls/notary.crt";
let result: Result<(PrivateKey, Vec<Certificate>)> =
load_tls_key_and_cert(private_key_pem_path, certificate_pem_path).await;
assert!(result.is_ok(), "Could not load tls private key and cert");
@@ -419,68 +322,21 @@ mod test {
#[tokio::test]
async fn test_load_attestation_key() {
let config = NotarySigningKeyProperties {
private_key_pem_path: "./fixture/notary/notary.key".to_string(),
public_key_pem_path: "./fixture/notary/notary.pub".to_string(),
let config = NotarizationProperties {
private_key_path: Some("../tests-integration/fixture/notary/notary.key".to_string()),
..Default::default()
};
load_attestation_key(&config).await.unwrap();
let result = get_attestation_key(&config).await;
assert!(result.is_ok(), "Could not load attestation key");
}
#[tokio::test]
async fn test_watch_and_reload_authorization_whitelist() {
// Clone fixture auth whitelist for testing
let original_whitelist_csv_path = "./fixture/auth/whitelist.csv";
let whitelist_csv_path = "./fixture/auth/whitelist_copied.csv".to_string();
std::fs::copy(original_whitelist_csv_path, &whitelist_csv_path).unwrap();
// Setup watcher
let config = NotaryServerProperties {
authorization: AuthorizationProperties {
enabled: true,
whitelist_csv_path: Some(whitelist_csv_path.clone()),
},
async fn test_generate_attestation_key() {
let config = NotarizationProperties {
private_key_path: None,
..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.authorization.whitelist_csv_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");
}
// 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();
let result = get_attestation_key(&config).await;
assert!(result.is_ok(), "Could not generate attestation key");
}
}

View File

@@ -21,12 +21,12 @@ where
pub fn init_tracing(config: &NotaryServerProperties) -> Result<()> {
// Retrieve log filtering logic from config
let directives = match &config.logging.filter {
let directives = match &config.log.filter {
// Use custom filter that is provided by user
Some(filter) => filter.clone(),
// Use the default filter when only verbosity level is provided
None => {
let level = Level::from_str(&config.logging.level)?;
let level = Level::from_str(&config.log.level)?;
format!("notary_server={level},tlsn_verifier={level},mpc_tls={level}")
}
};
@@ -34,7 +34,7 @@ pub fn init_tracing(config: &NotaryServerProperties) -> Result<()> {
Registry::default()
.with(filter_layer)
.with(format_layer(config.logging.format))
.with(format_layer(config.log.format))
.try_init()?;
Ok(())

View File

@@ -23,16 +23,16 @@ use tracing::{debug, error, info, trace};
use uuid::Uuid;
use crate::{
domain::notary::{
NotarizationRequestQuery, NotarizationSessionRequest, NotarizationSessionResponse,
NotaryGlobals,
},
error::NotaryServerError,
service::{
axum_websocket::{header_eq, WebSocketUpgrade},
tcp::{tcp_notarize, TcpUpgrade},
websocket::websocket_notarize,
},
types::{
NotarizationRequestQuery, NotarizationSessionRequest, NotarizationSessionResponse,
NotaryGlobals,
},
};
/// A wrapper enum to facilitate extracting TCP connection for either WebSocket

View File

@@ -10,7 +10,7 @@ use std::future::Future;
use tokio::time::Instant;
use tracing::{debug, error, info};
use crate::{domain::notary::NotaryGlobals, service::notary_service, NotaryServerError};
use crate::{service::notary_service, types::NotaryGlobals, NotaryServerError};
/// Custom extractor used to extract underlying TCP connection for TCP client —
/// using the same upgrade primitives used by the WebSocket implementation where

View File

@@ -3,8 +3,8 @@ use tracing::{debug, error, info};
use ws_stream_tungstenite::WsStream;
use crate::{
domain::notary::NotaryGlobals,
service::{axum_websocket::WebSocket, notary_service},
types::NotaryGlobals,
};
/// Perform notarization using the established websocket connection

View File

@@ -1,45 +0,0 @@
use crate::{CliFields, NotaryServerProperties};
use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize)]
pub struct Settings {
#[serde(flatten)]
pub config: NotaryServerProperties,
}
impl Settings {
pub fn new(cli_fields: &CliFields) -> Result<Self, ConfigError> {
let config_path = Path::new(&cli_fields.config_file);
let mut builder = Config::builder()
// Load base configuration
.add_source(File::from(config_path))
// Add in settings from environment variables (with a prefix of NOTARY_SERVER and '__'
// as separator).
.add_source(
Environment::with_prefix("NOTARY_SERVER")
.try_parsing(true)
.prefix_separator("__")
.separator("__"),
);
// Apply CLI argument overrides
if let Some(port) = cli_fields.port {
builder = builder.set_override("server.port", port)?;
}
if let Some(tls_enabled) = cli_fields.tls_enabled {
builder = builder.set_override("tls.enabled", tls_enabled)?;
}
if let Some(log_level) = &cli_fields.log_level {
builder = builder.set_override("logging.level", log_level.clone())?;
}
let config = builder.build()?;
let settings: Settings = config.try_deserialize()?;
Ok(settings)
}
}

View File

@@ -1,6 +1,12 @@
use const_oid::db::rfc5912::ID_EC_PUBLIC_KEY as OID_EC_PUBLIC_KEY;
use core::fmt;
use pkcs8::{der::Encode, AssociatedOid, DecodePrivateKey, ObjectIdentifier, PrivateKeyInfo};
use eyre::{eyre, Result};
use pkcs8::{
der::{self, pem::PemLabel, Encode},
spki::{DynAssociatedAlgorithmIdentifier, SubjectPublicKeyInfoRef},
AssociatedOid, DecodePrivateKey, LineEnding, PrivateKeyInfo,
};
use rand06_compat::Rand0_6CompatExt;
use tlsn_core::signing::{Secp256k1Signer, Secp256r1Signer, SignatureAlgId, Signer};
use tracing::error;
@@ -14,9 +20,6 @@ impl TryFrom<PrivateKeyInfo<'_>> for AttestationKey {
type Error = pkcs8::Error;
fn try_from(pkcs8: PrivateKeyInfo<'_>) -> Result<Self, Self::Error> {
const OID_EC_PUBLIC_KEY: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
// For now we only support elliptic curve keys.
if pkcs8.algorithm.oid != OID_EC_PUBLIC_KEY {
error!("unsupported key algorithm OID: {:?}", pkcs8.algorithm.oid);
@@ -47,6 +50,25 @@ impl TryFrom<PrivateKeyInfo<'_>> for AttestationKey {
}
impl AttestationKey {
/// Samples a new attestation key of the given signature algorithm.
pub fn random(alg_id: &str) -> Result<Self> {
match alg_id.to_uppercase().as_str() {
"SECP256K1" => Ok(Self {
alg_id: SignatureAlgId::SECP256K1,
key: SigningKey::Secp256k1(k256::ecdsa::SigningKey::random(
&mut rand::rng().compat(),
)),
}),
"SECP256R1" => Ok(Self {
alg_id: SignatureAlgId::SECP256R1,
key: SigningKey::Secp256r1(p256::ecdsa::SigningKey::random(
&mut rand::rng().compat(),
)),
}),
alg_id => Err(eyre!("unsupported signature algorithm: {alg_id} — only secp256k1 and secp256r1 are supported")),
}
}
/// Creates a new signer using this key.
pub fn into_signer(self) -> Box<dyn Signer + Send + Sync> {
match self.key {
@@ -58,6 +80,42 @@ impl AttestationKey {
}
}
}
/// Returns the verifying key in compressed bytes.
pub fn verifying_key_bytes(&self) -> Vec<u8> {
match self.key {
SigningKey::Secp256k1(ref key) => key
.verifying_key()
.to_encoded_point(true)
.as_bytes()
.to_vec(),
SigningKey::Secp256r1(ref key) => key
.verifying_key()
.to_encoded_point(true)
.as_bytes()
.to_vec(),
}
}
/// Returns the verifying key in compressed PEM format.
pub fn verifying_key_pem(&self) -> Result<String, pkcs8::spki::Error> {
let algorithm = match &self.key {
SigningKey::Secp256k1(key) => key.verifying_key().algorithm_identifier()?,
SigningKey::Secp256r1(key) => key.verifying_key().algorithm_identifier()?,
};
let verifying_key_bytes = self.verifying_key_bytes();
let subject_public_key = der::asn1::BitStringRef::new(0, &verifying_key_bytes)?;
let der: der::Document = pkcs8::SubjectPublicKeyInfo {
algorithm,
subject_public_key,
}
.try_into()?;
let pem = der.to_pem(SubjectPublicKeyInfoRef::PEM_LABEL, LineEnding::LF)?;
Ok(pem)
}
}
impl fmt::Debug for AttestationKey {
@@ -72,3 +130,24 @@ enum SigningKey {
Secp256k1(k256::ecdsa::SigningKey),
Secp256r1(p256::ecdsa::SigningKey),
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::read_to_string;
#[test]
fn test_verifying_key_pem() {
let attestation_key_pem =
read_to_string("../tests-integration/fixture/notary/notary.key").unwrap();
let attestation_key = AttestationKey::from_pkcs8_pem(&attestation_key_pem).unwrap();
let verifying_key_pem = attestation_key.verifying_key_pem().unwrap();
let expected_verifying_key_pem =
read_to_string("../tests-integration/fixture/notary/notary.pub").unwrap();
assert_eq!(verifying_key_pem, expected_verifying_key_pem);
}
}

View File

@@ -1,8 +1,4 @@
use k256::ecdsa::{SigningKey, VerifyingKey as PublicKey};
use mc_sgx_dcap_types::{QlError, Quote3};
use once_cell::sync::OnceCell;
use pkcs8::{EncodePrivateKey, LineEnding};
use rand06_compat::Rand0_6CompatExt;
use serde::{Deserialize, Serialize};
use std::{
fs,
@@ -12,11 +8,6 @@ use std::{
};
use tracing::{debug, error, instrument};
lazy_static::lazy_static! {
static ref SECP256K1_OID: simple_asn1::OID = simple_asn1::oid!(1, 3, 132, 0, 10);
static ref ECDSA_OID: simple_asn1::OID = simple_asn1::oid!(1, 2, 840, 10045, 2, 1);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Quote {
@@ -65,29 +56,8 @@ impl From<QlError> for QuoteError {
}
}
static PUBLIC_KEY: OnceCell<PublicKey> = OnceCell::new();
fn pem_der_encode_with_asn1(public_point: &[u8]) -> String {
use simple_asn1::*;
let ecdsa_oid = ASN1Block::ObjectIdentifier(0, ECDSA_OID.clone());
let secp256k1_oid = ASN1Block::ObjectIdentifier(0, SECP256K1_OID.clone());
let alg_id = ASN1Block::Sequence(0, vec![ecdsa_oid, secp256k1_oid]);
let key_bytes = ASN1Block::BitString(0, public_point.len() * 8, public_point.to_vec());
let blocks = vec![alg_id, key_bytes];
let der_out = simple_asn1::to_der(&ASN1Block::Sequence(0, blocks))
.expect("Failed to encode ECDSA private key as DER");
pem::encode(&pem::Pem {
tag: "PUBLIC KEY".to_string(),
contents: der_out,
})
}
#[instrument(level = "debug", skip_all)]
async fn gramine_quote() -> Result<Quote, QuoteError> {
async fn gramine_quote(public_key: Vec<u8>) -> Result<Quote, QuoteError> {
//// Check if the the gramine pseudo-hardware exists
if !Path::new("/dev/attestation/quote").exists() {
return Ok(Quote::default());
@@ -107,14 +77,7 @@ async fn gramine_quote() -> Result<Quote, QuoteError> {
//// Writing the pubkey to bind the instance to the hw (note: this is not
//// mrsigner)
fs::write(
"/dev/attestation/user_report_data",
PUBLIC_KEY
.get()
.expect("pub_key_get")
.to_encoded_point(true)
.as_bytes(),
)?;
fs::write("/dev/attestation/user_report_data", public_key)?;
//// Reading from the gramine quote pseudo-hardware `/dev/attestation/quote`
let mut quote_file = File::open("/dev/attestation/quote")?;
@@ -137,29 +100,9 @@ async fn gramine_quote() -> Result<Quote, QuoteError> {
})
}
pub fn generate_ephemeral_keypair(notary_private: &str, notary_public: &str) {
let signing_key = SigningKey::random(&mut rand::rng().compat());
let pem_string = signing_key
.clone()
.to_pkcs8_pem(LineEnding::LF)
.expect("to pem");
std::fs::write(notary_private, pem_string).expect("fs::write");
let der = signing_key
.verifying_key()
.to_encoded_point(true)
.to_bytes();
let pem_spki_pub = pem_der_encode_with_asn1(&der);
std::fs::write(notary_public, pem_spki_pub).expect("fs::write");
let _ = PUBLIC_KEY
.set(*signing_key.verifying_key())
.map_err(|_| "Public key has already been set");
}
pub async fn quote() -> Quote {
pub async fn quote(public_key: Vec<u8>) -> Quote {
//// tee-detection logic will live here, for now its only gramine-sgx
match gramine_quote().await {
match gramine_quote(public_key).await {
Ok(quote) => quote,
Err(err) => {
error!("Failed to retrieve quote: {:?}", err);

View File

@@ -6,7 +6,24 @@ use std::{
use tlsn_core::CryptoProvider;
use tokio::sync::Semaphore;
use crate::{config::NotarizationProperties, domain::auth::AuthorizationWhitelistRecord};
#[cfg(feature = "tee_quote")]
use crate::tee::Quote;
use crate::{auth::AuthorizationWhitelistRecord, config::NotarizationProperties};
/// Response object of the /info API
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InfoResponse {
/// Current version of notary-server
pub version: String,
/// Public key of the notary signing key
pub public_key: String,
/// Current git commit hash of notary-server
pub git_commit_hash: String,
/// Hardware attestation
#[cfg(feature = "tee_quote")]
pub quote: Quote,
}
/// Response object of the /session API
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -1,5 +1,6 @@
use eyre::Result;
use eyre::{eyre, Result};
use serde::de::DeserializeOwned;
use std::path::Path;
/// Parse a yaml configuration file into a struct
pub fn parse_config_file<T: DeserializeOwned>(location: &str) -> Result<T> {
@@ -20,19 +21,34 @@ pub fn parse_csv_file<T: DeserializeOwned>(location: &str) -> Result<Vec<T>> {
Ok(table)
}
/// Prepend a file path with a base directory if the path is not absolute.
pub fn prepend_file_path<S: AsRef<str>>(file_path: S, base_dir: S) -> Result<String> {
let path = Path::new(file_path.as_ref());
if !path.is_absolute() {
Ok(Path::new(base_dir.as_ref())
.join(path)
.to_str()
.ok_or_else(|| eyre!("Failed to convert path to str"))?
.to_string())
} else {
Ok(file_path.as_ref().to_string())
}
}
#[cfg(test)]
mod test {
use crate::{
config::NotaryServerProperties, domain::auth::AuthorizationWhitelistRecord,
util::parse_csv_file,
auth::AuthorizationWhitelistRecord,
config::NotaryServerProperties,
util::{parse_csv_file, prepend_file_path},
};
use super::{parse_config_file, Result};
#[test]
fn test_parse_config_file() {
let location = "./config/config.yaml";
let location = "../tests-integration/fixture/config/config.yaml";
let config: Result<NotaryServerProperties> = parse_config_file(location);
assert!(
config.is_ok(),
@@ -42,11 +58,26 @@ mod test {
#[test]
fn test_parse_csv_file() {
let location = "./fixture/auth/whitelist.csv";
let location = "../tests-integration/fixture/auth/whitelist.csv";
let table: Result<Vec<AuthorizationWhitelistRecord>> = parse_csv_file(location);
assert!(
table.is_ok(),
"Could not open csv or read the csv's values."
);
}
#[test]
fn test_prepend_file_path() {
let base_dir = "/base/dir";
let relative_path = "relative/path";
let absolute_path = "/absolute/path";
let result = prepend_file_path(relative_path, base_dir);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "/base/dir/relative/path");
let result = prepend_file_path(absolute_path, base_dir);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "/absolute/path");
}
}

View File

@@ -0,0 +1,45 @@
host: "0.0.0.0"
port: 7047
html_info: |
<head>
<meta charset="UTF-8">
<meta name="author" content="tlsnotary">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<svg width="86" height="88" viewBox="0 0 86 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5484 0.708986C25.5484 0.17436 26.1196 -0.167376 26.5923 0.0844205L33.6891 3.86446C33.9202 3.98756 34.0645 4.22766 34.0645 4.48902V9.44049H37.6129C38.0048 9.44049 38.3226 9.75747 38.3226 10.1485V21.4766L36.1936 20.0606V11.5645H34.0645V80.9919C34.0645 81.1134 34.0332 81.2328 33.9735 81.3388L30.4251 87.6388C30.1539 88.1204 29.459 88.1204 29.1878 87.6388L25.6394 81.3388C25.5797 81.2328 25.5484 81.1134 25.5484 80.9919V0.708986Z" fill="#243F5F"/>
<path d="M21.2903 25.7246V76.7012H12.7742V34.2207H0V25.7246H21.2903Z" fill="#243F5F"/>
<path d="M63.871 76.7012H72.3871V34.2207H76.6452V76.7012H85.1613V25.7246H63.871V76.7012Z" fill="#243F5F"/>
<path d="M38.3226 25.7246H59.6129V34.2207H46.8387V46.9649H59.6129V76.7012H38.3226V68.2051H51.0968V55.4609H38.3226V25.7246Z" fill="#243F5F"/>
</svg>
<h1>Notary Server {version}!</h1>
<ul>
<li>public key: <pre>{public_key}</pre></li>
<li>git commit hash: <a href="https://github.com/tlsnotary/tlsn/commit/{git_commit_hash}">{git_commit_hash}</a></li>
<li><a href="healthcheck">health check</a></li>
<li><a href="info">info</a></li>
</ul>
</body>
concurrency: 32
notarization:
max_sent_data: 4096
max_recv_data: 16384
timeout: 1800
private_key_path: "../notary/notary.key"
signature_algorithm: secp256k1
tls:
enabled: false
private_key_path: "../tls/key.pem"
certificate_path: "../tls/cert.pem"
log:
level: DEBUG
format: COMPACT
auth:
enabled: false
whitelist_path: "../auth/whitelist.csv"

View File

@@ -28,9 +28,8 @@ use tracing_subscriber::EnvFilter;
use ws_stream_tungstenite::WsStream;
use notary_server::{
read_pem_file, run_server, AuthorizationProperties, LoggingProperties, NotarizationProperties,
NotarizationSessionRequest, NotarizationSessionResponse, NotaryServerProperties,
NotarySigningKeyProperties, ServerProperties, TLSProperties,
read_pem_file, run_server, AuthorizationProperties, LogProperties, NotarizationProperties,
NotarizationSessionRequest, NotarizationSessionResponse, NotaryServerProperties, TLSProperties,
};
const MAX_SENT_DATA: usize = 1 << 13;
@@ -38,8 +37,8 @@ const MAX_RECV_DATA: usize = 1 << 13;
const NOTARY_HOST: &str = "127.0.0.1";
const NOTARY_DNS: &str = "tlsnotaryserver.io";
const NOTARY_CA_CERT_PATH: &str = "../server/fixture/tls/rootCA.crt";
const NOTARY_CA_CERT_BYTES: &[u8] = include_bytes!("../../server/fixture/tls/rootCA.crt");
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";
fn get_server_config(
@@ -49,33 +48,26 @@ fn get_server_config(
concurrency: usize,
) -> NotaryServerProperties {
NotaryServerProperties {
server: ServerProperties {
name: NOTARY_DNS.to_string(),
host: NOTARY_HOST.to_string(),
port,
html_info: "example html response".to_string(),
},
host: NOTARY_HOST.to_string(),
port,
html_info: "example html response".to_string(),
notarization: NotarizationProperties {
max_sent_data: 1 << 13,
max_recv_data: 1 << 14,
timeout: 1800,
private_key_path: Some("./fixture/notary/notary.key".to_string()),
..Default::default()
},
tls: TLSProperties {
enabled: tls_enabled,
private_key_pem_path: Some("../server/fixture/tls/notary.key".to_string()),
certificate_pem_path: Some("../server/fixture/tls/notary.crt".to_string()),
private_key_path: Some("./fixture/tls/notary.key".to_string()),
certificate_path: Some("./fixture/tls/notary.crt".to_string()),
},
notary_key: NotarySigningKeyProperties {
private_key_pem_path: "../server/fixture/notary/notary.key".to_string(),
public_key_pem_path: "../server/fixture/notary/notary.pub".to_string(),
},
logging: LoggingProperties {
level: "DEBUG".to_string(),
log: LogProperties {
..Default::default()
},
authorization: AuthorizationProperties {
auth: AuthorizationProperties {
enabled: auth_enabled,
whitelist_csv_path: Some("../server/fixture/auth/whitelist.csv".to_string()),
whitelist_path: Some("./fixture/auth/whitelist.csv".to_string()),
},
concurrency,
}
@@ -118,11 +110,11 @@ fn tcp_prover_client(notary_config: NotaryServerProperties) -> NotaryClient {
let mut notary_client_builder = NotaryClient::builder();
notary_client_builder
.host(&notary_config.server.host)
.port(notary_config.server.port)
.host(&notary_config.host)
.port(notary_config.port)
.enable_tls(false);
if notary_config.authorization.enabled {
if notary_config.auth.enabled {
notary_client_builder.api_key(API_KEY);
}
@@ -160,8 +152,8 @@ async fn tls_prover(notary_config: NotaryServerProperties) -> (NotaryConnection,
root_cert_store.add(&certificate).unwrap();
let notary_client = NotaryClient::builder()
.host(&notary_config.server.name)
.port(notary_config.server.port)
.host(NOTARY_DNS)
.port(notary_config.port)
.root_cert_store(root_cert_store)
.build()
.unwrap();
@@ -285,8 +277,8 @@ async fn test_tcp_prover<S: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
async fn test_websocket_prover() {
// Notary server configuration setup
let notary_config = setup_config_and_server(100, 7050, true, false, 100).await;
let notary_host = notary_config.server.host.clone();
let notary_port = notary_config.server.port;
let notary_host = notary_config.host.clone();
let notary_port = notary_config.port;
// Connect to the notary server via TLS-WebSocket
// Try to avoid dealing with transport layer directly to mimic the limitation of