Skip to content

Commit de2a8d9

Browse files
committed
sled-agent: add RoT attestation endpoints
1 parent 1c0f2b3 commit de2a8d9

24 files changed

Lines changed: 11014 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 40 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ assert_cmd = "2.0.17"
396396
async-bb8-diesel = "0.2"
397397
async-recursion = "1.1.1"
398398
async-trait = "0.1.89"
399+
attest-data = { git = "https://github.com/oxidecomputer/dice-util", rev = "b04fa144cf810e3e4ecbf2bbab5df920afe76e26" }
399400
attest-mock = { git = "https://github.com/oxidecomputer/dice-util", rev = "10952e8d9599b735b85d480af3560a11700e5b64" }
400401
atomicwrites = "0.4.4"
401402
authz-macros = { path = "nexus/authz-macros" }
@@ -524,6 +525,7 @@ http-body-util = "0.1.3"
524525
http-range = "0.1.5"
525526
httpmock = "0.8.0-alpha.1"
526527
httptest = "0.16.3"
528+
hubpack = "0.1"
527529
hubtools = { git = "https://github.com/oxidecomputer/hubtools.git", rev = "2b1ef9b38d75563ea800baa3b17327eec17b1b7a" }
528530
humantime = "2.2.0"
529531
hyper = "1.6.0"
@@ -555,7 +557,7 @@ jiff = "0.2.15"
555557
key-manager = { path = "key-manager" }
556558
kstat-rs = "0.2.4"
557559
libc = "0.2.174"
558-
libipcc = { git = "https://github.com/oxidecomputer/ipcc-rs", rev = "524eb8f125003dff50b9703900c6b323f00f9e1b" }
560+
libipcc = { git = "https://github.com/oxidecomputer/ipcc-rs", rev = "7cdf2ab9c8d9e9267a8b366aa780c6c26f9a5ecf" }
559561
libfalcon = { git = "https://github.com/oxidecomputer/falcon", branch = "main" }
560562
libnvme = { git = "https://github.com/oxidecomputer/libnvme", rev = "dd5bb221d327a1bc9287961718c3c10d6bd37da0" }
561563
linear-map = "1.2.0"
@@ -825,6 +827,7 @@ wicket = { path = "wicket" }
825827
wicket-common = { path = "wicket-common" }
826828
wicketd-api = { path = "wicketd-api" }
827829
wicketd-client = { path = "clients/wicketd-client" }
830+
x509-cert = { version = "0.2.5", default-features = false }
828831
xshell = "0.2.7"
829832
zerocopy = "0.8.26"
830833
zeroize = { version = "1.8.1", features = ["zeroize_derive", "std"] }

