|
2 | 2 | // |
3 | 3 | // SPDX-License-Identifier: Apache-2.0 |
4 | 4 |
|
| 5 | +use std::collections::HashMap; |
5 | 6 | use std::sync::{Arc, Mutex}; |
6 | 7 | use std::time::{Duration, Instant}; |
7 | 8 |
|
@@ -92,11 +93,35 @@ pub struct AppState { |
92 | 93 | inner: Arc<AppStateInner>, |
93 | 94 | } |
94 | 95 |
|
| 96 | +/// The app root key of one KMS root epoch, with its KMS signature. |
| 97 | +#[derive(Clone)] |
| 98 | +pub(crate) struct AppRoot { |
| 99 | + pub epoch: u32, |
| 100 | + pub k256_key: Vec<u8>, |
| 101 | + pub signing_key: SigningKey, |
| 102 | + pub kms_signature: Vec<u8>, |
| 103 | +} |
| 104 | + |
| 105 | +impl AppRoot { |
| 106 | + fn new(epoch: u32, k256_key: Vec<u8>, kms_signature: Vec<u8>) -> Result<Self> { |
| 107 | + let signing_key = SigningKey::from_slice(&k256_key) |
| 108 | + .context("the app root k256 key is not a valid secp256k1 scalar")?; |
| 109 | + Ok(Self { |
| 110 | + epoch, |
| 111 | + k256_key, |
| 112 | + signing_key, |
| 113 | + kms_signature, |
| 114 | + }) |
| 115 | + } |
| 116 | +} |
| 117 | + |
95 | 118 | struct AppStateInner { |
96 | 119 | config: Config, |
97 | 120 | keys: AppKeys, |
98 | 121 | vm_config: String, |
99 | 122 | cert_client: CertRequestClient, |
| 123 | + /// Held across a fetch so concurrent requests for one epoch fetch it once. |
| 124 | + historical_roots: tokio::sync::Mutex<HashMap<u32, AppRoot>>, |
100 | 125 | demo_cert: tokio::sync::OnceCell<String>, |
101 | 126 | /// When the last demo-cert request failed; held while one is running. |
102 | 127 | demo_cert_last_failure: tokio::sync::Mutex<Option<Instant>>, |
@@ -300,6 +325,7 @@ impl AppState { |
300 | 325 | config, |
301 | 326 | keys, |
302 | 327 | cert_client, |
| 328 | + historical_roots: Default::default(), |
303 | 329 | demo_cert: Default::default(), |
304 | 330 | demo_cert_last_failure: Default::default(), |
305 | 331 | vm_config, |
@@ -377,6 +403,58 @@ impl AppState { |
377 | 403 | Ok(key) |
378 | 404 | } |
379 | 405 |
|
| 406 | + /// KMS root epoch of the app keys this agent serves. |
| 407 | + pub(crate) fn root_epoch(&self) -> u32 { |
| 408 | + self.inner.keys.root_epoch() |
| 409 | + } |
| 410 | + |
| 411 | + /// The app root key and its KMS signature of `root_epoch`; the current |
| 412 | + /// ones when unset. An older epoch is requested from the KMS once and kept |
| 413 | + /// in memory. |
| 414 | + pub(crate) async fn app_root(&self, root_epoch: Option<u32>) -> Result<AppRoot> { |
| 415 | + let current = self.root_epoch(); |
| 416 | + let epoch = root_epoch.unwrap_or(current); |
| 417 | + if epoch == current { |
| 418 | + return Ok(AppRoot { |
| 419 | + epoch, |
| 420 | + k256_key: self.app_root_k256_key().to_vec(), |
| 421 | + signing_key: self.app_root_signing_key()?.clone(), |
| 422 | + kms_signature: self.kms_k256_signature().to_vec(), |
| 423 | + }); |
| 424 | + } |
| 425 | + if epoch > current { |
| 426 | + anyhow::bail!("root epoch {epoch} is newer than the current one, {current}"); |
| 427 | + } |
| 428 | + self.historical_root(epoch).await |
| 429 | + } |
| 430 | + |
| 431 | + pub(crate) async fn historical_root(&self, epoch: u32) -> Result<AppRoot> { |
| 432 | + let mut roots = self.inner.historical_roots.lock().await; |
| 433 | + if let Some(root) = roots.get(&epoch) { |
| 434 | + return Ok(root.clone()); |
| 435 | + } |
| 436 | + let keys = self |
| 437 | + .inner |
| 438 | + .cert_client |
| 439 | + .get_app_key(epoch) |
| 440 | + .await |
| 441 | + .with_context(|| format!("failed to fetch the app keys of root epoch {epoch}"))?; |
| 442 | + let root = AppRoot::new(epoch, keys.k256_key, keys.k256_signature)?; |
| 443 | + roots.insert(epoch, root.clone()); |
| 444 | + Ok(root) |
| 445 | + } |
| 446 | + |
| 447 | + #[cfg(test)] |
| 448 | + pub(crate) async fn insert_historical_root( |
| 449 | + &self, |
| 450 | + epoch: u32, |
| 451 | + k256_key: Vec<u8>, |
| 452 | + kms_signature: Vec<u8>, |
| 453 | + ) { |
| 454 | + let root = AppRoot::new(epoch, k256_key, kms_signature).unwrap(); |
| 455 | + self.inner.historical_roots.lock().await.insert(epoch, root); |
| 456 | + } |
| 457 | + |
380 | 458 | /// The KMS root key's signature over the app root public key: the second |
381 | 459 | /// link of every signature chain, produced outside this agent and passed |
382 | 460 | /// through byte-for-byte on both API surfaces. |
@@ -1152,6 +1230,8 @@ pub(crate) mod tests { |
1152 | 1230 | .expect("certificate carries an attestation") |
1153 | 1231 | } |
1154 | 1232 |
|
| 1233 | + pub(crate) const HISTORICAL_K256_KEY: [u8; 32] = [0x11; 32]; |
| 1234 | + |
1155 | 1235 | pub(crate) async fn setup_test_state() -> (AppState, tempfile::NamedTempFile) { |
1156 | 1236 | setup_test_state_with_platform(None).await |
1157 | 1237 | } |
@@ -1442,6 +1522,7 @@ pNs85uhOZE8z2jr8Pg== |
1442 | 1522 | keys: dummy_keys, |
1443 | 1523 | vm_config: String::new(), |
1444 | 1524 | cert_client: dummy_cert_client, |
| 1525 | + historical_roots: Default::default(), |
1445 | 1526 | demo_cert: Default::default(), |
1446 | 1527 | demo_cert_last_failure: Default::default(), |
1447 | 1528 | platform: Arc::new(TestSimulatorPlatform { |
|
0 commit comments