//! 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; /// A type alias to abstract DH private keys pub type DHPrivateKeyPEM = Vec; /// 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> { 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> { 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)) } }