diff --git a/Cargo.lock b/Cargo.lock index 375dbef..4bcaaa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -619,6 +619,20 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "dnapi-rs" +version = "0.1.0" +dependencies = [ + "base64 0.21.0", + "base64-serde", + "log", + "reqwest", + "serde", + "serde_json", + "trifid-pki", + "url", +] + [[package]] name = "dotenvy" version = "0.15.6" @@ -2061,18 +2075,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.158" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.158" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", @@ -2081,9 +2095,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" dependencies = [ "itoa", "ryu", @@ -2436,6 +2450,7 @@ dependencies = [ "clap", "ctrlc", "dirs 5.0.0", + "dnapi-rs", "flate2", "hex", "ipnet", diff --git a/Cargo.toml b/Cargo.toml index 1ad3cfa..31bce56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "trifid-api", "tfclient", - "trifid-pki" + "trifid-pki", + "dnapi-rs" ] \ No newline at end of file diff --git a/dnapi-rs/Cargo.toml b/dnapi-rs/Cargo.toml new file mode 100644 index 0000000..8f4b645 --- /dev/null +++ b/dnapi-rs/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "dnapi-rs" +version = "0.1.0" +edition = "2021" +description = "A rust client for the Defined Networking API" +license = "AGPL-3.0-or-later" +documentation = "https://docs.rs/dnapi-rs" +homepage = "https://git.e3t.cc/~core/trifid" +repository = "https://git.e3t.cc/~core/trifid" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.159", features = ["derive"] } +base64-serde = "0.7.0" +log = "0.4.17" +reqwest = { version = "0.11.16", features = ["blocking", "json"] } +url = "2.3.1" +base64 = "0.21.0" +serde_json = "1.0.95" +trifid-pki = { version = "0.1.6", path = "../trifid-pki" } \ No newline at end of file diff --git a/dnapi-rs/src/client.rs b/dnapi-rs/src/client.rs new file mode 100644 index 0000000..4dc1f83 --- /dev/null +++ b/dnapi-rs/src/client.rs @@ -0,0 +1 @@ +//! Client structs to handle communication with the Defined Networking API. \ No newline at end of file diff --git a/dnapi-rs/src/credentials.rs b/dnapi-rs/src/credentials.rs new file mode 100644 index 0000000..4d55ad4 --- /dev/null +++ b/dnapi-rs/src/credentials.rs @@ -0,0 +1,38 @@ +//! Contains the `Credentials` struct, which contains all keys, IDs, organizations and other identity-related and security-related data that is persistent in a `Client` + +use std::error::Error; +use trifid_pki::cert::{deserialize_ed25519_public, serialize_ed25519_public}; +use trifid_pki::ed25519_dalek::{SigningKey, VerifyingKey}; + +/// Contains information necessary to make requests against the `DNClient` API. +pub struct Credentials { + /// The assigned Host ID that this client represents + pub host_id: String, + /// The ed25519 private key used to sign requests against the API + pub ed_privkey: SigningKey, + /// The counter used in the other API requests. It is unknown what the purpose of this is, but the original client persists it and it is needed for API calls. + pub counter: u32, + /// The set of trusted ed25519 keys that may be used by the API to sign API responses. + pub trusted_keys: Vec +} + +/// Converts an array of `VerifyingKey`s to a singular bundle of PEM-encoded keys +pub fn ed25519_public_keys_to_pem(keys: &[VerifyingKey]) -> Vec { + let mut res = vec![]; + + for key in keys { + res.append(&mut serialize_ed25519_public(&key.to_bytes())); + } + + res +} + +pub fn ed25519_public_keys_from_pem(pem: Vec) -> Result, Box> { + let mut keys = vec![]; + + for key in keys.chunks(32) { + + } + + Ok(keys) +} \ No newline at end of file diff --git a/dnapi-rs/src/lib.rs b/dnapi-rs/src/lib.rs new file mode 100644 index 0000000..3cc1117 --- /dev/null +++ b/dnapi-rs/src/lib.rs @@ -0,0 +1,20 @@ +//! # dnapi-rs +//! **dnapi-rs** is a Rust-native crate for interacting with the Defined Networking client API. It is a direct port of `dnapi`, an officially maintained API client by Defined Networking. +//! +//! This crate is maintained as a part of the trifid project. Check out the other crates in [the git repository](https://git.e3t.cc/~core/trifid). + +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] +#![deny(missing_docs)] +#![deny(clippy::missing_errors_doc)] +#![deny(clippy::missing_panics_doc)] +#![deny(clippy::missing_safety_doc)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::module_name_repetitions)] + +pub mod message; +pub mod client; +pub mod credentials; \ No newline at end of file diff --git a/dnapi-rs/src/message.rs b/dnapi-rs/src/message.rs new file mode 100644 index 0000000..e52140e --- /dev/null +++ b/dnapi-rs/src/message.rs @@ -0,0 +1,197 @@ +//! Models for interacting with the Defined Networking API. + +use base64_serde::base64_serde_type; +use serde::{Serialize, Deserialize}; + +/// The version 1 `DNClient` API endpoint +const ENDPOINT_V1: &str = "/v1/dnclient"; + +base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD); + +#[derive(Serialize, Deserialize)] +/// `RequestV1` is the version 1 `DNClient` request message. +pub struct RequestV1 { + /// Version is always 1 + pub version: i32, + #[serde(rename = "hostID")] + /// The Host ID of this dnclient instance + pub host_id: String, + /// The counter last returned by the server + pub counter: u32, + #[serde(with = "Base64Standard")] + /// A base64-encoded message + pub message: Vec, + #[serde(with = "Base64Standard")] + /// An ed25519 signature over the `message`, which can be verified with the host's previously enrolled ed25519 public key + pub signature: Vec +} + +#[derive(Serialize, Deserialize)] +/// `RequestWrapper` wraps a `DNClient` request message. It consists of a +/// type and value, with the type string indicating how to interpret the value blob. +pub struct RequestWrapper { + #[serde(rename = "type")] + /// The type of the message. Used to determine how `value` is encoded + pub message_type: String, + #[serde(with = "Base64Standard")] + /// A base64-encoded arbitrary message, the type of which is stated in `message_type` + pub value: Vec, + /// The timestamp of when this message was sent. Follows the format `%Y-%m-%dT%H:%M:%S.%f%:z`, or: + /// <4-digit year>--T::. + /// For example: + /// `2023-03-29T09:56:42.380006369-04:00` + /// would represent `29 March 03, 2023, 09:56:42.380006369 UTC-4` + pub timestamp: String +} + +#[derive(Serialize, Deserialize)] +/// `SignedResponseWrapper` contains a response message and a signature to validate inside `data`. +pub struct SignedResponseWrapper { + /// The response data contained in this message + pub data: SignedResponse +} + +#[derive(Serialize, Deserialize)] +/// `SignedResponse` contains a response message and a signature to validate. +pub struct SignedResponse { + /// The API version - always 1 + pub version: i32, + #[serde(with = "Base64Standard")] + /// The Base64-encoded message signed inside this message + pub message: Vec, + #[serde(with = "Base64Standard")] + /// The ed25519 signature over the `message` + pub signature: Vec +} + +#[derive(Serialize, Deserialize)] +/// `CheckForUpdateResponseWrapper` contains a response to `CheckForUpdate` inside "data." +pub struct CheckForUpdateResponseWrapper { + /// The response data contained in this message + pub data: CheckForUpdateResponse +} + +#[derive(Serialize, Deserialize)] +/// `CheckForUpdateResponse` is the response generated for a `CheckForUpdate` request. +pub struct CheckForUpdateResponse { + #[serde(rename = "updateAvailable")] + /// Set to true if a config update is available + pub update_available: bool +} + +#[derive(Serialize, Deserialize)] +/// `DoUpdateRequest` is the request sent for a `DoUpdate` request. +pub struct DoUpdateRequest { + #[serde(rename = "edPubkeyPEM")] + #[serde(with = "Base64Standard")] + /// The new ed25519 public key that should be used for future API requests + pub ed_pubkey_pem: Vec, + #[serde(rename = "dhPubkeyPEM")] + #[serde(with = "Base64Standard")] + /// The new ECDH public key that the Nebula certificate should be signed for + pub dh_pubkey_pem: Vec, + #[serde(with = "Base64Standard")] + /// A randomized value used to uniquely identify this request. + /// The original client uses a randomized, 16-byte value here, which dnapi-rs replicates + pub nonce: Vec +} + +#[derive(Serialize, Deserialize)] +/// A server response to a `DoUpdateRequest`, with the updated config and key information +pub struct DoUpdateResponse { + #[serde(with = "Base64Standard")] + /// The base64-encoded Nebula config. It does **NOT** have a private-key, which must be inserted explicitly before Nebula can be ran + pub config: Vec, + /// The new config counter. It is unknown what the purpose of this is, but the original client keeps track of it and it is used later in the api + pub counter: u32, + #[serde(with = "Base64Standard")] + /// The same base64-encoded nonce that was sent in the `DoUpdateRequest`. + pub nonce: Vec, + #[serde(rename = "trustedKeys")] + #[serde(with = "Base64Standard")] + /// A new set of trusted ed25519 keys that can be used by the server to sign messages. + pub trusted_keys: Vec +} + +/// The REST enrollment endpoint +const ENROLL_ENDPOINT: &str = "/v2/enroll"; + +#[derive(Serialize, Deserialize)] +/// `EnrollRequest` is issued to the `ENROLL_ENDPOINT` to enroll this `dnclient` with a dnapi organization +pub struct EnrollRequest { + /// The enrollment code given by the API server. + pub code: String, + #[serde(rename = "dhPubkey")] + #[serde(with = "Base64Standard")] + /// The ECDH public-key that should be used to sign the Nebula certificate given to this node. + pub dh_pubkey: Vec, + #[serde(rename = "edPubkey")] + #[serde(with = "Base64Standard")] + /// The Ed25519 public-key that this node will use to sign messages sent to the API. + pub ed_pubkey: Vec, + /// The timestamp of when this request was sent. Follows the format `%Y-%m-%dT%H:%M:%S.%f%:z`, or: + /// <4-digit year>--T::. + /// For example: + /// `2023-03-29T09:56:42.380006369-04:00` + /// would represent `29 March 03, 2023, 09:56:42.380006369 UTC-4` + pub timestamp: String +} + + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +/// The response to an `EnrollRequest` +pub enum EnrollResponse { + /// A successful enrollment, with a `data` field pointing to an `EnrollResponseData` + Success { + /// The response data from this response + data: EnrollResponseData + }, + /// An unsuccessful enrollment, with an `errors` field pointing to an array of `APIError`s. + Error { + /// A list of `APIError`s that happened while trying to enroll. `APIErrors` is a type alias to `Vec` + errors: APIErrors + } +} + +#[derive(Serialize, Deserialize)] +/// The data included in an successful enrollment. +pub struct EnrollResponseData { + #[serde(with = "Base64Standard")] + /// The base64-encoded Nebula config. It does **NOT** have a private-key, which must be inserted explicitly before Nebula can be ran + pub config: Vec, + #[serde(rename = "hostID")] + /// The server-side Host ID that this node now has. + pub host_id: String, + /// The new config counter. It is unknown what the purpose of this is, but the original client keeps track of it and it is used later in the api + pub counter: u32, + #[serde(rename = "trustedKeys")] + #[serde(with = "Base64Standard")] + /// A new set of trusted ed25519 keys that can be used by the server to sign messages. + pub trusted_keys: Vec, + /// The organization data that this node is now a part of + pub organization: EnrollResponseDataOrg +} + +#[derive(Serialize, Deserialize)] +/// The organization data that this node is now a part of +pub struct EnrollResponseDataOrg { + /// The organization ID that this node is now a part of + pub id: String, + /// The name of the organization that this node is now a part of + pub name: String +} + +#[derive(Serialize, Deserialize)] +/// `APIError` represents a single error returned in an API error response. +pub struct APIError { + /// The error code + pub code: String, + /// The human-readable error message + pub message: String, + /// An optional path to where the error occured + pub path: Option +} + +/// A type alias to a array of `APIErrors`. Just for parity with dnapi. +pub type APIErrors = Vec; \ No newline at end of file diff --git a/tfclient/Cargo.toml b/tfclient/Cargo.toml index df081aa..56fa277 100644 --- a/tfclient/Cargo.toml +++ b/tfclient/Cargo.toml @@ -24,6 +24,7 @@ base64 = "0.21.0" chrono = "0.4.24" ipnet = "2.7.1" base64-serde = "0.7.0" +dnapi-rs = { version = "0.1.0", path = "../dnapi-rs" } [build-dependencies] serde = { version = "1.0.157", features = ["derive"] } diff --git a/tfclient/src/api.rs b/tfclient/src/api.rs deleted file mode 100644 index fd3e77e..0000000 --- a/tfclient/src/api.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::error::Error; -use base64_serde::base64_serde_type; -use log::trace; -use reqwest::blocking::Client; -use serde::{Serialize, Deserialize}; -use url::Url; - -const ENDPOINT_V1: &str = "/v1/dnclient"; - -base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD); - -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()?) -} - -#[derive(Serialize, Deserialize)] -pub struct RequestV1 { - pub version: i32, - #[serde(rename = "hostID")] - pub host_id: String, - pub counter: u32, - pub message: String, - #[serde(with = "Base64Standard")] - pub signature: Vec -} - -#[derive(Serialize, Deserialize)] -pub struct RequestWrapper { - #[serde(rename = "type")] - pub message_type: String, - #[serde(with = "Base64Standard")] - pub value: Vec, - pub timestamp: String -} - -#[derive(Serialize, Deserialize)] -pub struct SignedResponseWrapper { - pub data: SignedResponse -} - -#[derive(Serialize, Deserialize)] -pub struct SignedResponse { - pub version: i32, - #[serde(with = "Base64Standard")] - pub message: Vec, - #[serde(with = "Base64Standard")] - pub signature: Vec -} - -#[derive(Serialize, Deserialize)] -pub struct CheckForUpdateResponseWrapper { - pub data: CheckForUpdateResponse -} - -#[derive(Serialize, Deserialize)] -pub struct CheckForUpdateResponse { - #[serde(rename = "updateAvailable")] - pub update_available: bool -} - -#[derive(Serialize, Deserialize)] -pub struct DoUpdateRequest { - #[serde(rename = "edPubkeyPEM")] - #[serde(with = "Base64Standard")] - pub ed_pubkey_pem: Vec, - #[serde(rename = "dhPubkeyPEM")] - #[serde(with = "Base64Standard")] - pub dh_pubkey_pem: Vec, - #[serde(with = "Base64Standard")] - pub nonce: Vec -} - -#[derive(Serialize, Deserialize)] -pub struct DoUpdateResponse { - #[serde(with = "Base64Standard")] - pub config: Vec, - pub counter: u32, - #[serde(with = "Base64Standard")] - pub nonce: Vec, - #[serde(rename = "trustedKeys")] - #[serde(with = "Base64Standard")] - pub trusted_keys: Vec -} - -const ENROLL_ENDPOINT: &str = "/v2/enroll"; - -#[derive(Serialize, Deserialize)] -pub struct EnrollRequest { - pub code: String, - #[serde(rename = "dhPubkey")] - #[serde(with = "Base64Standard")] - pub dh_pubkey: Vec, - #[serde(rename = "edPubkey")] - #[serde(with = "Base64Standard")] - pub ed_pubkey: Vec, - pub timestamp: String -} - - -#[derive(Serialize, Deserialize)] -#[serde(untagged)] -pub enum EnrollResponse { - Success { - data: EnrollResponseData - }, - Error { - errors: APIErrors - } -} - -#[derive(Serialize, Deserialize)] -pub struct EnrollResponseData { - #[serde(with = "Base64Standard")] - pub config: Vec, - #[serde(rename = "hostID")] - pub host_id: String, - pub counter: u32, - #[serde(rename = "trustedKeys")] - #[serde(with = "Base64Standard")] - pub trusted_keys: Vec, - pub organization: EnrollResponseDataOrg -} - -#[derive(Serialize, Deserialize)] -pub struct EnrollResponseDataOrg { - pub id: String, - pub name: String -} - -#[derive(Serialize, Deserialize)] -pub struct APIError { - pub code: String, - pub message: String, - pub path: Option -} - -pub type APIErrors = Vec; \ No newline at end of file diff --git a/tfclient/src/apiworker.rs b/tfclient/src/apiworker.rs index b410f4a..a5f85ce 100644 --- a/tfclient/src/apiworker.rs +++ b/tfclient/src/apiworker.rs @@ -8,7 +8,7 @@ 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::message::{APIResponse, enroll, EnrollRequest}; use crate::config::{load_cdata, save_cdata, TFClientConfig}; use crate::daemon::ThreadMessageSender; diff --git a/tfclient/src/dnapi/mod.rs b/tfclient/src/dnapi/mod.rs deleted file mode 100644 index e5697e1..0000000 --- a/tfclient/src/dnapi/mod.rs +++ /dev/null @@ -1 +0,0 @@ -/// Essentially a direct port of https://github.com/DefinedNet/dnapi \ No newline at end of file diff --git a/tfclient/src/main.rs b/tfclient/src/main.rs index e8e11fa..4c0adfe 100644 --- a/tfclient/src/main.rs +++ b/tfclient/src/main.rs @@ -23,7 +23,6 @@ pub mod config; pub mod service; pub mod apiworker; pub mod socketworker; -pub mod api; pub mod socketclient; pub mod timerworker; diff --git a/trifid-pki/src/cert.rs b/trifid-pki/src/cert.rs index c5495e6..190baf0 100644 --- a/trifid-pki/src/cert.rs +++ b/trifid-pki/src/cert.rs @@ -313,6 +313,26 @@ pub fn deserialize_ed25519_public(bytes: &[u8]) -> Result, Box Result>, Box> { + let mut keys = vec![]; + let pems = pem::parse_many(bytes)?; + + for pem in pems { + if pem.tag != ED25519_PUBLIC_KEY_BANNER { + return Err(KeyError::WrongPemTag.into()) + } + if pem.contents.len() != 64 { + return Err(KeyError::Not64Bytes.into()) + } + keys.push(pem.contents); + } + + Ok(keys) +} + impl NebulaCertificate { /// Sign a nebula certificate with the provided private key /// # Errors diff --git a/trifid-pki/src/test.rs b/trifid-pki/src/test.rs index 55f465e..3d328dc 100644 --- a/trifid-pki/src/test.rs +++ b/trifid-pki/src/test.rs @@ -6,7 +6,7 @@ use std::net::Ipv4Addr; use std::ops::{Add, Sub}; use std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH}; use ipnet::Ipv4Net; -use crate::cert::{CertificateValidity, deserialize_ed25519_private, deserialize_ed25519_public, deserialize_nebula_certificate, deserialize_nebula_certificate_from_pem, deserialize_x25519_private, deserialize_x25519_public, NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private, serialize_ed25519_public, serialize_x25519_private, serialize_x25519_public}; +use crate::cert::{CertificateValidity, deserialize_ed25519_private, deserialize_ed25519_public, deserialize_ed25519_public_many, deserialize_nebula_certificate, deserialize_nebula_certificate_from_pem, deserialize_x25519_private, deserialize_x25519_public, NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private, serialize_ed25519_public, serialize_x25519_private, serialize_x25519_public}; use std::str::FromStr; use ed25519_dalek::{SigningKey, VerifyingKey}; use quick_protobuf::{MessageWrite, Writer}; @@ -300,6 +300,16 @@ fn ed25519_serialization() { assert!(deserialize_ed25519_private(&[0u8; 32]).is_err()); assert_eq!(deserialize_ed25519_public(&serialize_ed25519_public(&bytes)).unwrap(), bytes); assert!(deserialize_ed25519_public(&[0u8; 32]).is_err()); + + let mut bytes = vec![]; + bytes.append(&mut serialize_ed25519_public(&[0u8; 64])); + bytes.append(&mut serialize_ed25519_public(&[1u8; 64])); + let deser = deserialize_ed25519_public_many(&bytes).unwrap(); + assert_eq!(deser[0], [0u8; 64]); + assert_eq!(deser[1], [1u8; 64]); + + bytes.append(&mut serialize_ed25519_public(&[1u8; 63])); + deserialize_ed25519_public_many(&bytes).unwrap_err(); } #[test]