1use crate::credentials::{ed25519_public_keys_from_pem, Credentials};
4use crate::crypto::{new_keys, nonce};
5use crate::message::{
6 CheckForUpdateResponseWrapper, DoUpdateRequest, DoUpdateResponse, EnrollRequest,
7 EnrollResponse, RequestV1, RequestWrapper, SignedResponseWrapper, CHECK_FOR_UPDATE, DO_UPDATE,
8 ENDPOINT_V1, ENROLL_ENDPOINT,
9};
10use base64::Engine;
11use chrono::Local;
12use log::{debug, error};
13use reqwest::StatusCode;
14use serde::{Deserialize, Serialize};
15use std::error::Error;
16use reqwest::header::HeaderValue;
17use trifid_pki::cert::serialize_ed25519_public;
18use trifid_pki::ed25519_dalek::{Signature, Signer, SigningKey, Verifier};
19use url::Url;
20
21pub type NebulaConfig = Vec<u8>;
23
24pub type DHPrivateKeyPEM = Vec<u8>;
26
27pub struct Client {
29 http_client: reqwest::Client,
30 server_url: Url,
31}
32
33#[derive(Serialize, Deserialize, Clone)]
34pub struct EnrollMeta {
36 pub organization_id: String,
38 pub organization_name: String,
40}
41
42impl Client {
43 pub fn new(user_agent: String, api_base: Url) -> Result<Self, Box<dyn Error>> {
47 let client = reqwest::Client::builder().user_agent(user_agent).build()?;
48 Ok(Self {
49 http_client: client,
50 server_url: api_base,
51 })
52 }
53
54 pub async fn enroll(
68 &self,
69 code: &str,
70 ) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials, EnrollMeta), Box<dyn Error>> {
71 debug!(
72 "making enrollment request to API {{server: {}, code: {}}}",
73 self.server_url, code
74 );
75
76 let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
77
78 let req_json = serde_json::to_string(&EnrollRequest {
79 code: code.to_string(),
80 dh_pubkey: dh_pubkey_pem,
81 ed_pubkey: serialize_ed25519_public(ed_pubkey.as_bytes()),
82 timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
83 })?;
84
85 let resp = self
86 .http_client
87 .post(self.server_url.join(ENROLL_ENDPOINT)?)
88 .body(req_json)
89 .header("Content-Type", "application/json")
90 .send()
91 .await?;
92
93 let empty_hval;
94 #[allow(clippy::unwrap_used)] {
95 empty_hval = HeaderValue::from_str("").unwrap();
96 };
97
98 let req_id = resp
99 .headers()
100 .get("X-Request-ID")
101 .unwrap_or(&empty_hval)
102 .to_str()?;
103 debug!("enrollment request complete {{req_id: {}}}", req_id);
104
105 let resp: EnrollResponse = resp.json().await?;
106
107 let r = match resp {
108 EnrollResponse::Success { data } => data,
109 EnrollResponse::Error { errors } => {
110 error!("unexpected error during enrollment: {}", errors[0].message);
111 return Err(errors[0].message.clone().into());
112 }
113 };
114
115 let meta = EnrollMeta {
116 organization_id: r.organization.id,
117 organization_name: r.organization.name,
118 };
119
120 let trusted_keys = ed25519_public_keys_from_pem(&r.trusted_keys)?;
121
122 let creds = Credentials {
123 host_id: r.host_id,
124 ed_privkey,
125 counter: r.counter,
126 trusted_keys,
127 };
128
129 Ok((r.config, dh_privkey_pem, creds, meta))
130 }
131
132 pub async fn check_for_update(&self, creds: &Credentials) -> Result<bool, Box<dyn Error>> {
136 let body = self
137 .post_dnclient(
138 CHECK_FOR_UPDATE,
139 &[],
140 &creds.host_id,
141 creds.counter,
142 &creds.ed_privkey,
143 )
144 .await?;
145
146 let result: CheckForUpdateResponseWrapper = serde_json::from_slice(&body)?;
147
148 Ok(result.data.update_available)
149 }
150
151 pub async fn do_update(
163 &self,
164 creds: &Credentials,
165 ) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials), Box<dyn Error>> {
166 let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
167
168 let update_keys = DoUpdateRequest {
169 ed_pubkey_pem: serialize_ed25519_public(ed_pubkey.as_bytes()),
170 dh_pubkey_pem,
171 nonce: nonce().to_vec(),
172 };
173
174 let update_keys_blob = serde_json::to_vec(&update_keys)?;
175
176 let resp = self
177 .post_dnclient(
178 DO_UPDATE,
179 &update_keys_blob,
180 &creds.host_id,
181 creds.counter,
182 &creds.ed_privkey,
183 )
184 .await?;
185
186 let result_wrapper: SignedResponseWrapper = serde_json::from_slice(&resp)?;
187
188 let mut valid = false;
189
190 for ca_pubkey in &creds.trusted_keys {
191 if ca_pubkey
192 .verify(
193 &result_wrapper.data.message,
194 &Signature::from_slice(&result_wrapper.data.signature)?,
195 )
196 .is_ok()
197 {
198 valid = true;
199 break;
200 }
201 }
202
203 if !valid {
204 return Err("Failed to verify signed API result".into());
205 }
206
207 debug!("deserializing result");
208
209 let result: DoUpdateResponse = serde_json::from_slice(&result_wrapper.data.message)?;
210
211 if result.nonce != update_keys.nonce {
212 error!(
213 "nonce mismatch between request {:x?} and response {:x?}",
214 result.nonce, update_keys.nonce
215 );
216 return Err("nonce mismatch between request and response".into());
217 }
218
219 let trusted_keys = ed25519_public_keys_from_pem(&result.trusted_keys)?;
220
221 let new_creds = Credentials {
222 host_id: creds.host_id.clone(),
223 ed_privkey,
224 counter: result.counter,
225 trusted_keys,
226 };
227
228 Ok((result.config, dh_privkey_pem, new_creds))
229 }
230
231 pub async fn post_dnclient(
239 &self,
240 req_type: &str,
241 value: &[u8],
242 host_id: &str,
243 counter: u32,
244 ed_privkey: &SigningKey,
245 ) -> Result<Vec<u8>, Box<dyn Error>> {
246 let encoded_msg = serde_json::to_string(&RequestWrapper {
247 message_type: req_type.to_string(),
248 value: value.to_vec(),
249 timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
250 })?;
251 let encoded_msg_bytes = encoded_msg.into_bytes();
252 let b64_msg = base64::engine::general_purpose::STANDARD.encode(encoded_msg_bytes);
253 let b64_msg_bytes = b64_msg.as_bytes();
254 let signature = ed_privkey.sign(b64_msg_bytes).to_vec();
255
256 ed_privkey.verify(b64_msg_bytes, &Signature::from_slice(&signature)?)?;
257 debug!("signature valid via clientside check");
258
259 let body = RequestV1 {
260 version: 1,
261 host_id: host_id.to_string(),
262 counter,
263 message: b64_msg,
264 signature,
265 };
266
267 let post_body = serde_json::to_string(&body)?;
268
269 let resp = self
270 .http_client
271 .post(self.server_url.join(ENDPOINT_V1)?)
272 .body(post_body)
273 .header("Content-Type", "application/json")
274 .send()
275 .await?;
276
277 match resp.status() {
278 StatusCode::OK => Ok(resp.bytes().await?.to_vec()),
279 StatusCode::FORBIDDEN => Err("Forbidden".into()),
280 _ => {
281 error!(
282 "dnclient endpoint returned bad status code {}",
283 resp.status()
284 );
285 Err("dnclient endpoint returned error".into())
286 }
287 }
288 }
289}