async client
This commit is contained in:
parent
9bc94d3a38
commit
80e2200a6e
|
@ -86,13 +86,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.64"
|
version = "0.1.68"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
|
checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.107",
|
"syn 2.0.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2092,7 +2092,7 @@ checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.4",
|
"syn 2.0.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2388,9 +2388,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.4"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae"
|
checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dnapi-rs"
|
name = "dnapi-rs"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "A rust client for the Defined Networking API"
|
description = "A rust client for the Defined Networking API"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
//! Client structs to handle communication with the Defined Networking API. This is the async client API - if you want blocking instead, enable the blocking (or default) feature instead.
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use chrono::Local;
|
||||||
|
use log::{debug, error};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use url::Url;
|
||||||
|
use trifid_pki::cert::serialize_ed25519_public;
|
||||||
|
use trifid_pki::ed25519_dalek::{Signature, Signer, SigningKey, Verifier};
|
||||||
|
use crate::credentials::{Credentials, ed25519_public_keys_from_pem};
|
||||||
|
use crate::crypto::{new_keys, nonce};
|
||||||
|
use crate::message::{CHECK_FOR_UPDATE, CheckForUpdateResponseWrapper, DO_UPDATE, DoUpdateRequest, DoUpdateResponse, ENDPOINT_V1, ENROLL_ENDPOINT, EnrollRequest, EnrollResponse, RequestV1, RequestWrapper, SignedResponseWrapper};
|
||||||
|
|
||||||
|
/// A type alias to abstract return types
|
||||||
|
pub type NebulaConfig = Vec<u8>;
|
||||||
|
|
||||||
|
/// A type alias to abstract DH private keys
|
||||||
|
pub type DHPrivateKeyPEM = Vec<u8>;
|
||||||
|
|
||||||
|
/// A combination of persistent data and HTTP client used for communicating with the API.
|
||||||
|
pub struct Client {
|
||||||
|
http_client: reqwest::Client,
|
||||||
|
server_url: Url
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A struct containing organization metadata returned as a result of enrollment
|
||||||
|
pub struct EnrollMeta {
|
||||||
|
/// The server organization ID this node is now a member of
|
||||||
|
pub organization_id: String,
|
||||||
|
/// The server organization name this node is now a member of
|
||||||
|
pub organization_name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
/// Create a new `Client` configured with the given User-Agent and API base.
|
||||||
|
/// # Errors
|
||||||
|
/// This function will return an error if the reqwest Client could not be created.
|
||||||
|
pub fn new(user_agent: String, api_base: Url) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::builder().user_agent(user_agent).build()?;
|
||||||
|
Ok(Self {
|
||||||
|
http_client: client,
|
||||||
|
server_url: api_base
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issues an enrollment request against the REST API using the given enrollment code, passing along a
|
||||||
|
/// locally generated DH X25519 Nebula key to be signed by the CA, and an Ed25519 key for future API
|
||||||
|
/// authentication. On success it returns the Nebula config generated by the server, a Nebula private key PEM,
|
||||||
|
/// credentials to be used for future DN API requests, and an object containing organization information.
|
||||||
|
/// # Errors
|
||||||
|
/// This function will return an error in any of the following situations:
|
||||||
|
/// - the `server_url` is invalid
|
||||||
|
/// - the HTTP request fails
|
||||||
|
/// - the HTTP response is missing X-Request-ID
|
||||||
|
/// - X-Request-ID isn't valid UTF-8
|
||||||
|
/// - the server returns an error
|
||||||
|
/// - the server returns invalid JSON
|
||||||
|
/// - the `trusted_keys` field is invalid
|
||||||
|
pub async fn enroll(&self, code: &str) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials, EnrollMeta), Box<dyn Error>> {
|
||||||
|
debug!("making enrollment request to API {{server: {}, code: {}}}", self.server_url, code);
|
||||||
|
|
||||||
|
let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
|
||||||
|
|
||||||
|
let req_json = serde_json::to_string(&EnrollRequest {
|
||||||
|
code: code.to_string(),
|
||||||
|
dh_pubkey: dh_pubkey_pem,
|
||||||
|
ed_pubkey: serialize_ed25519_public(ed_pubkey.as_bytes()),
|
||||||
|
timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let resp = self.http_client.post(self.server_url.join(ENROLL_ENDPOINT)?).body(req_json).send().await?;
|
||||||
|
|
||||||
|
let req_id = resp.headers().get("X-Request-ID").ok_or("Response missing X-Request-ID")?.to_str()?;
|
||||||
|
debug!("enrollment request complete {{req_id: {}}}", req_id);
|
||||||
|
|
||||||
|
let resp: EnrollResponse = resp.json().await?;
|
||||||
|
|
||||||
|
let r = match resp {
|
||||||
|
EnrollResponse::Success { data } => data,
|
||||||
|
EnrollResponse::Error { errors } => {
|
||||||
|
error!("unexpected error during enrollment: {}", errors[0].message);
|
||||||
|
return Err(errors[0].message.clone().into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = EnrollMeta {
|
||||||
|
organization_id: r.organization.id,
|
||||||
|
organization_name: r.organization.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
let trusted_keys = ed25519_public_keys_from_pem(&r.trusted_keys)?;
|
||||||
|
|
||||||
|
let creds = Credentials {
|
||||||
|
host_id: r.host_id,
|
||||||
|
ed_privkey,
|
||||||
|
counter: r.counter,
|
||||||
|
trusted_keys,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((r.config, dh_privkey_pem, creds, meta))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a signed message to the `DNClient` API to learn if there is a new configuration available.
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if the dnclient request fails, or the server returns invalid data.
|
||||||
|
pub async fn check_for_update(&self, creds: &Credentials) -> Result<bool, Box<dyn Error>> {
|
||||||
|
let body = self.post_dnclient(CHECK_FOR_UPDATE, &[], &creds.host_id, creds.counter, &creds.ed_privkey).await?;
|
||||||
|
|
||||||
|
let result: CheckForUpdateResponseWrapper = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
|
Ok(result.data.update_available)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a signed message to the `DNClient` API to fetch the new configuration update. During this call a new
|
||||||
|
/// DH X25519 keypair is generated for the new Nebula certificate as well as a new Ed25519 keypair for `DNClient` API
|
||||||
|
/// communication. On success it returns the new config, a Nebula private key PEM to be inserted into the config
|
||||||
|
/// and new `DNClient` API credentials
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error in any of the following scenarios:
|
||||||
|
/// - if the message could not be serialized
|
||||||
|
/// - if the request fails
|
||||||
|
/// - if the response could not be deserialized
|
||||||
|
/// - if the signature is invalid
|
||||||
|
/// - if the keys are invalid
|
||||||
|
pub async fn do_update(&self, creds: &Credentials) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials), Box<dyn Error>> {
|
||||||
|
let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
|
||||||
|
|
||||||
|
let update_keys = DoUpdateRequest {
|
||||||
|
ed_pubkey_pem: serialize_ed25519_public(ed_pubkey.as_bytes()),
|
||||||
|
dh_pubkey_pem,
|
||||||
|
nonce: nonce().to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let update_keys_blob = serde_json::to_vec(&update_keys)?;
|
||||||
|
|
||||||
|
let resp = self.post_dnclient(DO_UPDATE, &update_keys_blob, &creds.host_id, creds.counter, &creds.ed_privkey).await?;
|
||||||
|
|
||||||
|
let result_wrapper: SignedResponseWrapper = serde_json::from_slice(&resp)?;
|
||||||
|
|
||||||
|
let mut valid = false;
|
||||||
|
|
||||||
|
for ca_pubkey in &creds.trusted_keys {
|
||||||
|
if ca_pubkey.verify(&result_wrapper.data.message, &Signature::from_slice(&result_wrapper.data.signature)?).is_ok() {
|
||||||
|
valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return Err("Failed to verify signed API result".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: DoUpdateResponse = serde_json::from_slice(&result_wrapper.data.message)?;
|
||||||
|
|
||||||
|
if result.nonce != update_keys.nonce {
|
||||||
|
error!("nonce mismatch between request {:x?} and response {:x?}", result.nonce, update_keys.nonce);
|
||||||
|
return Err("nonce mismatch between request and response".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
let trusted_keys = ed25519_public_keys_from_pem(&result.trusted_keys)?;
|
||||||
|
|
||||||
|
let new_creds = Credentials {
|
||||||
|
host_id: creds.host_id.clone(),
|
||||||
|
ed_privkey,
|
||||||
|
counter: result.counter,
|
||||||
|
trusted_keys,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((result.config, dh_privkey_pem, new_creds))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps and signs the given `req_type` and value, and then makes the API call.
|
||||||
|
/// On success, returns the response body.
|
||||||
|
/// # Errors
|
||||||
|
/// This function will return an error if:
|
||||||
|
/// - serialization in any step fails
|
||||||
|
/// - if the `server_url` is invalid
|
||||||
|
/// - if the request could not be sent
|
||||||
|
pub async fn post_dnclient(&self, req_type: &str, value: &[u8], host_id: &str, counter: u32, ed_privkey: &SigningKey) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
|
let encoded_msg = serde_json::to_string(&RequestWrapper {
|
||||||
|
message_type: req_type.to_string(),
|
||||||
|
value: value.to_vec(),
|
||||||
|
timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
|
||||||
|
})?;
|
||||||
|
let encoded_msg_bytes = encoded_msg.into_bytes();
|
||||||
|
let signature = ed_privkey.sign(&encoded_msg_bytes).to_vec();
|
||||||
|
let body = RequestV1 {
|
||||||
|
version: 1,
|
||||||
|
host_id: host_id.to_string(),
|
||||||
|
counter,
|
||||||
|
message: encoded_msg_bytes,
|
||||||
|
signature,
|
||||||
|
};
|
||||||
|
|
||||||
|
let post_body = serde_json::to_string(&body)?;
|
||||||
|
|
||||||
|
let resp = self.http_client.post(self.server_url.join(ENDPOINT_V1)?).body(post_body).send().await?;
|
||||||
|
|
||||||
|
match resp.status() {
|
||||||
|
StatusCode::OK => {
|
||||||
|
Ok(resp.bytes().await?.to_vec())
|
||||||
|
},
|
||||||
|
StatusCode::FORBIDDEN => {
|
||||||
|
Err("Forbidden".into())
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
error!("dnclient endpoint returned bad status code {}", resp.status());
|
||||||
|
Err("dnclient endpoint returned error".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ base64 = "0.21.0"
|
||||||
chrono = "0.4.24"
|
chrono = "0.4.24"
|
||||||
ipnet = "2.7.1"
|
ipnet = "2.7.1"
|
||||||
base64-serde = "0.7.0"
|
base64-serde = "0.7.0"
|
||||||
dnapi-rs = { version = "0.1.0", path = "../dnapi-rs" }
|
dnapi-rs = { version = "0.1.1", path = "../dnapi-rs" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
serde = { version = "1.0.157", features = ["derive"] }
|
serde = { version = "1.0.157", features = ["derive"] }
|
||||||
|
|
|
@ -3,12 +3,10 @@ use base64::Engine;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use trifid_pki::ca::NebulaCAPool;
|
|
||||||
use trifid_pki::cert::{serialize_ed25519_public, serialize_x25519_public};
|
use trifid_pki::cert::{serialize_ed25519_public, serialize_x25519_public};
|
||||||
use trifid_pki::ed25519_dalek::{SecretKey, SigningKey};
|
use trifid_pki::ed25519_dalek::{SecretKey, SigningKey};
|
||||||
use trifid_pki::rand_core::OsRng;
|
use trifid_pki::rand_core::OsRng;
|
||||||
use trifid_pki::x25519_dalek::StaticSecret;
|
use trifid_pki::x25519_dalek::StaticSecret;
|
||||||
use crate::message::{APIResponse, enroll, EnrollRequest};
|
|
||||||
use crate::config::{load_cdata, save_cdata, TFClientConfig};
|
use crate::config::{load_cdata, save_cdata, TFClientConfig};
|
||||||
use crate::daemon::ThreadMessageSender;
|
use crate::daemon::ThreadMessageSender;
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ use std::fs;
|
||||||
|
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use dnapi_rs::credentials::Credentials;
|
||||||
|
|
||||||
use crate::dirs::{get_cdata_dir, get_cdata_file, get_config_dir, get_config_file};
|
use crate::dirs::{get_cdata_dir, get_cdata_file, get_config_dir, get_config_file};
|
||||||
|
|
||||||
|
@ -20,13 +21,8 @@ pub struct TFClientConfig {
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct TFClientData {
|
pub struct TFClientData {
|
||||||
pub host_id: Option<String>,
|
pub host_id: Option<String>,
|
||||||
pub ed_privkey: Option<[u8; 32]>,
|
|
||||||
pub dh_privkey: Option<[u8; 32]>,
|
pub dh_privkey: Option<[u8; 32]>,
|
||||||
pub counter: i32,
|
pub creds: Option<Credentials>
|
||||||
pub key_pool: Option<String>,
|
|
||||||
pub org_id: Option<String>,
|
|
||||||
pub org_name: Option<String>,
|
|
||||||
pub config: Option<String>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_config(instance: &str) -> Result<(), Box<dyn Error>> {
|
pub fn create_config(instance: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
|
Loading…
Reference in New Issue