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, trace};
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::blocking::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::blocking::Client::builder()
48 .user_agent(user_agent)
49 .build()?;
50 Ok(Self {
51 http_client: client,
52 server_url: api_base,
53 })
54 }
55
56 pub fn enroll(
70 &self,
71 code: &str,
72 ) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials, EnrollMeta), Box<dyn Error>> {
73 debug!(
74 "making enrollment request to API {{server: {}, code: {}}}",
75 self.server_url, code
76 );
77
78 let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
79
80 let req_json = serde_json::to_string(&EnrollRequest {
81 code: code.to_string(),
82 dh_pubkey: dh_pubkey_pem,
83 ed_pubkey: serialize_ed25519_public(ed_pubkey.as_bytes()),
84 timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
85 })?;
86
87 let resp = self
88 .http_client
89 .post(self.server_url.join(ENROLL_ENDPOINT)?)
90 .header("Content-Type", "application/json")
91 .body(req_json)
92 .send()?;
93
94 let empty_hval;
95 #[allow(clippy::unwrap_used)] {
96 empty_hval = HeaderValue::from_str("").unwrap();
97 };
98
99 let req_id = resp
100 .headers()
101 .get("X-Request-ID")
102 .unwrap_or(&empty_hval)
103 .to_str()?;
104 debug!("enrollment request complete {{req_id: {}}}", req_id);
105
106 let resp: EnrollResponse = resp.json()?;
107
108 let r = match resp {
109 EnrollResponse::Success { data } => data,
110 EnrollResponse::Error { errors } => {
111 error!("unexpected error during enrollment: {}", errors[0].message);
112 return Err(errors[0].message.clone().into());
113 }
114 };
115
116 let meta = EnrollMeta {
117 organization_id: r.organization.id,
118 organization_name: r.organization.name,
119 };
120
121 debug!("parsing public keys");
122
123 let trusted_keys = ed25519_public_keys_from_pem(&r.trusted_keys)?;
124
125 let creds = Credentials {
126 host_id: r.host_id,
127 ed_privkey,
128 counter: r.counter,
129 trusted_keys,
130 };
131
132 Ok((r.config, dh_privkey_pem, creds, meta))
133 }
134
135 pub fn check_for_update(&self, creds: &Credentials) -> Result<bool, Box<dyn Error>> {
139 let body = self.post_dnclient(
140 CHECK_FOR_UPDATE,
141 &[],
142 &creds.host_id,
143 creds.counter,
144 &creds.ed_privkey,
145 )?;
146
147 let result: CheckForUpdateResponseWrapper = serde_json::from_slice(&body)?;
148
149 Ok(result.data.update_available)
150 }
151
152 pub fn do_update(
164 &self,
165 creds: &Credentials,
166 ) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials), Box<dyn Error>> {
167 let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
168
169 let update_keys = DoUpdateRequest {
170 ed_pubkey_pem: serialize_ed25519_public(ed_pubkey.as_bytes()),
171 dh_pubkey_pem,
172 nonce: nonce().to_vec(),
173 };
174
175 let update_keys_blob = serde_json::to_vec(&update_keys)?;
176
177 let resp = self.post_dnclient(
178 DO_UPDATE,
179 &update_keys_blob,
180 &creds.host_id,
181 creds.counter,
182 &creds.ed_privkey,
183 )?;
184
185 let result_wrapper: SignedResponseWrapper = serde_json::from_slice(&resp)?;
186
187 let mut valid = false;
188
189 for ca_pubkey in &creds.trusted_keys {
190 if ca_pubkey
191 .verify(
192 &result_wrapper.data.message,
193 &Signature::from_slice(&result_wrapper.data.signature)?,
194 )
195 .is_ok()
196 {
197 valid = true;
198 break;
199 }
200 }
201
202 if !valid {
203 return Err("Failed to verify signed API result".into());
204 }
205
206 let result: DoUpdateResponse = serde_json::from_slice(&result_wrapper.data.message)?;
207
208 if result.nonce != update_keys.nonce {
209 error!(
210 "nonce mismatch between request {:x?} and response {:x?}",
211 result.nonce, update_keys.nonce
212 );
213 return Err("nonce mismatch between request and response".into());
214 }
215
216 if result.counter <= creds.counter {
217 error!(
218 "counter in request {} should be less than counter in response {}",
219 creds.counter, result.counter
220 );
221 return Err("received older config than what we already had".into());
222 }
223
224 let trusted_keys = ed25519_public_keys_from_pem(&result.trusted_keys)?;
225
226 let new_creds = Credentials {
227 host_id: creds.host_id.clone(),
228 ed_privkey,
229 counter: result.counter,
230 trusted_keys,
231 };
232
233 Ok((result.config, dh_privkey_pem, new_creds))
234 }
235
236 pub fn post_dnclient(
244 &self,
245 req_type: &str,
246 value: &[u8],
247 host_id: &str,
248 counter: u32,
249 ed_privkey: &SigningKey,
250 ) -> Result<Vec<u8>, Box<dyn Error>> {
251 let encoded_msg = serde_json::to_string(&RequestWrapper {
252 message_type: req_type.to_string(),
253 value: value.to_vec(),
254 timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
255 })?;
256 let encoded_msg_bytes = encoded_msg.into_bytes();
257 let b64_msg = base64::engine::general_purpose::STANDARD.encode(encoded_msg_bytes);
258 let b64_msg_bytes = b64_msg.as_bytes();
259 let signature = ed_privkey.sign(b64_msg_bytes).to_vec();
260
261 ed_privkey.verify(b64_msg_bytes, &Signature::from_slice(&signature)?)?;
262 debug!("signature valid via clientside check");
263 debug!(
264 "signed with key: {:x?}",
265 ed_privkey.verifying_key().as_bytes()
266 );
267
268 let body = RequestV1 {
269 version: 1,
270 host_id: host_id.to_string(),
271 counter,
272 message: b64_msg,
273 signature,
274 };
275
276 let post_body = serde_json::to_string(&body)?;
277
278 trace!("sending dnclient request {}", post_body);
279
280 let resp = self
281 .http_client
282 .post(self.server_url.join(ENDPOINT_V1)?)
283 .body(post_body)
284 .header("Content-Type", "application/json")
285 .send()?;
286
287 match resp.status() {
288 StatusCode::OK => Ok(resp.bytes()?.to_vec()),
289 StatusCode::FORBIDDEN => Err("Forbidden".into()),
290 _ => {
291 error!(
292 "dnclient endpoint returned bad status code {}",
293 resp.status()
294 );
295 Err("dnclient endpoint returned error".into())
296 }
297 }
298 }
299}