1use std::future::Future;
2
3use anyhow::anyhow;
4use base64::prelude::*;
5use chacha_box_ietf::{PublicKey, SecretKey, unseal};
6use clerk_report::PublishedVersionEntry;
7use http::HeaderValue;
8use log::info;
9use routex_api::keys::{Response, SettlementBoxMessage};
10
11#[derive(Debug)]
12pub struct KeySettlement<C> {
14 secret_key: SecretKey,
15 server_key: Option<(PublicKey, HeaderValue)>,
16 #[allow(clippy::struct_field_names)]
17 core: C,
18 system_version: Option<PublishedVersionEntry>,
19}
20
21pub trait KeySettlementCore {
22 type Data: ?Sized;
23
24 fn request(
25 &self,
26 public_key: [u8; PublicKey::size()],
27 data: &Self::Data,
28 ) -> impl Future<Output = anyhow::Result<Response>>;
29
30 fn new_session(&mut self, _id: [u8; 32], _public_key: &PublicKey, _secret_key: &SecretKey) {}
31}
32
33impl<C: KeySettlementCore> KeySettlement<C> {
34 pub fn new(core: C) -> Self {
35 Self {
36 secret_key: generate_key(),
37 server_key: None,
38 core,
39 system_version: None,
40 }
41 }
42
43 pub async fn settle(&mut self, data: &C::Data) -> anyhow::Result<()> {
53 let response = self
55 .core
56 .request(*self.secret_key.public_key().as_bytes(), data)
57 .await?;
58
59 if let Err(err) = verify_attestation(&response) {
60 return Err(anyhow!(
61 "Invalid attestation report ({err:?}): {:?}",
62 response.attestation_report
63 ));
64 }
65
66 let (server_public_key, session_id) = unseal(&self.secret_key, &response.chacha_box)
67 .map_err(|err| {
68 anyhow!("Could not unseal chacha box containing routex's public key: {err:?}")
69 })
70 .and_then(|box_bytes| {
71 serde_json::from_slice::<SettlementBoxMessage>(&box_bytes).map_err(Into::into)
72 })
73 .and_then(|contents| {
74 PublicKey::from_slice(&contents.public_key)
75 .map_err(|err| anyhow!("Could not deserialize routex's public key: {err:?}"))
76 .map(|public_key| (public_key, contents.session_id))
77 })?;
78
79 self.core
80 .new_session(session_id, &server_public_key, &self.secret_key);
81
82 self.server_key = Some((
83 server_public_key,
84 HeaderValue::from_str(&BASE64_STANDARD.encode(session_id))
85 .expect("Value should be valid"),
86 ));
87
88 self.system_version = Some(response.system_version);
89
90 Ok(())
91 }
92
93 pub fn system_version(&self) -> Option<&PublishedVersionEntry> {
95 self.system_version.as_ref()
96 }
97
98 pub async fn seal(&mut self, data: &[u8], user_data: &C::Data) -> anyhow::Result<Vec<u8>> {
110 self.try_f(|s| s.try_seal(data), user_data).await
111 }
112
113 pub fn try_seal(&self, data: &[u8]) -> Option<Vec<u8>> {
121 self.server_key
122 .as_ref()
123 .map(|(key, _)| chacha_box_ietf::seal(key, data).expect("Encrypt should work"))
124 }
125
126 pub fn unseal(&self, data: &[u8]) -> Result<Vec<u8>, chacha_box_ietf::Error> {
132 chacha_box_ietf::unseal(&self.secret_key, data)
133 }
134
135 pub async fn session_id(&mut self, user_data: &C::Data) -> anyhow::Result<&HeaderValue> {
143 self.try_f(|s: &mut KeySettlement<C>| s.try_session_id(), user_data)
144 .await
145 }
146
147 pub fn try_session_id(&self) -> Option<&HeaderValue> {
151 self.server_key.as_ref().map(|(_, session_id)| session_id)
152 }
153
154 async fn try_f<'a, T>(
155 &'a mut self,
156 f: impl FnOnce(&'a mut Self) -> Option<T>,
157 user_data: &C::Data,
158 ) -> anyhow::Result<T> {
159 if self.server_key.is_none() {
160 info!("No key set, running a key settlement.");
161 self.settle(user_data).await?;
162 }
163
164 Ok(f(self).expect("Key should be set"))
165 }
166}
167
168#[cfg(feature = "unattested")]
169fn generate_key() -> SecretKey {
170 routex_keys_fixtures::fixed_client_key().into()
171}
172
173#[cfg(not(feature = "unattested"))]
174fn generate_key() -> SecretKey {
175 SecretKey::generate()
176}
177
178pub fn verify_attestation(response: &Response) -> std::result::Result<(), anyhow::Error> {
190 use clerk_report::verification::{Requirements, RootStore, verify_report};
191
192 let root_store = RootStore::default();
193 let report = verify_report(
194 &response.attestation_report,
195 std::io::Cursor::new(response.vcek.as_bytes()),
196 &root_store,
197 &Requirements::default(),
198 )
199 .map_err(|err| anyhow!("Verification resulted in error: {err:?}"))?;
200
201 verify_chacha_box(response, &report)?;
203
204 response
206 .system_version
207 .verify_signature()
208 .map_err(|err| anyhow!("Could not verify system version's signature: {err:?}"))?;
209
210 if report.measurement
214 == response
215 .system_version
216 .launch_measurement
217 .resolve(response.vcpus)
218 {
219 Ok(())
220 } else {
221 Err(anyhow!(
222 "Reported measurement {:?} doesn't match expected measurement {:?}",
223 report.measurement,
224 response.system_version.launch_measurement
225 ))
226 }
227}
228
229fn verify_chacha_box(
230 response: &Response,
231 report: &clerk_report::AttestationReport,
232) -> std::result::Result<(), anyhow::Error> {
233 use sha2::{Digest, Sha256};
234
235 if &report.report_data[..32] == Sha256::digest(&response.chacha_box).as_slice() {
236 Ok(())
237 } else {
238 Err(anyhow!(
239 "Data in attestation report doesn't match chacha box"
240 ))
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use base64::prelude::*;
247 use chacha_box_ietf::{PublicKey, SecretKey};
248 use rand::{TryRng, rngs::SysRng};
249 use routex_api::keys::Response;
250 use routex_keys_fixtures::fixed_test_key_response;
251
252 use super::{KeySettlement, KeySettlementCore};
253
254 struct FixedResponse(routex_api::keys::Response);
255
256 impl FixedResponse {
257 fn new(response: routex_api::keys::Response) -> Self {
258 Self(response)
259 }
260 }
261
262 impl KeySettlementCore for FixedResponse {
263 type Data = ();
264
265 fn request(
266 &self,
267 _public_key: [u8; 32],
268 _data: &Self::Data,
269 ) -> impl std::future::Future<Output = anyhow::Result<routex_api::keys::Response>> {
270 std::future::ready(Ok(self.0.clone()))
271 }
272 }
273
274 struct ShouldNotSettle;
275
276 impl KeySettlementCore for ShouldNotSettle {
277 type Data = ();
278
279 fn request(
280 &self,
281 _public_key: [u8; 32],
282 _data: &Self::Data,
283 ) -> impl std::future::Future<Output = anyhow::Result<routex_api::keys::Response>> {
284 panic!("Unexpectedly called key settlement function");
285 #[allow(unreachable_code)]
287 std::future::ready(Ok(fixed_test_key_response()))
288 }
289 }
290
291 fn fixed_settlement() -> KeySettlement<FixedResponse> {
292 settlement_with_response(fixed_test_key_response())
293 }
294
295 fn settlement_with_response(
296 response: routex_api::keys::Response,
297 ) -> KeySettlement<FixedResponse> {
298 KeySettlement {
299 secret_key: routex_keys_fixtures::fixed_client_key().into(),
300 server_key: None,
301 core: FixedResponse::new(response),
302 system_version: None,
303 }
304 }
305
306 fn assert_err_starts_with<T: std::fmt::Debug, E: std::fmt::Debug + std::fmt::Display>(
307 value: Result<T, E>,
308 expectation: &str,
309 ) {
310 if let Err(err) = value {
311 let err_str = err.to_string();
312 if !err_str.starts_with(expectation) {
313 assert_eq!(expectation, err_str);
314 }
315 } else {
316 panic!("Expected Err, got {value:?}");
317 }
318 }
319
320 #[tokio::test]
321 async fn test_seal_unseal_roundtrip() {
322 let key = chacha_box_ietf::SecretKey::generate();
323 let mut settlement = KeySettlement {
324 secret_key: key.clone(),
325 server_key: Some((key.public_key(), "session-id".try_into().unwrap())),
326 core: ShouldNotSettle,
327 system_version: None,
328 };
329 let mut data = [0u8; 42];
330 SysRng.try_fill_bytes(&mut data).unwrap();
331
332 let secret_box = settlement.seal(&data, &()).await.unwrap();
333 let unsealed_data = settlement.unseal(&secret_box).unwrap();
334
335 assert_ne!(&data[..], secret_box);
336 assert_eq!(&data[..], unsealed_data);
337 }
338
339 #[tokio::test]
340 async fn test_settle() {
341 let mut settlement = fixed_settlement();
342 settlement.settle(&()).await.unwrap();
343 }
344
345 #[tokio::test]
346 async fn test_settle_invalid_vcek() {
347 let mut settlement = settlement_with_response({
348 let mut response = fixed_test_key_response();
349 response.vcek = "invalid".into();
350 response
351 });
352 let result = settlement.settle(&()).await;
353 assert_err_starts_with(
354 result,
355 "Invalid attestation report (Verification resulted in error: ChainBroken)",
356 );
357 }
358
359 #[tokio::test]
360 async fn test_settle_invalid_attestation_report_signature() {
361 let mut settlement = settlement_with_response({
362 let mut response = fixed_test_key_response();
363 response.attestation_report[0] = 42;
364 response
365 });
366 let result = settlement.settle(&()).await;
367 assert_err_starts_with(
368 result,
369 "Invalid attestation report (Verification resulted in error: ReportSignatureMismatch",
370 );
371 }
372
373 #[tokio::test]
374 async fn test_settle_invalid_system_version_signature() {
375 let mut settlement = settlement_with_response({
376 let mut response = fixed_test_key_response();
377 response.system_version.signature.value[0] = 42;
378 response
379 });
380 let result = settlement.settle(&()).await;
381 assert_err_starts_with(
382 result,
383 "Invalid attestation report (Could not verify system version's signature: SignatureError",
384 );
385 }
386
387 #[tokio::test]
388 async fn test_session_id() {
389 let mut settlement = fixed_settlement();
390 let session_id = settlement.session_id(&()).await.unwrap();
391 assert_eq!(
392 session_id.to_str().unwrap(),
393 BASE64_STANDARD.encode(routex_keys_fixtures::fixed_session_id())
394 );
395 }
396
397 #[tokio::test]
398 async fn test_session_id_settled_key() {
399 let key = chacha_box_ietf::SecretKey::generate();
400 let expected_session_id: http::HeaderValue = "session-id".try_into().unwrap();
401 let mut settlement = KeySettlement {
402 secret_key: key.clone(),
403 server_key: Some((key.public_key(), expected_session_id.clone())),
404 core: ShouldNotSettle,
405 system_version: None,
406 };
407
408 let session_id = settlement.session_id(&()).await.unwrap();
409
410 assert_eq!(session_id, &expected_session_id);
411 }
412
413 #[test]
414 fn test_seal_settled_key() {
415 let secret_key = SecretKey::from(routex_keys_fixtures::fixed_client_key());
416 let server_key = Some((secret_key.public_key(), "session-id".try_into().unwrap()));
417
418 let settlement = KeySettlement {
419 secret_key,
420 server_key,
421 core: ShouldNotSettle,
422 system_version: None,
423 };
424 let sealed = settlement.try_seal(&[]);
425 assert!(sealed.is_some());
426 }
427
428 #[test]
429 fn test_seal_not_settled_yet() {
430 let settlement = KeySettlement::new(ShouldNotSettle);
431 assert_eq!(None, settlement.try_seal(&[]));
432 }
433
434 #[tokio::test]
435 async fn test_new_session_callback() {
436 struct Core {
437 session: bool,
438 }
439
440 impl KeySettlementCore for Core {
441 type Data = ();
442
443 fn request(
444 &self,
445 _public_key: [u8; PublicKey::size()],
446 _data: &Self::Data,
447 ) -> impl Future<Output = anyhow::Result<Response>> {
448 std::future::ready(Ok(fixed_test_key_response()))
449 }
450
451 fn new_session(
452 &mut self,
453 _session_id: [u8; 32],
454 _public_key: &PublicKey,
455 _secret_key: &SecretKey,
456 ) {
457 self.session = true;
458 }
459 }
460
461 let mut settlement = KeySettlement {
462 secret_key: routex_keys_fixtures::fixed_client_key().into(),
463 server_key: None,
464 core: Core { session: false },
465 system_version: None,
466 };
467
468 settlement.settle(&()).await.unwrap();
469
470 assert!(settlement.core.session);
471 }
472}