2023-03-29 22:13:04 +00:00
//! 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.
2023-05-14 17:47:49 +00:00
use crate ::credentials ::{ ed25519_public_keys_from_pem , Credentials } ;
use crate ::crypto ::{ new_keys , nonce } ;
use crate ::message ::{
CheckForUpdateResponseWrapper , DoUpdateRequest , DoUpdateResponse , EnrollRequest ,
EnrollResponse , RequestV1 , RequestWrapper , SignedResponseWrapper , CHECK_FOR_UPDATE , DO_UPDATE ,
ENDPOINT_V1 , ENROLL_ENDPOINT ,
} ;
use base64 ::Engine ;
2023-03-29 22:13:04 +00:00
use chrono ::Local ;
use log ::{ debug , error } ;
use reqwest ::StatusCode ;
2023-05-14 17:47:49 +00:00
use serde ::{ Deserialize , Serialize } ;
use std ::error ::Error ;
2023-03-29 22:13:04 +00:00
use trifid_pki ::cert ::serialize_ed25519_public ;
use trifid_pki ::ed25519_dalek ::{ Signature , Signer , SigningKey , Verifier } ;
2023-05-14 17:47:49 +00:00
use url ::Url ;
2023-03-29 22:13:04 +00:00
/// 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 ,
2023-05-14 17:47:49 +00:00
server_url : Url ,
2023-03-29 22:13:04 +00:00
}
2023-03-29 22:44:46 +00:00
#[ derive(Serialize, Deserialize, Clone) ]
2023-03-29 22:13:04 +00:00
/// 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
2023-05-14 17:47:49 +00:00
pub organization_name : String ,
2023-03-29 22:13:04 +00:00
}
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 ,
2023-05-14 17:47:49 +00:00
server_url : api_base ,
2023-03-29 22:13:04 +00:00
} )
}
/// 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
2023-05-14 17:47:49 +00:00
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
) ;
2023-03-29 22:13:04 +00:00
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 ( ) ,
} ) ? ;
2023-05-14 17:47:49 +00:00
let resp = self
. http_client
. post ( self . server_url . join ( ENROLL_ENDPOINT ) ? )
. body ( req_json )
. header ( " Content-Type " , " application/json " )
. send ( )
. await ? ;
let req_id = resp
. headers ( )
. get ( " X-Request-ID " )
. ok_or ( " Response missing X-Request-ID " ) ?
. to_str ( ) ? ;
2023-03-29 22:13:04 +00:00
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 > > {
2023-05-14 17:47:49 +00:00
let body = self
. post_dnclient (
CHECK_FOR_UPDATE ,
& [ ] ,
& creds . host_id ,
creds . counter ,
& creds . ed_privkey ,
)
. await ? ;
2023-03-29 22:13:04 +00:00
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
2023-05-14 17:47:49 +00:00
pub async fn do_update (
& self ,
creds : & Credentials ,
) -> Result < ( NebulaConfig , DHPrivateKeyPEM , Credentials ) , Box < dyn Error > > {
2023-03-29 22:13:04 +00:00
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 ) ? ;
2023-05-14 17:47:49 +00:00
let resp = self
. post_dnclient (
DO_UPDATE ,
& update_keys_blob ,
& creds . host_id ,
creds . counter ,
& creds . ed_privkey ,
)
. await ? ;
2023-03-29 22:13:04 +00:00
let result_wrapper : SignedResponseWrapper = serde_json ::from_slice ( & resp ) ? ;
let mut valid = false ;
for ca_pubkey in & creds . trusted_keys {
2023-05-14 17:47:49 +00:00
if ca_pubkey
. verify (
& result_wrapper . data . message ,
& Signature ::from_slice ( & result_wrapper . data . signature ) ? ,
)
. is_ok ( )
{
2023-03-29 22:13:04 +00:00
valid = true ;
break ;
}
}
if ! valid {
2023-05-14 17:47:49 +00:00
return Err ( " Failed to verify signed API result " . into ( ) ) ;
2023-03-29 22:13:04 +00:00
}
2023-05-15 17:39:15 +00:00
debug! ( " deserializing result " ) ;
2023-03-29 22:13:04 +00:00
let result : DoUpdateResponse = serde_json ::from_slice ( & result_wrapper . data . message ) ? ;
if result . nonce ! = update_keys . nonce {
2023-05-14 17:47:49 +00:00
error! (
" nonce mismatch between request {:x?} and response {:x?} " ,
result . nonce , update_keys . nonce
) ;
return Err ( " nonce mismatch between request and response " . into ( ) ) ;
2023-03-29 22:13:04 +00:00
}
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
2023-05-14 17:47:49 +00:00
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 > > {
2023-03-29 22:13:04 +00:00
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 ( ) ;
2023-03-30 16:13:29 +00:00
let b64_msg = base64 ::engine ::general_purpose ::STANDARD . encode ( encoded_msg_bytes ) ;
let b64_msg_bytes = b64_msg . as_bytes ( ) ;
let signature = ed_privkey . sign ( b64_msg_bytes ) . to_vec ( ) ;
ed_privkey . verify ( b64_msg_bytes , & Signature ::from_slice ( & signature ) ? ) ? ;
debug! ( " signature valid via clientside check " ) ;
2023-03-29 22:13:04 +00:00
let body = RequestV1 {
version : 1 ,
host_id : host_id . to_string ( ) ,
counter ,
2023-03-30 16:13:29 +00:00
message : b64_msg ,
2023-03-29 22:13:04 +00:00
signature ,
} ;
let post_body = serde_json ::to_string ( & body ) ? ;
2023-05-14 17:47:49 +00:00
let resp = self
. http_client
. post ( self . server_url . join ( ENDPOINT_V1 ) ? )
. body ( post_body )
2023-05-15 17:39:15 +00:00
. header ( " Content-Type " , " application/json " )
2023-05-14 17:47:49 +00:00
. send ( )
. await ? ;
2023-03-29 22:13:04 +00:00
match resp . status ( ) {
2023-05-14 17:47:49 +00:00
StatusCode ::OK = > Ok ( resp . bytes ( ) . await ? . to_vec ( ) ) ,
StatusCode ::FORBIDDEN = > Err ( " Forbidden " . into ( ) ) ,
2023-03-29 22:13:04 +00:00
_ = > {
2023-05-14 17:47:49 +00:00
error! (
" dnclient endpoint returned bad status code {} " ,
resp . status ( )
) ;
2023-03-29 22:13:04 +00:00
Err ( " dnclient endpoint returned error " . into ( ) )
}
}
}
2023-05-14 17:47:49 +00:00
}