Skip to main content

routex_settlement/
lib.rs

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)]
12/// Client-side key settlement handling
13pub 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    /// Settle a new key with the service
44    ///
45    /// # Errors
46    ///
47    /// Returns errors if the request fails, the attestation verification fails or the response does not meet the expectations.
48    ///
49    /// # Panics
50    ///
51    /// Panics if Base64 values are not valid HTTP header values.
52    pub async fn settle(&mut self, data: &C::Data) -> anyhow::Result<()> {
53        // Request a remote attestation from the TEE which authenticates the TEE's public key and our client public key
54        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    /// System version for the currently established session
94    pub fn system_version(&self) -> Option<&PublishedVersionEntry> {
95        self.system_version.as_ref()
96    }
97
98    /// Seal data for the service
99    ///
100    /// Settles a key if none is settled.
101    ///
102    /// # Errors
103    ///
104    /// Forwards errors from [`settle`](Self::settle)
105    ///
106    /// # Panics
107    ///
108    /// Panics if [`chacha_box_ietf::seal`] panics.
109    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    /// Seal data for the service
114    ///
115    /// Returns [`None`] if no key is settled.
116    ///
117    /// # Panics
118    ///
119    /// Panics if [`chacha_box_ietf::seal`] panics.
120    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    /// Unseal data from the service
127    ///
128    /// # Errors
129    ///
130    /// Forwards errors from [`chacha_box_ietf::unseal`].
131    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    /// Return a settled session ID
136    ///
137    /// Settles a key if none is settled.
138    ///
139    /// # Errors
140    ///
141    /// Forwards errors from [`settle`](Self::settle)
142    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    /// Return a settled session ID
148    ///
149    /// Returns [`None`] if no key is settled.
150    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
178/// Verify that
179/// - a YAXI-provisioned TEE created and signed the attestation report
180/// - the attestation report authenticates the chacha box which seals the TEE's public key.
181///   Therefore, after verification of the attestation report, unsealing the
182///   box with our secret key authenticates the public keys of both parties.
183/// - the attestation report authenticates the chacha box, so transitively also our public
184///   key and the TEE's public key when we are able to unseal the box
185///
186/// # Errors
187///
188/// Returns an error when any step of the verification fails.
189pub 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    // Attestation is signed by AMD, now verify that the chacha box is part of attestation
202    verify_chacha_box(response, &report)?;
203
204    // Verify that the data in `system_version` was signed by a well-known YAXI key
205    response
206        .system_version
207        .verify_signature()
208        .map_err(|err| anyhow!("Could not verify system version's signature: {err:?}"))?;
209
210    // The reported measurement has to match the measurement specified in `system_version`. As
211    // the expected measurement was signed by a YAXI key (see the step above), this guarantees
212    // that the TEE is YAXI-provisioned
213    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            // Otherwise, rustc complains about () not being a Future
286            #[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}