diff --git a/Cargo.lock b/Cargo.lock index 928491968ce..b21a2e8c2fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1164,7 +1164,7 @@ dependencies = [ "rustls", "tokio", "tokio-rustls", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -1705,6 +1705,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bytes", "cfg-if", "chrono", "dropshot", @@ -1726,14 +1727,18 @@ dependencies = [ "serde_json", "serial_test", "slog", + "slog-async", + "slog-term", "smf", "socket2", + "spdm", "structopt", "subprocess", "tar", "tempfile", "thiserror", "tokio", + "tokio-util", "toml", "uuid", "zone", @@ -2705,7 +2710,7 @@ dependencies = [ "log", "ring", "sct", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -3176,6 +3181,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "spdm" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/spdm?rev=9742f6e#9742f6eae7b86cc8bc8bc2fb0feeb44f770a1fb6" +dependencies = [ + "bitflags", + "rand", + "ring", + "webpki 0.22.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -3632,7 +3648,7 @@ checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls", "tokio", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -4112,13 +4128,23 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki", + "webpki 0.21.4", ] [[package]] diff --git a/sled-agent/Cargo.toml b/sled-agent/Cargo.toml index 9175f613688..f929cdaf6be 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -7,6 +7,7 @@ license = "MPL-2.0" [dependencies] anyhow = "1.0.51" async-trait = "0.1.51" +bytes = "1.1" cfg-if = "1.0" chrono = { version = "0.4", features = [ "serde" ] } dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main" } @@ -23,12 +24,14 @@ serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" slog = { version = "2.5", features = [ "max_level_trace", "release_max_level_debug" ] } smf = "0.2" +spdm = { git = "https://github.com/oxidecomputer/spdm", rev = "9742f6e" } socket2 = { version = "0.4", features = [ "all" ] } structopt = "0.3" tar = "0.4" tempfile = "3.2" thiserror = "1.0" tokio = { version = "1.14", features = [ "full" ] } +tokio-util = { version = "0.6", features = ["codec"] } toml = "0.5.6" uuid = { version = "0.8", features = [ "serde", "v4" ] } zone = "0.1" @@ -41,6 +44,8 @@ openapi-lint = { git = "https://github.com/oxidecomputer/openapi-lint", branch = openapiv3 = "0.5.0" serial_test = "0.5" subprocess = "0.2.8" +slog-async = "2.6" +slog-term = "2.8" # # Disable doc builds by default for our binaries to work around issue diff --git a/sled-agent/src/bootstrap/agent.rs b/sled-agent/src/bootstrap/agent.rs index 75dea014406..7a2264aec08 100644 --- a/sled-agent/src/bootstrap/agent.rs +++ b/sled-agent/src/bootstrap/agent.rs @@ -7,6 +7,7 @@ use super::client::types as bootstrap_types; use super::client::Client as BootstrapClient; use super::discovery; +use super::spdm::SpdmError; use super::views::ShareResponse; use omicron_common::api::external::Error as ExternalError; use omicron_common::backoff::{ @@ -46,6 +47,9 @@ pub enum BootstrapError { #[error("Error making HTTP request")] Api(#[from] anyhow::Error), + #[error("Error running SPDM protocol: {0}")] + Spdm(#[from] SpdmError), + #[error("Not enough peers to unlock storage")] NotEnoughPeers, } diff --git a/sled-agent/src/bootstrap/mod.rs b/sled-agent/src/bootstrap/mod.rs index 9d3e47d21a6..a6255398e56 100644 --- a/sled-agent/src/bootstrap/mod.rs +++ b/sled-agent/src/bootstrap/mod.rs @@ -12,4 +12,5 @@ mod http_entrypoints; mod multicast; mod params; pub mod server; +mod spdm; mod views; diff --git a/sled-agent/src/bootstrap/spdm/error.rs b/sled-agent/src/bootstrap/spdm/error.rs new file mode 100644 index 00000000000..9e2477ced19 --- /dev/null +++ b/sled-agent/src/bootstrap/spdm/error.rs @@ -0,0 +1,32 @@ +//! Wrap errors returned from the `spdm` crate and std::io::Error. + +use spdm::{requester::RequesterError, responder::ResponderError}; +use thiserror::Error; + +/// Describes errors that arise from use of the SPDM protocol library +#[derive(Error, Debug)] +pub enum SpdmError { + #[error("requester error: {0}")] + Requester(RequesterError), + + #[error("responder error: {0}")] + Responder(ResponderError), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error("invalid state transition: expected {expected}, got {got}")] + InvalidState { expected: &'static str, got: &'static str }, +} + +impl From for SpdmError { + fn from(e: RequesterError) -> Self { + SpdmError::Requester(e) + } +} + +impl From for SpdmError { + fn from(e: ResponderError) -> Self { + SpdmError::Responder(e) + } +} diff --git a/sled-agent/src/bootstrap/spdm/mod.rs b/sled-agent/src/bootstrap/spdm/mod.rs new file mode 100644 index 00000000000..3303249eeed --- /dev/null +++ b/sled-agent/src/bootstrap/spdm/mod.rs @@ -0,0 +1,57 @@ +//! Instantiate a SPDM requester and responder with particular capabilities, +//! algorithms, and credentials. +//! +//! Sled agents run the SPDM protocol over a tokio TCP stream with a 2 byte size +//! header for framing. + +mod error; +mod requester; +mod responder; + +use std::io::{Error, ErrorKind}; + +use bytes::{Bytes, BytesMut}; +use futures::{SinkExt, StreamExt}; +use slog::Logger; +use tokio::net::TcpStream; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; + +// 2^16 - 2 bytes for a header +const MAX_BUF_SIZE: usize = 65534; + +pub use error::SpdmError; + +pub struct Transport { + framed: Framed, +} + +impl Transport { + // We use 2-byte size framed headers. + #[allow(dead_code)] + pub const HEADER_LEN: usize = 2; + + #[allow(dead_code)] + pub fn new(sock: TcpStream) -> Transport { + Transport { + framed: LengthDelimitedCodec::builder() + .length_field_length(Self::HEADER_LEN) + .new_framed(sock), + } + } + + pub async fn send(&mut self, data: &[u8]) -> Result<(), SpdmError> { + let data = Bytes::copy_from_slice(data); + self.framed.send(data).await.map_err(|e| e.into()) + } + + pub async fn recv(&mut self, log: &Logger) -> Result { + if let Some(rsp) = self.framed.next().await { + let rsp = rsp?; + debug!(log, "Received {:x?}", &rsp[..]); + Ok(rsp) + } else { + Err(Error::new(ErrorKind::ConnectionAborted, "SPDM channel closed") + .into()) + } + } +} diff --git a/sled-agent/src/bootstrap/spdm/requester.rs b/sled-agent/src/bootstrap/spdm/requester.rs new file mode 100644 index 00000000000..32974754f88 --- /dev/null +++ b/sled-agent/src/bootstrap/spdm/requester.rs @@ -0,0 +1,172 @@ +use slog::Logger; + +use spdm::msgs::algorithms::*; +use spdm::msgs::capabilities::{GetCapabilities, ReqFlags}; +use spdm::requester::{self, algorithms, capabilities, id_auth}; +use spdm::{ + config::{MAX_CERT_CHAIN_SIZE, NUM_SLOTS}, + Transcript, +}; + +use super::{SpdmError, Transport, MAX_BUF_SIZE}; + +// A `Ctx` contains shared types for use by a requester task +struct Ctx { + buf: [u8; MAX_BUF_SIZE], + log: Logger, + transport: Transport, + transcript: Transcript, +} + +impl Ctx { + fn new(log: Logger, transport: Transport) -> Ctx { + Ctx { + buf: [0u8; MAX_BUF_SIZE], + log, + transport, + transcript: Transcript::new(), + } + } + + async fn negotiate_version( + &mut self, + ) -> Result { + let state = requester::start(); + let data = + state.write_get_version(&mut self.buf, &mut self.transcript)?; + + debug!(self.log, "Requester sending GET_VERSION"); + self.transport.send(data).await?; + + let rsp = self.transport.recv(&self.log).await?; + debug!(self.log, "Requester received VERSION"); + + state.handle_msg(&rsp[..], &mut self.transcript).map_err(|e| e.into()) + } + + async fn negotiate_capabilities( + &mut self, + mut state: capabilities::State, + ) -> Result { + let req = GetCapabilities { + ct_exponent: 12, + flags: ReqFlags::CERT_CAP + | ReqFlags::CHAL_CAP + | ReqFlags::ENCRYPT_CAP + | ReqFlags::MAC_CAP + | ReqFlags::MUT_AUTH_CAP + | ReqFlags::KEY_EX_CAP + | ReqFlags::ENCAP_CAP + | ReqFlags::HBEAT_CAP + | ReqFlags::KEY_UPD_CAP, + }; + + debug!(self.log, "Requester sending GET_CAPABILITIES"); + let data = + state.write_msg(&req, &mut self.buf, &mut self.transcript)?; + self.transport.send(data).await?; + + let rsp = self.transport.recv(&self.log).await?; + debug!(self.log, "Requester received CAPABILITIES"); + state.handle_msg(&rsp, &mut self.transcript).map_err(|e| e.into()) + } + + async fn negotiate_algorithms( + &mut self, + mut state: algorithms::State, + ) -> Result { + let req = NegotiateAlgorithms { + measurement_spec: MeasurementSpec::DMTF, + base_asym_algo: BaseAsymAlgo::ECDSA_ECC_NIST_P256, + base_hash_algo: BaseHashAlgo::SHA_256, + num_algorithm_requests: 4, + algorithm_requests: [ + AlgorithmRequest::Dhe(DheAlgorithm { + supported: DheFixedAlgorithms::SECP_256_R1, + }), + AlgorithmRequest::Aead(AeadAlgorithm { + supported: AeadFixedAlgorithms::AES_256_GCM, + }), + AlgorithmRequest::ReqBaseAsym(ReqBaseAsymAlgorithm { + supported: ReqBaseAsymFixedAlgorithms::ECDSA_ECC_NIST_P256, + }), + AlgorithmRequest::KeySchedule(KeyScheduleAlgorithm { + supported: KeyScheduleFixedAlgorithms::SPDM, + }), + ], + }; + + debug!(self.log, "Requester sending NEGOTIATE_ALGORITHMS"); + let data = state.write_msg(req, &mut self.buf, &mut self.transcript)?; + self.transport.send(data).await?; + + let rsp = self.transport.recv(&self.log).await?; + debug!(self.log, "Requester received ALGORITHMS"); + + state + .handle_msg::( + &rsp, + &mut self.transcript, + ) + .map_err(|e| e.into()) + } +} + +/// Run the requester side of the SPDM protocol. +/// +/// The protocol operates over a TCP stream framed with a 2 byte size +/// header. Requesters and Responders are decoupled from whether the endpoint of +/// a socket is a TCP client or server. +#[allow(dead_code)] +pub async fn run(log: Logger, transport: Transport) -> Result<(), SpdmError> { + let mut ctx = Ctx::new(log, transport); + + info!(ctx.log, "Requester starting version negotiation"); + let state = ctx.negotiate_version().await?; + + info!(ctx.log, "Requester starting capabilities negotiation"); + let state = ctx.negotiate_capabilities(state).await?; + + info!(ctx.log, "Requester starting algorithms negotiation"); + let _state = ctx.negotiate_algorithms(state).await?; + + info!(ctx.log, "Requester completed negotiation phase"); + debug!(ctx.log, "Requester transcript: {:x?}", ctx.transcript.get()); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use slog::Drain; + use tokio::net::{TcpListener, TcpStream}; + + use super::super::responder; + use super::*; + + #[tokio::test] + async fn negotiation() { + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + let log = slog::Logger::root(drain, o!("component" => "spdm")); + let log2 = log.clone(); + + let addr: SocketAddr = "127.0.0.1:9999".parse().unwrap(); + let listener = TcpListener::bind(addr.clone()).await.unwrap(); + + let handle = tokio::spawn(async move { + let (sock, _) = listener.accept().await.unwrap(); + let transport = Transport::new(sock); + responder::run(log, transport).await.unwrap(); + }); + + let sock = TcpStream::connect(addr).await.unwrap(); + let transport = Transport::new(sock); + run(log2, transport).await.unwrap(); + + handle.await.unwrap(); + } +} diff --git a/sled-agent/src/bootstrap/spdm/responder.rs b/sled-agent/src/bootstrap/spdm/responder.rs new file mode 100644 index 00000000000..780fb4dd1cd --- /dev/null +++ b/sled-agent/src/bootstrap/spdm/responder.rs @@ -0,0 +1,135 @@ +use slog::Logger; + +use spdm::msgs::capabilities::{Capabilities, RspFlags}; +use spdm::responder::{self, algorithms, capabilities, id_auth}; +use spdm::Transcript; + +use super::{SpdmError, Transport, MAX_BUF_SIZE}; + +// A `Ctx` contains shared types for use by a responder task +struct Ctx { + buf: [u8; MAX_BUF_SIZE], + log: Logger, + transport: Transport, + transcript: Transcript, +} + +impl Ctx { + fn new(log: Logger, transport: Transport) -> Ctx { + Ctx { + buf: [0u8; MAX_BUF_SIZE], + log, + transport, + transcript: Transcript::new(), + } + } + + async fn negotiate_version( + &mut self, + ) -> Result { + let state = responder::start(); + let req = self.transport.recv(&self.log).await?; + + let (data, state) = + state.handle_msg(&req[..], &mut self.buf, &mut self.transcript)?; + debug!(self.log, "Responder received GET_VERSION"); + + self.transport.send(data).await?; + debug!(self.log, "Responder sent VERSION"); + Ok(state) + } + + async fn negotiate_capabilities( + &mut self, + state: capabilities::State, + ) -> Result { + let supported = Capabilities { + ct_exponent: 14, + flags: RspFlags::CERT_CAP + | RspFlags::CHAL_CAP + | RspFlags::ENCRYPT_CAP + | RspFlags::MAC_CAP + | RspFlags::MUT_AUTH_CAP + | RspFlags::KEY_EX_CAP + | RspFlags::ENCAP_CAP + | RspFlags::HBEAT_CAP + | RspFlags::KEY_UPD_CAP, + }; + + let req = self.transport.recv(&self.log).await?; + let (data, transition) = state.handle_msg( + supported, + &req[..], + &mut self.buf, + &mut self.transcript, + )?; + debug!(self.log, "Responder received GET_CAPABILITIES"); + + // We expect to transition to the Algorithms state + // TODO: Handle both states + use responder::capabilities::Transition; + let state = match transition { + Transition::Algorithms(state) => state, + _ => { + return Err(SpdmError::InvalidState { + expected: "Algorithms", + got: "Capabilities", + }) + } + }; + + self.transport.send(data).await?; + debug!(self.log, "Responder sent CAPABILITIES"); + Ok(state) + } + + async fn select_algorithms( + &mut self, + state: algorithms::State, + ) -> Result { + let req = self.transport.recv(&self.log).await?; + let (data, transition) = + state.handle_msg(&req[..], &mut self.buf, &mut self.transcript)?; + debug!(self.log, "Responder received NEGOTIATE_ALGORITHMS"); + + // We expect to transition to the Algorithms state + // TODO: Handle both states + use responder::algorithms::Transition; + let state = match transition { + Transition::IdAuth(state) => state, + _ => { + return Err(SpdmError::InvalidState { + expected: "IdAuth", + got: "Capabilities", + }) + } + }; + + self.transport.send(data).await?; + debug!(self.log, "Responder sent ALGORITHMS"); + Ok(state) + } +} + +/// Run the responder side of the SPDM protocol. +/// +/// The protocol operates over a TCP stream framed with a 2 byte size +/// header. Requesters and Responders are decoupled from whether the endpoint of +/// a socket is a TCP client or server. +#[allow(dead_code)] +pub async fn run(log: Logger, transport: Transport) -> Result<(), SpdmError> { + let mut ctx = Ctx::new(log, transport); + + info!(ctx.log, "Responder starting version negotiation"); + let state = ctx.negotiate_version().await?; + + info!(ctx.log, "Responder starting capabilities negotiation"); + let state = ctx.negotiate_capabilities(state).await?; + + info!(ctx.log, "Responder starting algorithms selection"); + let _state = ctx.select_algorithms(state).await?; + + info!(ctx.log, "Responder completed negotiation phase"); + debug!(ctx.log, "Responder transcript: {:x?}\n", ctx.transcript.get()); + Ok(()) +}