diff --git a/Cargo.lock b/Cargo.lock index a0817fc2f65..d35eef9706e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,14 +457,14 @@ dependencies = [ [[package]] name = "attest-data" version = "0.5.0" -source = "git+https://github.com/oxidecomputer/dice-util?rev=ff9f27aa0d6ef6fb64c349890b6e3c242ea3d8fc#ff9f27aa0d6ef6fb64c349890b6e3c242ea3d8fc" +source = "git+https://github.com/oxidecomputer/dice-util?rev=994c3fcdb7e3897283f5082a892b67db397c0738#994c3fcdb7e3897283f5082a892b67db397c0738" dependencies = [ "const-oid 0.9.6", "der 0.7.10", - "getrandom 0.3.4", + "getrandom 0.4.3", "hex", "hubpack", - "rats-corim 0.1.0 (git+https://github.com/oxidecomputer/rats-corim)", + "rats-corim 0.1.0 (git+https://github.com/oxidecomputer/rats-corim?rev=1493d7f58e189125641d81f03f2dfb5941567c82)", "salty", "serde", "serde_with 3.21.0", @@ -476,15 +476,15 @@ dependencies = [ [[package]] name = "attest-mock" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dice-util?rev=4a39ef08d81e5177edee0bddb1146032aa21074d#4a39ef08d81e5177edee0bddb1146032aa21074d" +source = "git+https://github.com/oxidecomputer/dice-util?rev=994c3fcdb7e3897283f5082a892b67db397c0738#994c3fcdb7e3897283f5082a892b67db397c0738" dependencies = [ "anyhow", - "attest-data 0.5.0 (git+https://github.com/oxidecomputer/dice-util?rev=4a39ef08d81e5177edee0bddb1146032aa21074d)", + "attest-data 0.5.0 (git+https://github.com/oxidecomputer/dice-util?rev=994c3fcdb7e3897283f5082a892b67db397c0738)", "clap", "hex", "hubpack", "knus", - "rats-corim 0.1.0 (git+https://github.com/oxidecomputer/rats-corim)", + "rats-corim 0.1.0 (git+https://github.com/oxidecomputer/rats-corim?rev=1493d7f58e189125641d81f03f2dfb5941567c82)", "slog", "slog-error-chain 0.1.0 (git+https://github.com/oxidecomputer/slog-error-chain?rev=15f69041f45774602108e47fb25e705dc23acfb2)", "thiserror 2.0.18", @@ -3002,21 +3002,25 @@ dependencies = [ [[package]] name = "dice-verifier" version = "0.3.0-pre0" -source = "git+https://github.com/oxidecomputer/dice-util?rev=ff9f27aa0d6ef6fb64c349890b6e3c242ea3d8fc#ff9f27aa0d6ef6fb64c349890b6e3c242ea3d8fc" +source = "git+https://github.com/oxidecomputer/dice-util?rev=994c3fcdb7e3897283f5082a892b67db397c0738#994c3fcdb7e3897283f5082a892b67db397c0738" dependencies = [ - "attest-data 0.5.0 (git+https://github.com/oxidecomputer/dice-util?rev=ff9f27aa0d6ef6fb64c349890b6e3c242ea3d8fc)", + "anyhow", + "async-trait", + "attest-data 0.5.0 (git+https://github.com/oxidecomputer/dice-util?rev=994c3fcdb7e3897283f5082a892b67db397c0738)", + "cfg-if", "const-oid 0.9.6", + "der 0.7.10", "ed25519-dalek 2.2.0", - "env_logger", + "helios-rot", "hex", "hubpack", - "libipcc 0.1.0 (git+https://github.com/oxidecomputer/ipcc-rs?rev=7cdf2ab9c8d9e9267a8b366aa780c6c26f9a5ecf)", - "log", "p384 0.13.1", - "rats-corim 0.1.0 (git+https://github.com/oxidecomputer/rats-corim)", + "rats-corim 0.1.0 (git+https://github.com/oxidecomputer/rats-corim?rev=1493d7f58e189125641d81f03f2dfb5941567c82)", + "rsa 0.9.10", + "serde", + "serde_with 3.21.0", + "sha2 0.10.9", "sha3 0.10.8", - "slog", - "tempfile", "thiserror 2.0.18", "x509-cert", ] @@ -4967,6 +4971,25 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "helios-rot" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/helios-rot?rev=020eb50ba6ddc92cf059b35e989da7bd19769749#020eb50ba6ddc92cf059b35e989da7bd19769749" +dependencies = [ + "anyhow", + "async-trait", + "cfg-if", + "der 0.7.10", + "getrandom 0.4.3", + "os-rot", + "p384 0.13.1", + "serde", + "serde_with 3.21.0", + "thiserror 2.0.18", + "tokio", + "x509-cert", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -6562,6 +6585,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "libipcc" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/ipcc-rs?rev=91f8c989f1cdc29c56720ab7663afdcaf5a96b72#91f8c989f1cdc29c56720ab7663afdcaf5a96b72" +dependencies = [ + "cfg-if", + "libc", + "thiserror 1.0.69", +] + [[package]] name = "libloading" version = "0.8.9" @@ -9922,6 +9955,7 @@ dependencies = [ "assert_matches", "async-trait", "atomicwrites", + "attest-mock", "base64 0.22.1", "bootstore", "bootstrap-agent-lockstep-api", @@ -9937,7 +9971,7 @@ dependencies = [ "crucible-agent-client", "crucible-client-types 0.1.0 (git+https://github.com/oxidecomputer/crucible?rev=f85cf0dc3631e2f0c13384787efb0efc1b642162)", "derive_more 0.99.20", - "dice-verifier 0.3.0-pre0 (git+https://github.com/oxidecomputer/dice-util?rev=ff9f27aa0d6ef6fb64c349890b6e3c242ea3d8fc)", + "dice-verifier 0.3.0-pre0 (git+https://github.com/oxidecomputer/dice-util?rev=994c3fcdb7e3897283f5082a892b67db397c0738)", "display-error-chain", "dropshot 0.18.0", "expectorate", @@ -9979,6 +10013,7 @@ dependencies = [ "omicron-uuid-kinds", "omicron-workspace-hack", "opte-ioctl", + "oxide-rot", "oxide-tokio-rt", "oximeter", "oximeter-instruments", @@ -10027,6 +10062,7 @@ dependencies = [ "smf 0.2.3", "sp-sim", "sprockets-tls", + "sprockets-tls-test-utils", "strum 0.27.2", "subprocess", "tar", @@ -10472,6 +10508,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "os-rot" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/helios-rot?rev=020eb50ba6ddc92cf059b35e989da7bd19769749#020eb50ba6ddc92cf059b35e989da7bd19769749" +dependencies = [ + "rustix 1.1.4", + "thiserror 2.0.18", +] + [[package]] name = "oso" version = "0.27.3" @@ -10562,6 +10607,30 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "oxide-rot" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/dice-util?rev=994c3fcdb7e3897283f5082a892b67db397c0738#994c3fcdb7e3897283f5082a892b67db397c0738" +dependencies = [ + "anyhow", + "async-trait", + "attest-data 0.5.0 (git+https://github.com/oxidecomputer/dice-util?rev=994c3fcdb7e3897283f5082a892b67db397c0738)", + "cfg-if", + "der 0.7.10", + "ed25519-dalek 2.2.0", + "hubpack", + "libipcc 0.1.0 (git+https://github.com/oxidecomputer/ipcc-rs?rev=91f8c989f1cdc29c56720ab7663afdcaf5a96b72)", + "p384 0.13.1", + "serde", + "serde_with 3.21.0", + "sha3 0.10.8", + "slog", + "tempfile", + "thiserror 2.0.18", + "tokio", + "x509-cert", +] + [[package]] name = "oxide-tokio-rt" version = "0.1.4" @@ -12734,6 +12803,21 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "rats-corim" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/rats-corim?rev=1493d7f58e189125641d81f03f2dfb5941567c82#1493d7f58e189125641d81f03f2dfb5941567c82" +dependencies = [ + "ciborium", + "ciborium-io", + "clap", + "hex", + "serde", + "serde_with 3.21.0", + "strum 0.26.3", + "thiserror 2.0.18", +] + [[package]] name = "rats-corim" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b5743480f5a..30720208a11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -455,7 +455,7 @@ assert_cmd = "2.0.17" async-bb8-diesel = "0.2" async-recursion = "1.1.1" async-trait = "0.1.89" -attest-mock = { git = "https://github.com/oxidecomputer/dice-util", rev = "4a39ef08d81e5177edee0bddb1146032aa21074d" } +attest-mock = { git = "https://github.com/oxidecomputer/dice-util", rev = "994c3fcdb7e3897283f5082a892b67db397c0738" } atomicwrites = "0.4.4" authz-macros = { path = "nexus/authz-macros" } backoff = { version = "0.4.0", features = [ "tokio" ] } @@ -522,7 +522,7 @@ debug-ignore = "1.0.5" derive_more = "0.99.20" derive-where = "1.5.0" dev-tools-common = { path = "dev-tools/common" } -dice-verifier = { git = "https://github.com/oxidecomputer/dice-util", rev = "ff9f27aa0d6ef6fb64c349890b6e3c242ea3d8fc", default-features = false } +dice-verifier = { git = "https://github.com/oxidecomputer/dice-util", rev = "994c3fcdb7e3897283f5082a892b67db397c0738", default-features = false } # Having the i-implement-... feature here makes diesel go away from the workspace-hack diesel = { version = "2.3.7", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] } diesel-dtrace = "0.5.0" @@ -715,6 +715,7 @@ omicron-workspace-hack = "0.1.0" omicron-zone-package = "0.12.3" oxide-client = { path = "clients/oxide-client" } oxide-tokio-rt = "0.1.4" +oxide-rot = { git = "https://github.com/oxidecomputer/dice-util", rev = "994c3fcdb7e3897283f5082a892b67db397c0738", default-features = false } oxide-update-engine = "0.1.2" oxide-update-engine-types = { version = "0.1.2", features = ["schemars08"] } oxide-update-engine-display = "0.1.2" diff --git a/sled-agent/Cargo.toml b/sled-agent/Cargo.toml index 57bf0007d04..380e5aaa865 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -29,7 +29,6 @@ chrono.workspace = true clap.workspace = true clickhouse-admin-types.workspace = true derive_more.workspace = true -dice-verifier = { workspace = true, features = ["ipcc", "mock"] } display-error-chain.workspace = true dropshot.workspace = true flate2.workspace = true @@ -63,6 +62,7 @@ omicron-ledger.workspace = true omicron-ddm-admin-client.workspace = true omicron-uuid-kinds.workspace = true omicron-workspace-hack.workspace = true +oxide-rot = { workspace = true, features = ["ipcc", "mock"] } oxide-tokio-rt.workspace = true oximeter.workspace = true oximeter-instruments.workspace = true @@ -142,6 +142,8 @@ opte-ioctl.workspace = true [dev-dependencies] assert_matches.workspace = true +attest-mock.workspace = true +dice-verifier.workspace = true expectorate.workspace = true guppy.workspace = true hex-literal.workspace = true @@ -156,6 +158,7 @@ reqwest = { workspace = true, features = ["blocking"] } subprocess.workspace = true slog-async.workspace = true slog-term.workspace = true +sprockets-tls-test-utils.workspace = true test-strategy.workspace = true tokio-stream.workspace = true tokio-util.workspace = true diff --git a/sled-agent/src/rot.rs b/sled-agent/src/rot.rs index 1f9ea0cf17b..30987848969 100644 --- a/sled-agent/src/rot.rs +++ b/sled-agent/src/rot.rs @@ -4,23 +4,20 @@ //! Mechanisms for interacting with the sled's RoT. -use dice_verifier::{Attest, AttestError, ipcc, mock}; use dropshot::HttpError; +use oxide_rot::{OxideRot, OxideRotError, ipcc, mock}; use sled_agent_types::rot as SaRotTypes; use slog::Logger; use slog_error_chain::InlineErrorChain; use sprockets_tls::keys::AttestConfig; use thiserror::Error; -use tokio::sync::{ - mpsc::{self, error::TrySendError}, - oneshot, -}; +use tokio::sync::{Mutex, MutexGuard, Semaphore, SemaphorePermit}; use x509_cert::der::{EncodePem, pem::LineEnding}; -pub struct MeasurementLog(dice_verifier::Log); +pub struct MeasurementLog(oxide_rot::Log); -impl From for MeasurementLog { - fn from(log: dice_verifier::Log) -> Self { +impl From for MeasurementLog { + fn from(log: oxide_rot::Log) -> Self { MeasurementLog(log) } } @@ -32,7 +29,7 @@ impl From for SaRotTypes::MeasurementLog { .iter() .copied() .map(|m| match m { - dice_verifier::Measurement::Sha3_256(d) => { + oxide_rot::Measurement::Sha3_256(d) => { SaRotTypes::Measurement::Sha3_256( SaRotTypes::Sha3_256Digest(d.0), ) @@ -61,22 +58,22 @@ impl From for SaRotTypes::CertificateChain { } } -pub struct Nonce(dice_verifier::Nonce); +pub struct QualifyingData(oxide_rot::QualifyingData); -impl From for Nonce { +impl From for QualifyingData { fn from(nonce: SaRotTypes::Nonce) -> Self { match nonce { SaRotTypes::Nonce::N32(n32) => { - Nonce(dice_verifier::Nonce::N32(n32.into())) + QualifyingData(oxide_rot::QualifyingData::Q32(n32.into())) } } } } -pub struct Attestation(dice_verifier::Attestation); +pub struct Attestation(oxide_rot::Attestation); -impl From for Attestation { - fn from(att: dice_verifier::Attestation) -> Self { +impl From for Attestation { + fn from(att: oxide_rot::Attestation) -> Self { Attestation(att) } } @@ -84,7 +81,7 @@ impl From for Attestation { impl From for SaRotTypes::Attestation { fn from(att: Attestation) -> Self { match att.0 { - dice_verifier::Attestation::Ed25519(sig) => { + oxide_rot::Attestation::Ed25519(sig) => { SaRotTypes::Attestation::Ed25519(SaRotTypes::Ed25519Signature( sig.0, )) @@ -95,38 +92,20 @@ impl From for SaRotTypes::Attestation { #[derive(Debug, Error)] pub enum RotError { - #[error("failed to create IPCC attestor: {0}")] - AttestIpcc(#[from] dice_verifier::ipcc::IpccError), - #[error("failed to create mock attestor: {0}")] - AttestMock(#[from] mock::AttestMockError), + AttestMock(#[from] mock::OxideRotMockError), #[error("attestation request failed: {0}")] - Attest(#[from] AttestError), + Attest(#[from] OxideRotError), #[error("RoT attestation queue full")] Busy, - - #[error("RoT attestation task gone - request cancelled")] - Shutdown, -} - -impl From> for RotError { - fn from(e: TrySendError) -> Self { - match e { - // Given the relatively large queue size, we always attempt to - // send the attestation messages via `try_send()` instead of - // potentially blocking forever. - TrySendError::Full(_) => RotError::Busy, - TrySendError::Closed(_) => RotError::Shutdown, - } - } } impl From for HttpError { fn from(e: RotError) -> Self { match e { - RotError::Busy | RotError::Shutdown => HttpError::for_unavail( + RotError::Busy => HttpError::for_unavail( None, InlineErrorChain::new(&e).to_string(), ), @@ -137,136 +116,231 @@ impl From for HttpError { } } -/// Depth of the request queue for Sled Agent to the RoT. +/// Maximum number of outstanding RoT requests. const QUEUE_SIZE: usize = 256; -type AttestationResponseTx = oneshot::Sender>; - -enum AttestationMessage { - GetMeasurementLog(AttestationResponseTx), - GetCertificateChain(AttestationResponseTx), - Attest(Nonce, AttestationResponseTx), +/// `RotAttestor` mediates access to the Oxide RoT on the sled +/// +/// Attestation requests (on a real sled) are proxied through a few layers: +/// Sled Agent ([`RotAttestor`]) <--(via IPCC)--> SP <-> RoT. The RoT services +/// a single attestation request at a time, so we serialize requests here +/// rather than having concurrent callers contend for IPCC. At most `QUEUE_SIZE` +/// requests may be outstanding at once. Requests made beyond this limit fail +/// immediately with [`RotError::Busy`]. +pub struct RotAttestor { + log: Logger, + queue: Semaphore, + attest: Mutex>, } -#[derive(Debug)] -pub struct RotAttestationHandle { - tx: mpsc::Sender, -} +impl RotAttestor { + pub fn new( + log: &Logger, + attest_config: &AttestConfig, + ) -> Result { + let log = log.new(o!( + "component" => "RotAttestor", + "interface" => match attest_config { + AttestConfig::Ipcc => "ipcc", + AttestConfig::Local { .. } => "mock", + } + )); + + let attest: Box = match attest_config { + AttestConfig::Ipcc => Box::new(ipcc::OxideRotIpcc::new()), + AttestConfig::Local { + priv_key, + cert_chain, + log, + test_corpus: _, + } => Box::new(mock::OxideRotMock::load(cert_chain, log, priv_key)?), + }; + + Ok(RotAttestor { + log, + queue: Semaphore::new(QUEUE_SIZE), + attest: Mutex::new(attest), + }) + } + + /// Wait for exclusive access to the attestor, reserving a queue slot for + /// the duration of the request. Fails with [`RotError::Busy`] if the + /// queue is full. + async fn acquire(&self) -> Result, RotError> { + let _permit = self.queue.try_acquire().map_err(|_| RotError::Busy)?; + let attest = self.attest.lock().await; + Ok(AttestGuard { _permit, attest }) + } -impl RotAttestationHandle { pub async fn get_measurement_log( &self, ) -> Result { - let (tx, rx) = oneshot::channel(); - self.tx.try_send(AttestationMessage::GetMeasurementLog(tx))?; - Ok(rx.await.map_err(|_| RotError::Shutdown)??) + let guard = self.acquire().await?; + debug!(self.log, "getting measurement log"); + Ok(guard.attest.get_measurement_log().await?.into()) } pub async fn get_certificate_chain( &self, ) -> Result { - let (tx, rx) = oneshot::channel(); - self.tx.try_send(AttestationMessage::GetCertificateChain(tx))?; - Ok(rx.await.map_err(|_| RotError::Shutdown)??) + let guard = self.acquire().await?; + debug!(self.log, "getting certificate chain"); + let chain = guard.attest.get_certificates().await?; + Ok(CertificateChain::try_from(chain).map_err(OxideRotError::from)?) } - pub async fn attest(&self, nonce: Nonce) -> Result { - let (tx, rx) = oneshot::channel(); - self.tx.try_send(AttestationMessage::Attest(nonce, tx))?; - Ok(rx.await.map_err(|_| RotError::Shutdown)??) + pub async fn attest( + &self, + qdata: QualifyingData, + ) -> Result { + let guard = self.acquire().await?; + debug!(self.log, "getting attestation"); + Ok(guard.attest.attest(&qdata.0).await?.into()) } } -/// Mediates access to the Oxide RoT on the current sled. -/// -/// The semantics are: -/// 1. Attestation requests (on a real sled) are proxied through a few layers: -/// Sled Agent ([`RotAttestationTask`]) <--(via IPCC)--> SP <-> RoT. -/// Those IPCC calls are made via an IOCTL (via [`ipcc::AttestIpcc`]) and -/// thus should be treated as "blocking I/O". -/// 2. There is only a single outstanding attestation request at any point. -/// 3. (1) + (2) leads us to service requests (via [`RotAttestationHandle`]) -/// from a single queue which is drained by a task started via -/// `spawn_blocking()` -- [`RotAttestationTask::run`]. -pub struct RotAttestationTask { - log: Logger, - rx: mpsc::Receiver, - attest: Box, +/// `AttestGuard` represents exclusive access to the attestor +struct AttestGuard<'a> { + _permit: SemaphorePermit<'a>, + attest: MutexGuard<'a, Box>, } -impl RotAttestationTask { - /// Creates and launches the RoT Attestation task with the given config. - /// On success, a handle is returned to use for submitting attestation - /// requests. - pub fn launch( - log: &Logger, - attest_config: &AttestConfig, - ) -> Result { - let (task, handle) = Self::new(log, attest_config)?; - tokio::task::spawn_blocking(move || task.run()); - Ok(handle) +#[cfg(test)] +mod tests { + use super::*; + use attest_mock::MockData; + use camino::Utf8Path; + use camino_tempfile::Utf8TempDir; + use dice_verifier::oxide_rot::verify_attestation; + use omicron_test_utils::dev::test_setup_log; + use sprockets_tls_test_utils::{ + OutputFileExistsBehavior, alias_prefix, certlist_path, private_key_path, + }; + use x509_cert::{Certificate, der::DecodePem}; + + // An arbitrary digest for the single measurement in the mock log. + const DIGEST: &str = + "be4df4e085175f3de0c8ac4837e1c2c9a34e8983209dac6b549e94154f7cdd9c"; + + /// Generate an `AttestConfig from `sprockets_tls::keys` w/ artifacts + /// required to mock the Oxide RoT + fn gen_mock_config(dir: &Utf8Path) -> AttestConfig { + // tests in this module need a single cert chain to represent the + // attestation signer + const LEAF_COUNT: usize = 1; + + let behavior = OutputFileExistsBehavior::Overwrite; + + let doc = sprockets_tls_test_utils::generate_config(LEAF_COUNT); + doc.write_key_pairs(dir.to_owned(), behavior).unwrap(); + doc.write_certificates(dir.to_owned(), behavior).unwrap(); + doc.write_certificate_lists(dir.to_owned(), behavior).unwrap(); + + let log_kdl = format!( + r#" + measurement {{ + algorithm "sha3-256" + digest "{DIGEST}" + }} + "# + ); + + let log = attest_mock::MockLog::parse("log.kdl", &log_kdl) + .unwrap() + .to_bytes() + .unwrap(); + std::fs::write(dir.join("log.bin"), &log).unwrap(); + + // sprockets test utils / pki-playground append a positive integer to + // the file name of the generated leaf. We've only created a single + // leaf & cert chain so this must be `1` + let alias = alias_prefix(LEAF_COUNT); + + AttestConfig::Local { + priv_key: private_key_path(dir.to_owned(), &alias), + cert_chain: certlist_path(dir.to_owned(), &alias), + log: dir.join("log.bin"), + test_corpus: vec![], + } } - fn new( - log: &Logger, - attest_config: &AttestConfig, - ) -> Result<(RotAttestationTask, RotAttestationHandle), RotError> { - let log = log.new(o!( - "component" => "RotAttestationTask", - "interface" => match attest_config { - AttestConfig::Ipcc => "ipcc", - AttestConfig::Local { .. } => "mock", - } + /// Use the `RotAttestor` to get and appraise an attestation + #[tokio::test] + async fn rot_attestor_verify_attestation() { + let logctx = test_setup_log("rot_attestor_verify_attestation"); + let tmp = Utf8TempDir::new().unwrap(); + let cfg = gen_mock_config(tmp.path()); + + // Get artifacts required to verify the attestation + let attestor = RotAttestor::new(&logctx.log, &cfg).unwrap(); + + let log = attestor.get_measurement_log().await.unwrap(); + let chain = attestor.get_certificate_chain().await.unwrap(); + let qdata: QualifyingData = SaRotTypes::Nonce::N32([7u8; 32]).into(); + let attestation = + attestor.attest(QualifyingData(qdata.0)).await.unwrap(); + + // The first cert in the chain is the leaf. This cert holds the public + // part of the key used to sign the attestation. + let alias = Certificate::from_pem(&chain.0[0]).unwrap(); + verify_attestation(&alias, &attestation.0, &log.0, &qdata.0) + .expect("attestation should verify against the alias cert"); + + let log: SaRotTypes::MeasurementLog = log.into(); + assert_eq!(log.0.len(), 1); + + let SaRotTypes::Measurement::Sha3_256(digest) = &log.0[0]; + assert_eq!(hex::encode(digest.0), DIGEST); + + let chain: SaRotTypes::CertificateChain = chain.into(); + assert!(!chain.0.is_empty()); + assert!( + chain + .0 + .iter() + .all(|c| c.starts_with("-----BEGIN CERTIFICATE-----")) + ); + + let SaRotTypes::Attestation::Ed25519(sig) = attestation.into(); + assert_eq!(sig.0.len(), 64); + + logctx.cleanup_successful(); + } + + /// Ensure new requests are rejected when the request queue is full + #[tokio::test] + async fn rot_attestor_queue_full() { + let logctx = test_setup_log("rot_attestor_queue_full"); + let tmp = Utf8TempDir::new().unwrap(); + let cfg = gen_mock_config(tmp.path()); + let attestor = RotAttestor::new(&logctx.log, &cfg).unwrap(); + + // fill request queue + attestor.queue.try_acquire_many(QUEUE_SIZE as u32).unwrap().forget(); + + // Request queue is full, additional requests are rejected + assert!(matches!( + attestor.get_measurement_log().await, + Err(RotError::Busy) + )); + assert!(matches!( + attestor.get_certificate_chain().await, + Err(RotError::Busy) )); + let qdata: QualifyingData = SaRotTypes::Nonce::N32([7u8; 32]).into(); + assert!(matches!(attestor.attest(qdata).await, Err(RotError::Busy))); - let attest: Box = match attest_config { - AttestConfig::Ipcc => Box::new(ipcc::AttestIpcc::new()?), - AttestConfig::Local { - priv_key, - cert_chain, - log, - test_corpus: _, - } => Box::new(mock::AttestMock::load(cert_chain, log, priv_key)?), - }; + // RotError::Busy is reported to clients as 503 / Service Unavailable + let err: HttpError = RotError::Busy.into(); + assert_eq!( + err.status_code.as_status(), + http::StatusCode::SERVICE_UNAVAILABLE + ); - let (tx, rx) = mpsc::channel(QUEUE_SIZE); - Ok(( - RotAttestationTask { log, rx, attest }, - RotAttestationHandle { tx }, - )) - } + // Once the outstanding requests complete, new ones succeed. + attestor.queue.add_permits(QUEUE_SIZE); + assert!(attestor.get_measurement_log().await.is_ok()); - /// Run the main request handler loop that processes incoming RoT - /// attestation requests. - /// - /// This should be run via `spawn_blocking` as we perform ipcc operations - /// via an ioctl and don't want to block any other tasks. - fn run(mut self) { - info!(self.log, "request loop started."); - loop { - let Some(req) = self.rx.blocking_recv() else { - warn!(self.log, "All senders dropped. Exiting."); - break; - }; - - match req { - AttestationMessage::GetMeasurementLog(reply_tx) => { - let log = self.attest.get_measurement_log(); - let _ = reply_tx.send(log.map(Into::into)); - } - AttestationMessage::GetCertificateChain(reply_tx) => { - let chain = - self.attest.get_certificates().and_then(|chain| { - CertificateChain::try_from(chain) - .map_err(AttestError::from) - }); - let _ = reply_tx.send(chain); - } - AttestationMessage::Attest(nonce, reply_tx) => { - let attestation = self.attest.attest(&nonce.0); - let _ = reply_tx.send(attestation.map(Into::into)); - } - } - } + logctx.cleanup_successful(); } } diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 6439e0f2af4..a045cf7feea 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -15,7 +15,7 @@ use crate::nexus::{ NexusClient, NexusNotifierHandle, NexusNotifierInput, NexusNotifierTask, }; use crate::probe_manager::ProbeManager; -use crate::rot::{RotAttestationHandle, RotAttestationTask}; +use crate::rot::RotAttestor; use crate::services::{self, ServiceManager, SledAgentInfo}; use crate::support_bundle::logs::SupportBundleLogs; use crate::support_bundle::storage::SupportBundleManager; @@ -498,7 +498,7 @@ struct SledAgentInner { nexus_notifier: NexusNotifierHandle, // A handle to the RoT for attestation requests. - rot_attestor: RotAttestationHandle, + rot_attestor: RotAttestor, // Object managing zone bundles. zone_bundler: zone_bundle::ZoneBundler, @@ -851,9 +851,7 @@ impl SledAgent { nexus_notifier_task.run().await; }); - // Spawn a background task for handling RoT attestation operations - let rot_attest_handle = - RotAttestationTask::launch(&log, &config.sprockets.attest)?; + let rot_attestor = RotAttestor::new(&log, &config.sprockets.attest)?; let currently_managed_zpools_rx = config_reconciler.currently_managed_zpools_rx().clone(); @@ -878,7 +876,7 @@ impl SledAgent { port_manager, services, nexus_notifier: nexus_notifier_handle, - rot_attestor: rot_attest_handle, + rot_attestor, zone_bundler: long_running_task_handles.zone_bundler.clone(), bootstore: long_running_task_handles.bootstore.clone(), trust_quorum: long_running_task_handles.trust_quorum.clone(), @@ -953,7 +951,7 @@ impl SledAgent { &self, rot: Rot, remote_addr: SocketAddr, - ) -> Result<&RotAttestationHandle, HttpError> { + ) -> Result<&RotAttestor, HttpError> { // We currently only support the LPC55 RoT let Rot::Oxide = rot; // And we only serve sled-local clients