trifid/dnapi-rs/src/client_blocking.rs

102 lines
4.0 KiB
Rust
Raw Normal View History

2023-03-29 18:31:07 +00:00
//! Client structs to handle communication with the Defined Networking API. This is the blocking client API - if you want async instead, set no-default-features and enable the async feature instead.
use std::error::Error;
use chrono::Local;
use log::{debug, error};
use url::Url;
use trifid_pki::cert::serialize_ed25519_public;
use crate::credentials::{Credentials, ed25519_public_keys_from_pem};
use crate::crypto::new_keys;
use crate::message::{ENROLL_ENDPOINT, EnrollRequest, EnrollResponse};
/// 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::blocking::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::blocking::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 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()?;
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()?;
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))
}
}