Skip to content

Commit

Permalink
Signatures for exit root server
Browse files Browse the repository at this point in the history
The exit root server now signs and encrypts exit server lists to
send back to clients. Contracts that have been processed already
are added to a cache which is automatically updated every 5 minutes
from the rpc server with the current list of exits.
  • Loading branch information
ch-iara committed Oct 8, 2024
1 parent 3ee30d8 commit 118127f
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test.db-journal

/scripts/cross_building/staging_dir/
/scripts/cross_building/staging/
/scripts/exit_trust_root
legacy-integration-tests/private-key*
legacy-integration-tests/deps/*
legacy-integration-tests/*
Expand Down
17 changes: 16 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions althea_types/src/exits/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::default_system_chain;
use crate::wg_key::WgKey;
use crate::{exits::identity::ExitIdentity, Identity, SystemChain};
use crypto_box::PublicKey;
use ipnetwork::IpNetwork;
use serde::Deserialize;
use serde::Serialize;
Expand Down Expand Up @@ -159,3 +160,11 @@ pub struct ExitClientDetails {
pub client_internal_ip: IpAddr,
pub internet_ipv6_subnet: Option<IpNetwork>,
}

/// Wrapper for secure box containing a Signed Exit Server List
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
pub struct EncryptedExitServerList {
pub pubkey: WgKey,
pub nonce: [u8; 24],
pub encrypted_exit_server_list: Vec<u8>,
}
11 changes: 11 additions & 0 deletions exit_trust_root/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ log = "0.4"
clarity = "1.4"
web30 = "1.4"
crypto_box = "0.9"
lazy_static = "1.5"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
toml = "0.5"
rita_client_registration = { path = "../rita_client_registration" }
# we don't call or us OpenSSL directly in this codebase, but by adding
# this dependency with this feature we can enforce that openssl is compiled
# in 'vendored' mode all the way down the tree. What this means is that we use
# an openssl implementation from the crate and not from the system, simplifying
# our build process for a lot of cross-compile situations
openssl = { version = "0.10", features = ["vendored"] }

[features]
development = []
5 changes: 5 additions & 0 deletions exit_trust_root/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[clarity_private_key]
'0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f1e'

[wg_private_key]
'0NZdyxzjnMEyEMn2PA+oal+nEbkp/sK1xC486Tqys3E='
146 changes: 115 additions & 31 deletions exit_trust_root/src/bin.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
use actix_web::rt::System;
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
use althea_types::{ExitServerList, SignedExitServerList};
use clarity::{Address, PrivateKey};
use althea_types::{EncryptedExitServerList, ExitServerList, SignedExitServerList};
use clarity::Address;
use config::{load_config, CONFIG};
use crypto_box::aead::{Aead, AeadCore, OsRng};
use crypto_box::{PublicKey, SalsaBox, SecretKey};
use env_logger::Env;
use lazy_static::lazy_static;
use log::info;
use rita_client_registration::client_db::get_exits_list;
use rustls::ServerConfig;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
use tls::{load_certs, load_clarity_private_key, load_rustls_private_key};
use tls::{load_certs, load_rustls_private_key};
use web30::client::Web3;
use web30::jsonrpc::error::Web3Error;

pub mod config;
pub mod tls;
Expand All @@ -25,58 +33,132 @@ pub const DOMAIN: &str = if cfg!(test) || DEVELOPMENT {
/// The backend RPC port for the info server fucntions implemented in this repo
const SERVER_PORT: u16 = 9000;

lazy_static! {
static ref EXIT_CONTRACT_CACHE: Arc<RwLock<HashMap<Address, ExitContractSignatureCacheValue>>> =
Arc::new(RwLock::new(HashMap::new()));
}

/// This endpoint retrieves and signs the data from any specified exit contract,
/// allowing this server to serve as a root of trust for several different exit contracts.
#[get("/{exit_contract}")]
async fn return_exit_contract_data(
exit_contract: web::Path<Address>,
// also- why are we being passed a cache here? this should come from our side
cache: web::Data<Arc<RwLock<HashMap<Address, ExitContractSignatureCacheValue>>>>,
pubkey: web::Data<[u8; 32]>,
) -> impl Responder {
match cache.read().unwrap().get(&exit_contract.into_inner()) {
let contract = exit_contract.into_inner();
let cache = EXIT_CONTRACT_CACHE.read().unwrap().clone();
match cache.get(&contract) {
Some(cache) => {
// is the idea here to return the data we would be populating in signature_update_loop?
// all that would save is time grabbing the private key from file- since the match in this
// fn in based on the same cache
HttpResponse::Ok().json(cache.to_encrypted_exit_server_list(load_clarity_private_key()))
// return an encrypted exit server list based on the given key
HttpResponse::Ok().json(cache.to_encrypted_exit_server_list((*pubkey.get_ref()).into()))
}
None => {
todo!()
// no data in cache for this exit contract, we need to retrieve it from rpc server, sign it,
// add it to the cache and return it
match retrieve_exit_server_list(contract).await {
Ok(cache_value) => {
// encrypt and return
return HttpResponse::Ok().json(
cache_value.to_encrypted_exit_server_list((*pubkey.get_ref()).into()),
);
}
Err(e) => {
info!("Failed to get exit list from contract {:?}", e);
HttpResponse::InternalServerError()
.json("Failed to get exit list from contract")
}
}
}
}
}

async fn retrieve_exit_server_list(
exit_contract: Address,
) -> Result<ExitContractSignatureCacheValue, Web3Error> {
const WEB3_TIMEOUT: Duration = Duration::from_secs(10);
let exits = get_exits_list(
&Web3::new("https://dai.althea.net", WEB3_TIMEOUT),
CONFIG.clarity_private_key.to_address(),
exit_contract,
)
.await;
match exits {
Ok(exits) => {
info!("Got exit list from contract");
let exit_list = ExitServerList {
contract: exit_contract,
exit_list: exits,
created: std::time::SystemTime::now(),
};
let nonce = SalsaBox::generate_nonce(&mut OsRng);
let cache_value = ExitContractSignatureCacheValue {
exit_list: exit_list.sign(CONFIG.clarity_private_key),
nonce: nonce.into(),
};
// add this new exit to the cache
EXIT_CONTRACT_CACHE
.write()
.unwrap()
.insert(exit_contract, cache_value.clone());
Ok(cache_value)
}
Err(e) => {
info!("Failed to get exit list from contract {:?}", e);
Err(e)
}
}
}

/// Cache struct for the exit contract signature data
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
struct ExitContractSignatureCacheValue {
exit_list: ExitServerList,
signature: Vec<u8>,
exit_list: SignedExitServerList,
nonce: [u8; 24],
}

impl ExitContractSignatureCacheValue {
fn to_encrypted_exit_server_list(&self, our_privkey: PrivateKey) -> SignedExitServerList {
self.exit_list.sign(our_privkey)
fn to_encrypted_exit_server_list(&self, their_pubkey: PublicKey) -> EncryptedExitServerList {
// we already have a signed list- now to encrypt it given the nonce & our... keys...
let plaintext = serde_json::to_string(&self.exit_list.data)
.expect("Failed to serialize ExitServerList")
.into_bytes();
// using the clarity private key as the Crypto_box SecretKey
let our_secretkey = SecretKey::from(CONFIG.clarity_private_key.to_bytes());
let b = SalsaBox::new(&their_pubkey, &our_secretkey);
let ciphertext = b.encrypt((&self.nonce).into(), plaintext.as_ref()).unwrap();
EncryptedExitServerList {
pubkey: CONFIG.wg_private_key,
nonce: self.nonce,
encrypted_exit_server_list: ciphertext,
}
}
}

const CACHE_TIMEOUT: Duration = Duration::from_secs(600);
// five minutes
const SIGNATURE_UPDATE_SLEEP: Duration = Duration::from_secs(300);

/// In order to improve scalability this loop grabs and signs an updated list of exits from each exit contract
/// that has previously been requested from this server every 5 minutes. This allows the server to return instantly
/// on the next request from the client without having to perform rpc query 1-1 with requests.
fn signature_update_loop(cache: Arc<RwLock<HashMap<Address, ExitContractSignatureCacheValue>>>) {
// where does privkey come from? exit root server must have its own key preset
let our_privkey = load_clarity_private_key();
fn signature_update_loop() {
thread::spawn(move || loop {
let cache = cache.write().unwrap();
for (_exit_contract, cache) in cache.iter() {
cache.to_encrypted_exit_server_list(our_privkey);
// do nothing with these results- does this get saved to a second cache?
// todo: possibly remove this loop
}
let runner = System::new();
runner.block_on(async move {
let cache = EXIT_CONTRACT_CACHE.read().unwrap().clone();
for (exit_contract, _value) in cache.iter() {
// get the latest exit list from the contract
match retrieve_exit_server_list(*exit_contract).await {
// grab the cache here so we don't lock it while awaiting for every single contract
Ok(cache_value) => {
let mut cache = EXIT_CONTRACT_CACHE.write().unwrap();
// update the cache
cache.insert(*exit_contract, cache_value);
}
Err(e) => {
info!("Failed to get exit list from contract {:?}", e);
}
}
}
});
thread::sleep(SIGNATURE_UPDATE_SLEEP);
});
}

Expand All @@ -85,10 +167,12 @@ async fn main() -> std::io::Result<()> {
openssl_probe::init_ssl_cert_env_vars();
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();

let exit_contract_data_cache: Arc<RwLock<HashMap<Address, ExitContractSignatureCacheValue>>> =
Arc::new(RwLock::new(HashMap::new()));
signature_update_loop(exit_contract_data_cache.clone());
let web_data = web::Data::new(exit_contract_data_cache.clone());
// ensure that the config file is valid, we discard the result and use
// lazy static variable after this
load_config();

signature_update_loop();
let web_data = web::Data::new(EXIT_CONTRACT_CACHE.read().unwrap().clone());

let server = HttpServer::new(move || {
App::new()
Expand Down
58 changes: 58 additions & 0 deletions exit_trust_root/src/config.rs
Original file line number Diff line number Diff line change
@@ -1 +1,59 @@
use std::{fs::File, io::Read};
use lazy_static::lazy_static;
use althea_types::WgKey;
use clarity::PrivateKey;
use log::error;
use serde::{Deserialize, Serialize};

use crate::DEVELOPMENT;

///Struct containing settings for Exit root server
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ConfigStruct {
pub clarity_private_key: PrivateKey,
pub wg_private_key: WgKey,
}

impl ConfigStruct {
pub fn load(path: String) -> Option<ConfigStruct> {
let mut config_toml = String::new();

let mut file = match File::open(path) {
Ok(file) => file,
Err(_) => {
error!("Could not find config file. Using default!");
return None;
}
};

file.read_to_string(&mut config_toml)
.unwrap_or_else(|err| panic!("Error while reading config: [{}]", err));

let res = toml::from_str(&config_toml).unwrap();
Some(res)
}
}

/// loads the exit root server config, broken out here so that
/// we can easily verify that the config is valid before starting
pub fn load_config() -> ConfigStruct {
// change the config name based on our development status
let file_name = if DEVELOPMENT || cfg!(test) {
"config.toml"
} else {
"/etc/exit_root_server.toml"
};
let config_structs = ConfigStruct::load(file_name.to_string());
if let Some(conf) = config_structs {
conf
} else {
panic!(
"Can not find configuration file! for filename {:?}",
file_name
);
}
}

lazy_static! {
pub static ref CONFIG: ConfigStruct = load_config();
}
4 changes: 2 additions & 2 deletions scripts/build-and-deploy-exit-root-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ rm ../scripts/exit_trust_root
set -e
# set for target cpu of target machine
cross build --target x86_64-unknown-linux-musl -p exit_trust_root --bin exit_trust_root
cp target/x86_64-unknown-linux-musl/exit_trust_root ../scripts
cp ../target/x86_64-unknown-linux-musl/debug/exit_trust_root ../scripts
popd

pushd $DIR
ansible-playbook -i hosts deploy-exit-trust-root.yml
ansible-playbook -i hosts deploy-exit-root-server.yml
popd

0 comments on commit 118127f

Please sign in to comment.