clients/sled-agent-client/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ progenitor::generate_api!(
4545
"oxnet" = "0.1.0",
4646
},
4747
replace = {
48+
Attestation = sled_agent_types_versions::latest::rot::Attestation,
4849
Baseboard = sled_agent_types_versions::latest::inventory::Baseboard,
4950
BaseboardId = sled_hardware_types::BaseboardId,
5051
ByteCount = omicron_common::api::external::ByteCount,
52+
CertificateChain = sled_agent_types_versions::latest::rot::CertificateChain,
5153
CommitRequest = trust_quorum_types::messages::CommitRequest,
5254
CommitStatus = trust_quorum_types::status::CommitStatus,
5355
CoordinatorStatus = trust_quorum_types::status::CoordinatorStatus,
@@ -71,9 +73,12 @@ progenitor::generate_api!(
7173
InventoryZpool = sled_agent_types_versions::latest::inventory::InventoryZpool,
7274
LrtqUpgradeMsg = trust_quorum_types::messages::LrtqUpgradeMsg,
7375
MacAddr = omicron_common::api::external::MacAddr,
76+
Measurement = sled_agent_types_versions::latest::rot::Measurement,
77+
MeasurementLog = sled_agent_types_versions::latest::rot::MeasurementLog,
7478
MupdateOverrideBootInventory = sled_agent_types_versions::latest::inventory::MupdateOverrideBootInventory,
7579
Name = omicron_common::api::external::Name,
7680
NetworkInterface = omicron_common::api::internal::shared::NetworkInterface,
81+
Nonce = sled_agent_types_versions::latest::rot::Nonce,
7782
OmicronPhysicalDiskConfig = omicron_common::disk::OmicronPhysicalDiskConfig,
7883
OmicronPhysicalDisksConfig = omicron_common::disk::OmicronPhysicalDisksConfig,
7984
OmicronSledConfig = sled_agent_types_versions::latest::inventory::OmicronSledConfig,
@@ -92,6 +97,7 @@ progenitor::generate_api!(
9297
RouterId = omicron_common::api::internal::shared::RouterId,
9398
RouterTarget = omicron_common::api::internal::shared::RouterTarget,
9499
RouterVersion = omicron_common::api::internal::shared::RouterVersion,
100+
Sha3_256Digest = sled_agent_types_versions::latest::rot::Sha3_256Digest,
95101
SledRole = sled_agent_types_versions::latest::inventory::SledRole,
96102
SourceNatConfigGeneric = omicron_common::api::internal::shared::SourceNatConfigGeneric,
97103
SwitchLocation = omicron_common::api::external::SwitchLocation,

ipcc/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ license = "MPL-2.0"
88
workspace = true
99

1010
[dependencies]
11+
attest-data.workspace = true
1112
ciborium.workspace = true
13+
hubpack.workspace = true
1214
omicron-common.workspace = true
1315
omicron-uuid-kinds.workspace = true
1416
serde.workspace = true
1517
tufaceous-artifact.workspace = true
1618
thiserror.workspace = true
1719
omicron-workspace-hack.workspace = true
1820
libipcc.workspace = true
21+
x509-cert.workspace = true
1922

2023
[dev-dependencies]
2124
omicron-common = { workspace = true, features = ["testing"] }

ipcc/src/lib.rs

Lines changed: 132 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,31 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
// Copyright 2023 Oxide Computer Company
5+
// Copyright 2026 Oxide Computer Company
66

77
//! An interface to libipcc (inter-processor communications channel) which
8-
//! currently supports looking up values stored in the SP by key. These
9-
//! values are variously static, passed from the control plane to the SP
10-
//! (through MGS) or set from userland via libipcc.
8+
//! currently supports looking up values stored in the SP by key as well as RoT
9+
//! attestation operations.
10+
//! The looked up SP values are variously static, passed from the control plane
11+
//! to the SP (through MGS) or set from userland via libipcc.
1112
12-
use libipcc::{IpccError, IpccHandle};
13+
use attest_data::messages::{HostToRotCommand, MAX_REQUEST_SIZE, RotToHost};
14+
use attest_data::{Attestation, Log, Nonce};
15+
use libipcc::IPCC_MAX_DATA_SIZE;
1316
use omicron_uuid_kinds::MupdateUuid;
1417
use serde::Deserialize;
1518
use serde::Serialize;
1619
use thiserror::Error;
1720
use tufaceous_artifact::ArtifactHash;
1821

22+
pub use libipcc::IpccError;
23+
1924
#[cfg(test)]
2025
use proptest::arbitrary::any;
2126
#[cfg(test)]
2227
use proptest::strategy::Strategy;
28+
use x509_cert::PkiPath;
29+
use x509_cert::der::{self, Decode, Reader};
2330

2431
/// Supported keys.
2532
///
@@ -134,6 +141,18 @@ pub enum InstallinatorImageIdError {
134141
DeserializationFailed(String),
135142
}
136143

144+
#[derive(Debug, Error)]
145+
pub enum AttestError {
146+
#[error(transparent)]
147+
Ipcc(#[from] IpccError),
148+
#[error("failed to send ipcc message to RoT: {0}")]
149+
HostToRot(#[from] attest_data::messages::HostToRotError),
150+
#[error("deserializing {0:?} failed: {1}")]
151+
Deserialize(RotToHost, String),
152+
#[error(transparent)]
153+
Certificate(#[from] x509_cert::der::Error),
154+
}
155+
137156
/// These are the IPCC keys we can look up.
138157
/// NB: These keys match the definitions found in libipcc (RFD 316) and should
139158
/// match the values in `[ipcc::Key]` one-to-one.
@@ -151,13 +170,13 @@ enum IpccKey {
151170
/// Interface to the inter-processor communications channel.
152171
/// For more information see rfd 316.
153172
pub struct Ipcc {
154-
handle: IpccHandle,
173+
handle: libipcc::IpccHandle,
155174
}
156175

157176
impl Ipcc {
158177
/// Creates a new `Ipcc` instance.
159178
pub fn new() -> Result<Self, IpccError> {
160-
let handle = IpccHandle::new()?;
179+
let handle = libipcc::IpccHandle::new()?;
161180
Ok(Self { handle })
162181
}
163182

@@ -173,6 +192,112 @@ impl Ipcc {
173192
.map_err(InstallinatorImageIdError::DeserializationFailed)?;
174193
Ok(id)
175194
}
195+
196+
pub fn get_measurement_log(&self) -> Result<Log, AttestError> {
197+
let mut rot_message = vec![0; MAX_REQUEST_SIZE];
198+
let mut rot_resp = vec![0; IPCC_MAX_DATA_SIZE];
199+
// Serializing is infallible
200+
let rot_req = match attest_data::messages::serialize(
201+
&mut rot_message,
202+
&HostToRotCommand::GetMeasurementLog,
203+
|_| 0,
204+
) {
205+
Ok(len) => &rot_message[..len],
206+
Err(err) => unreachable!(
207+
"failed to serialize GetMeasurementLog command: {err}"
208+
),
209+
};
210+
let resp_len = self.handle.rot_request(rot_req, &mut rot_resp)?;
211+
let data = attest_data::messages::parse_response(
212+
&rot_resp[..resp_len],
213+
RotToHost::RotMeasurementLog,
214+
)?;
215+
let log = match hubpack::deserialize(data) {
216+
Ok((log, _)) => log,
217+
Err(err) => {
218+
return Err(AttestError::Deserialize(
219+
RotToHost::RotMeasurementLog,
220+
format!("{err}"),
221+
));
222+
}
223+
};
224+
Ok(log)
225+
}
226+
227+
pub fn get_certificates(&self) -> Result<PkiPath, AttestError> {
228+
let mut rot_message = vec![0; MAX_REQUEST_SIZE];
229+
let mut rot_resp = vec![0; IPCC_MAX_DATA_SIZE];
230+
// Serializing is infallible
231+
let rot_req = match attest_data::messages::serialize(
232+
&mut rot_message,
233+
&HostToRotCommand::GetCertificates,
234+
|_| 0,
235+
) {
236+
Ok(len) => &rot_message[..len],
237+
Err(err) => unreachable!(
238+
"failed to serialize GetCertificates command: {err}"
239+
),
240+
};
241+
let resp_len = self.handle.rot_request(rot_req, &mut rot_resp)?;
242+
let data = attest_data::messages::parse_response(
243+
&rot_resp[..resp_len],
244+
RotToHost::RotCertificates,
245+
)?;
246+
247+
// The returned payload is the DER encoded certificate chain itself
248+
// which we decode into a more usable `PkiPath`
249+
let mut certs = PkiPath::new();
250+
assert!(data.len() < u32::from(der::Length::MAX) as usize);
251+
let mut reader = der::SliceReader::new(data).unwrap();
252+
while !reader.is_finished() {
253+
let cert = reader
254+
.tlv_bytes()
255+
.and_then(|bytes| x509_cert::Certificate::from_der(bytes))
256+
.map_err(|err| {
257+
AttestError::Deserialize(
258+
RotToHost::RotCertificates,
259+
format!("[{}] {err}", certs.len()),
260+
)
261+
})?;
262+
certs.push(cert);
263+
}
264+
265+
Ok(certs)
266+
}
267+
268+
pub fn attest(&self, nonce: Nonce) -> Result<Attestation, AttestError> {
269+
let mut rot_message = vec![0; MAX_REQUEST_SIZE];
270+
let mut rot_resp = vec![0; IPCC_MAX_DATA_SIZE];
271+
// Serializing is infallible
272+
let rot_req = match attest_data::messages::serialize(
273+
&mut rot_message,
274+
&HostToRotCommand::Attest,
275+
|buf| {
276+
buf[..Nonce::LENGTH].copy_from_slice(nonce.as_ref());
277+
Nonce::LENGTH
278+
},
279+
) {
280+
Ok(len) => &rot_message[..len],
281+
Err(err) => {
282+
unreachable!("failed to serialize Attest command: {err}")
283+
}
284+
};
285+
let resp_len = self.handle.rot_request(rot_req, &mut rot_resp)?;
286+
let data = attest_data::messages::parse_response(
287+
&rot_resp[..resp_len],
288+
RotToHost::RotAttestation,
289+
)?;
290+
let attestation = match hubpack::deserialize(data) {
291+
Ok((attestation, _)) => attestation,
292+
Err(err) => {
293+
return Err(AttestError::Deserialize(
294+
RotToHost::RotAttestation,
295+
format!("{err}"),
296+
));
297+
}
298+
};
299+
Ok(attestation)
300+
}
176301
}
177302

178303
#[cfg(test)]

0 commit comments

Comments
 (0)