diff --git a/Cargo.lock b/Cargo.lock index aecf850..652bc4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,9 +219,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "js-sys", @@ -1856,9 +1856,9 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "reqwest" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" +checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" dependencies = [ "base64 0.21.0", "bytes", @@ -2420,6 +2420,8 @@ dependencies = [ name = "tfclient" version = "0.1.0" dependencies = [ + "base64 0.21.0", + "chrono", "clap", "ctrlc", "dirs 5.0.0", diff --git a/api/v2/enroll.txt b/api/v2/enroll.txt index bc3a41e..afead04 100644 --- a/api/v2/enroll.txt +++ b/api/v2/enroll.txt @@ -7,6 +7,9 @@ Connection: close {"code":"xM22QsIzd4F0nLDTbh86RCSYwelfU_Hshqt-7u4yy_Y","dhPubkey":"LS0tLS1CRUdJTiBORUJVTEEgWDI1NTE5IFBVQkxJQyBLRVktLS0tLQpqZW9aaDZZYUNNSHZKK04zWGRlQ1hCbHo3dm5saTBjL1NlQ1hVR3lYbEIwPQotLS0tLUVORCBORUJVTEEgWDI1NTE5IFBVQkxJQyBLRVktLS0tLQo=","edPubkey":"LS0tLS1CRUdJTiBORUJVTEEgRUQyNTUxOSBQVUJMSUMgS0VZLS0tLS0KWHE0RG9mUGJoQzBubjc4VEhRWUxhNC83V1Ixei9iU1kzSm9pRzNRZ1VMcz0KLS0tLS1FTkQgTkVCVUxBIEVEMjU1MTkgUFVCTElDIEtFWS0tLS0tCg==","timestamp":"2023-02-01T13:24:56.380006369-05:00"} +2023-02-01T13:24:56.380006369-05:00 +%Y-%m-%dT%H:%M:%S.%f-%:z + HTTP/2 200 OK Cache-Control: no-store Content-Security-Policy: default-src 'none' diff --git a/tfclient/Cargo.toml b/tfclient/Cargo.toml index d1590d6..0639ee2 100644 --- a/tfclient/Cargo.toml +++ b/tfclient/Cargo.toml @@ -19,6 +19,9 @@ toml = "0.7.3" serde = { version = "1.0.158", features = ["derive"] } serde_json = "1.0.94" ctrlc = "3.2.5" +reqwest = { version = "0.11.16", features = ["blocking"] } +base64 = "0.21.0" +chrono = "0.4.24" [build-dependencies] serde = { version = "1.0.157", features = ["derive"] } diff --git a/tfclient/src/api.rs b/tfclient/src/api.rs new file mode 100644 index 0000000..5d5653a --- /dev/null +++ b/tfclient/src/api.rs @@ -0,0 +1,70 @@ +use std::error::Error; +use log::trace; +use reqwest::blocking::Client; +use serde::{Serialize, Deserialize}; +use url::Url; + +#[derive(Serialize, Deserialize)] +pub struct EnrollRequest { + pub code: String, + #[serde(rename = "dhPubkey")] + pub dh_pubkey: String, + #[serde(rename = "edPubkey")] + pub ed_pubkey: String, + pub timestamp: String, +} + +#[derive(Serialize, Deserialize)] +pub struct EnrollResponseMetadata {} + +#[derive(Serialize, Deserialize)] +pub struct EnrollResponseOrganization { + pub id: String, + pub name: String, +} + +#[derive(Serialize, Deserialize)] +pub struct EnrollResponseData { + pub config: String, + #[serde(rename = "hostID")] + pub host_id: String, + pub counter: i64, + #[serde(rename = "trustedKeys")] + pub trusted_keys: String, + pub organization: EnrollResponseOrganization, +} + +#[derive(Serialize, Deserialize)] +pub struct EnrollResponse { + pub data: EnrollResponseData, + pub metadata: EnrollResponseMetadata, +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum APIResponse { + Error(EnrollError), + Success(EnrollResponse) +} + +#[derive(Serialize, Deserialize)] +pub struct EnrollError { + pub errors: Vec +} +#[derive(Serialize, Deserialize)] +pub struct EnrollErrorSingular { + pub code: String, + pub message: String +} + +pub fn enroll(server: &Url, request: &EnrollRequest) -> Result> { + let endpoint = server.join("/v2/enroll")?; + let client = Client::new(); + + let text = serde_json::to_string(request)?; + + trace!("sending enroll: {}", text); + + let resp = client.post(endpoint).body(text).send()?; + Ok(resp.json()?) +} \ No newline at end of file diff --git a/tfclient/src/apiworker.rs b/tfclient/src/apiworker.rs index 1b2707e..b749fdf 100644 --- a/tfclient/src/apiworker.rs +++ b/tfclient/src/apiworker.rs @@ -1,16 +1,24 @@ use std::sync::mpsc::{Receiver, TryRecvError}; -use log::{error, info}; +use base64::Engine; +use chrono::Local; +use log::{error, info, warn}; +use url::Url; +use trifid_pki::cert::{serialize_ed25519_public, serialize_x25519_public}; use trifid_pki::ed25519_dalek::{SecretKey, SigningKey}; use trifid_pki::rand_core::OsRng; use trifid_pki::x25519_dalek::StaticSecret; +use crate::api::{APIResponse, enroll, EnrollRequest}; use crate::config::{load_cdata, save_cdata, TFClientConfig}; use crate::daemon::ThreadMessageSender; pub enum APIWorkerMessage { - Shutdown + Shutdown, + Enroll { code: String } } -pub fn apiworker_main(config: TFClientConfig, instance: String, _transmitters: ThreadMessageSender, rx: Receiver) { +pub fn apiworker_main(config: TFClientConfig, instance: String, url: String, _transmitters: ThreadMessageSender, rx: Receiver) { + let server = Url::parse(&url).unwrap(); + // Generate dhPubkey and edPubkey if it doesn't exist // Load vardata let mut vdata = match load_cdata(&instance) { @@ -58,6 +66,65 @@ pub fn apiworker_main(config: TFClientConfig, instance: String, _transmitters: T APIWorkerMessage::Shutdown => { info!("recv on command socket: shutdown, stopping"); break; + }, + APIWorkerMessage::Enroll { code } => { + info!("recv on command socket: enroll {}", code); + let mut cdata = match load_cdata(&instance) { + Ok(c) => c, + Err(e) => { + error!("error in api worker thread: {}", e); + error!("APIWorker exiting with error"); + return; + } + }; + if cdata.host_id.is_some() { + warn!("enrollment failed: already enrolled"); + continue; + } + let dh_encoded = base64::engine::general_purpose::STANDARD.encode(serialize_x25519_public(&dh_key.to_bytes())); + let ed_encoded = base64::engine::general_purpose::STANDARD.encode(serialize_ed25519_public(&ed_key.to_bytes())); + let req = EnrollRequest { + code, + dh_pubkey: dh_encoded, + ed_pubkey: ed_encoded, + timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(), + }; + let res = match enroll(&server, &req) { + Ok(res) => res, + Err(e) => { + error!("error in api worker thread: {}", e); + error!("APIWorker exiting with error"); + return; + } + }; + let resp = match res { + APIResponse::Error(e) => { + error!("error with enrollment: {}: {}", e.errors[0].code, e.errors[0].message); + continue; + } + APIResponse::Success(resp) => resp + }; + + info!("Enrolled with server. Host-ID {} config count {}", resp.data.host_id, resp.data.counter); + info!("NebulaCAPool {}, org {} {}", resp.data.trusted_keys, resp.data.organization.name, resp.data.organization.id); + info!("Config: {}", resp.data.config); + + cdata.host_id = Some(resp.data.host_id); + cdata.counter = resp.data.counter as i32; + cdata.ca_pool = Some(resp.data.trusted_keys); + cdata.org_name = Some(resp.data.organization.name); + cdata.org_id = Some(resp.data.organization.id); + cdata.config = Some(resp.data.config); + + // Save vardata + match save_cdata(&instance, cdata) { + Ok(_) => (), + Err(e) => { + error!("Error saving cdata: {}", e); + error!("APIWorker exiting with error"); + return; + } + } } } }, diff --git a/tfclient/src/config.rs b/tfclient/src/config.rs index 39be10e..1eb41fc 100644 --- a/tfclient/src/config.rs +++ b/tfclient/src/config.rs @@ -17,7 +17,12 @@ pub struct TFClientConfig { pub struct TFClientData { pub host_id: Option, pub ed_privkey: Option<[u8; 32]>, - pub dh_privkey: Option<[u8; 32]> + pub dh_privkey: Option<[u8; 32]>, + pub counter: i32, + pub ca_pool: Option, + pub org_id: Option, + pub org_name: Option, + pub config: Option } pub fn create_config(instance: &str) -> Result<(), Box> { @@ -52,7 +57,7 @@ pub fn create_cdata(instance: &str) -> Result<(), Box> { info!("Creating data directory..."); fs::create_dir_all(get_cdata_dir(instance).ok_or("Unable to load data dir")?)?; info!("Copying default data file to config directory..."); - let config = TFClientData { host_id: None, ed_privkey: None, dh_privkey: None }; + let config = TFClientData { host_id: None, ed_privkey: None, dh_privkey: None, counter: 0, ca_pool: None, org_id: None, org_name: None, config: None }; let config_str = toml::to_string(&config)?; fs::write(get_cdata_file(instance).ok_or("Unable to load data dir")?, config_str)?; Ok(()) diff --git a/tfclient/src/daemon.rs b/tfclient/src/daemon.rs index 7f16b30..052efc8 100644 --- a/tfclient/src/daemon.rs +++ b/tfclient/src/daemon.rs @@ -74,8 +74,9 @@ pub fn daemon_main(name: String, server: String) { let config_api = config.clone(); let transmitter_api = transmitter.clone(); let name_api = name.clone(); + let server_api = server.clone(); let api_thread = thread::spawn(move || { - apiworker_main(config_api, name_api, transmitter_api, rx_api); + apiworker_main(config_api, name_api, server_api,transmitter_api, rx_api); }); info!("Starting Nebula thread..."); diff --git a/tfclient/src/main.rs b/tfclient/src/main.rs index 49f95b8..5331186 100644 --- a/tfclient/src/main.rs +++ b/tfclient/src/main.rs @@ -23,6 +23,8 @@ pub mod config; pub mod service; pub mod apiworker; pub mod socketworker; +pub mod api; +pub mod socketclient; pub mod nebula_bin { include!(concat!(env!("OUT_DIR"), "/nebula.bin.rs")); @@ -37,6 +39,7 @@ use std::fs; use clap::{Parser, ArgAction, Subcommand}; use log::{error, info}; use simple_logger::SimpleLogger; +use crate::config::load_config; use crate::dirs::get_data_dir; use crate::embedded_nebula::{run_embedded_nebula, run_embedded_nebula_cert}; use crate::service::entry::{cli_install, cli_start, cli_stop, cli_uninstall}; @@ -228,7 +231,23 @@ fn main() { Commands::Run { name, server } => { daemon::daemon_main(name, server); } - Commands::Enroll { .. } => {} + Commands::Enroll { name, code } => { + info!("Loading config..."); + let config = match load_config(&name) { + Ok(cfg) => cfg, + Err(e) => { + error!("Error loading configuration: {}", e); + std::process::exit(1); + } + }; + match socketclient::enroll(&code, &config) { + Ok(_) => (), + Err(e) => { + error!("Error sending enrollment request: {}", e); + std::process::exit(1); + } + }; + } } } diff --git a/tfclient/src/socketclient.rs b/tfclient/src/socketclient.rs new file mode 100644 index 0000000..5ce1337 --- /dev/null +++ b/tfclient/src/socketclient.rs @@ -0,0 +1,58 @@ +use std::error::Error; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::net::{IpAddr, SocketAddr, TcpStream}; +use log::{error, info}; +use crate::config::TFClientConfig; +use crate::socketworker::{ctob, DisconnectReason, JSON_API_VERSION, JsonMessage}; + +pub fn enroll(code: &str, config: &TFClientConfig) -> Result<(), Box> { + info!("Connecting to local command socket..."); + let mut stream = TcpStream::connect(SocketAddr::new(IpAddr::from([127, 0, 0, 1]), config.listen_port))?; + let mut stream2 = stream.try_clone()?; + let mut reader = BufReader::new(&stream2); + + info!("Sending Hello..."); + stream.write_all(&ctob(JsonMessage::Hello { + version: JSON_API_VERSION, + }))?; + info!("Waiting for hello..."); + let msg = read_msg(&mut reader)?; + match msg { + JsonMessage::Hello { .. } => { + info!("Server sent hello, connection established") + } + JsonMessage::Goodbye { reason } => { + error!("Disconnected by server. Reason: {:?}", reason); + return Err("Disconnected by server".into()); + } + _ => { + error!("Server returned unexpected message: {:?}", msg); + error!("Sending goodbye and exiting"); + stream.write_all(&ctob(JsonMessage::Goodbye { + reason: DisconnectReason::UnexpectedMessageType, + }))?; + return Err("Unexpected message type by server".into()); + } + } + + info!("Sending enroll request..."); + stream.write_all(&ctob(JsonMessage::Enroll { + code: code.to_string(), + }))?; + + info!("Sending disconnect..."); + stream.write_all(&ctob(JsonMessage::Goodbye { + reason: DisconnectReason::Done, + }))?; + + info!("Sent enroll request to tfclient daemon. Check logs to see if the enrollment was successful."); + + Ok(()) +} + +fn read_msg(reader: &mut BufReader<&TcpStream>) -> Result> { + let mut str = String::new(); + reader.read_line(&mut str)?; + let msg: JsonMessage = serde_json::from_str(&str)?; + Ok(msg) +} \ No newline at end of file diff --git a/tfclient/src/socketworker.rs b/tfclient/src/socketworker.rs index a48c8b6..389edb8 100644 --- a/tfclient/src/socketworker.rs +++ b/tfclient/src/socketworker.rs @@ -228,6 +228,12 @@ fn senthello_handle(client: &mut Client, transmitter: &ThreadMessageSender, comm has_id: data.host_id.is_some(), id: data.host_id }))?; + }, + + JsonMessage::Enroll { code } => { + info!("Client sent enroll with code {}", code); + info!("Sending enroll request to apiworker"); + transmitter.api_thread.send(APIWorkerMessage::Enroll { code }).unwrap(); } _ => { @@ -242,7 +248,7 @@ fn senthello_handle(client: &mut Client, transmitter: &ThreadMessageSender, comm Ok(should_disconnect) } -fn ctob(command: JsonMessage) -> Vec { +pub fn ctob(command: JsonMessage) -> Vec { let command_str = serde_json::to_string(&command).unwrap() + "\n"; command_str.into_bytes() } @@ -256,7 +262,7 @@ pub const JSON_API_VERSION: i32 = 1; #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "method")] -enum JsonMessage { +pub enum JsonMessage { #[serde(rename = "hello")] Hello { version: i32 @@ -273,12 +279,16 @@ enum JsonMessage { HostID { has_id: bool, id: Option + }, + #[serde(rename = "enroll")] + Enroll { + code: String } } #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type")] -enum DisconnectReason { +pub enum DisconnectReason { #[serde(rename = "unsupported_version")] UnsupportedVersion { expected: i32, got: i32 }, #[serde(rename = "unexpected_message_type")]