From de400b957819556cf6caae3761b5996fc41541ce Mon Sep 17 00:00:00 2001 From: Philip Tricca Date: Sun, 6 Sep 2026 09:54:26 -0700 Subject: [PATCH] appraise measurements from helios rot cert chain Measurement appraisal will only work for test cases until we decide what policy we want to enforce in the product. Our test cases currently evaluate a policy that requires the leaf and the last intermediates contain measurements that correspond to reference measurements for the "phase2" and "hbs" images. These identifiers are not yet standardised so we limit their use to test cases. We downgrade the sha2 crate from 0.11.0 -> 0.10.9 for compatibility with the `digest` crate used here. --- Cargo.lock | 71 ++++- Cargo.toml | 7 +- verifier/Cargo.toml | 7 +- verifier/build.rs | 22 +- verifier/src/helios_rot.rs | 515 ++++++++++++++++++++++++++++++++++++- verifier/test-corim.kdl | 21 ++ 6 files changed, 624 insertions(+), 19 deletions(-) create mode 100644 verifier/test-corim.kdl diff --git a/Cargo.lock b/Cargo.lock index ffebfa5e..38bcb7e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "CoreFoundation-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e9889e6db118d49d88d84728d0e964d973a5680befb5f85f55141beea5c20b" +dependencies = [ + "libc", + "mach", +] + +[[package]] +name = "IOKit-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99696c398cbaf669d2368076bdb3d627fb0ce51a26899d7c61228c5c0af3bf4a" +dependencies = [ + "CoreFoundation-sys", + "libc", + "mach", +] + [[package]] name = "addr2line" version = "0.25.1" @@ -1539,7 +1560,7 @@ dependencies = [ "pem-rfc7468 1.0.0", "rpassword", "serde_json", - "serialport", + "serialport 4.2.1-alpha.0", "sha3", "string-error", "tempfile", @@ -1584,8 +1605,10 @@ dependencies = [ "anyhow", "async-trait", "attest-data", + "attest-mock", "cfg-if", "const-oid 0.9.6", + "der", "dice-verifier", "ed25519-dalek", "helios-rot", @@ -1595,6 +1618,9 @@ dependencies = [ "pki-playground", "rats-corim", "rsa", + "serde", + "serde_with", + "sha2 0.10.9", "sha3", "thiserror 2.0.18", "tokio", @@ -3383,6 +3409,15 @@ dependencies = [ "serde", ] +[[package]] +name = "mach" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd13ee2dd61cc82833ba05ade5a30bb3d63f7ced605ef827063c63078302de9" +dependencies = [ + "libc", +] + [[package]] name = "mach2" version = "0.4.2" @@ -3562,6 +3597,17 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.26.4" @@ -4414,7 +4460,7 @@ dependencies = [ "scroll", "serde", "serde_yaml", - "serialport", + "serialport 4.7.1-alpha.0", "thiserror 2.0.18", "tracing", "uf2-decode", @@ -4819,7 +4865,7 @@ dependencies = [ [[package]] name = "rats-corim" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/rats-corim#bb4a08dd507514f98c54f5fc67eadf14a0705f4e" +source = "git+https://github.com/oxidecomputer/rats-corim?rev=1493d7f58e189125641d81f03f2dfb5941567c82#1493d7f58e189125641d81f03f2dfb5941567c82" dependencies = [ "ciborium", "ciborium-io", @@ -5591,6 +5637,22 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serialport" +version = "4.2.1-alpha.0" +source = "git+https://github.com/jgallagher/serialport-rs?branch=illumos-support#36c9f0e0292eac32215ab77c5847bf7afb13f000" +dependencies = [ + "CoreFoundation-sys", + "IOKit-sys", + "bitflags 1.3.2", + "cfg-if", + "libudev", + "mach2 0.4.2", + "nix 0.24.3", + "regex", + "winapi", +] + [[package]] name = "serialport" version = "4.7.1-alpha.0" @@ -5601,7 +5663,6 @@ dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", "io-kit-sys 0.4.1", - "libudev", "mach2 0.4.2", "nix 0.26.4", "scopeguard", @@ -7580,7 +7641,7 @@ dependencies = [ "ron 0.8.1", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.10.9", "yubihsm", ] diff --git a/Cargo.toml b/Cargo.toml index b7fe4da5..9822de63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ der = { version = "0.7.10", default-features = false } ecdsa = { version = "0.16", default-features = false } ed25519-dalek = { version = "2.1", default-features = false } env_logger = { version = "0.11.11", default-features = false } +flagset = "0.4.7" getrandom = "0.4.3" hex.version = "0.4" helios-rot = { git = "https://github.com/oxidecomputer/os-rot", rev = "4cdbacb6c3c7c88fdd086140dabf83e9763e0fba" } @@ -43,7 +44,7 @@ log = { version = "0.4.33", features = ["std"] } p384 = { version = "0.13.1", default-features = false } pem-rfc7468 = { version = "1.0.0", default-features = false } pki-playground = { git = "https://github.com/oxidecomputer/pki-playground", rev = "98865b0f4716c52adcb6d940fdb8c3ab90ad409a" } -rats-corim.git = "https://github.com/oxidecomputer/rats-corim" +rats-corim = { git = "https://github.com/oxidecomputer/rats-corim", rev = "1493d7f58e189125641d81f03f2dfb5941567c82" } ron = "0.8" rpassword = "7.5.4" rsa = "0.9.8" @@ -52,8 +53,8 @@ serde = { version = "1.0.228", default-features = false } serde-big-array = "0.5.1" serde_json = { version = "1.0.150", features = ["std", "alloc"] } serde_with = { version = "3.21.0", default-features = false } -serialport = { git = "https://github.com/oxidecomputer/serialport-rs", branch = "illumos-support-4.7.1" } -sha2 = "0.11.0" +serialport = { git = "https://github.com/jgallagher/serialport-rs", branch = "illumos-support" } +sha2 = "0.10.9" sha3 = { version = "0.10.8", default-features = false } sled-agent-client = { git = "https://github.com/oxidecomputer/omicron", rev = "f8e9052b768e1fa9c3fc16ef033fcab9ede3b806" } sled-agent-types-versions = { git = "https://github.com/oxidecomputer/omicron", rev = "f8e9052b768e1fa9c3fc16ef033fcab9ede3b806" } diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml index 67241e0b..9d240502 100644 --- a/verifier/Cargo.toml +++ b/verifier/Cargo.toml @@ -9,6 +9,7 @@ license = "MPL-2.0" attest-data = { path = "../attest-data", features = ["std"] } async-trait.workspace = true const-oid.workspace = true +der.workspace = true ed25519-dalek = { workspace = true, features = ["std"] } helios-rot.workspace = true hex = { workspace = true } @@ -16,12 +17,16 @@ hubpack.workspace = true p384 = { workspace = true, default-features = true } rats-corim.workspace = true rsa = { workspace = true, features = ["sha2"] } +serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true, features = ["macros"] } +sha2.workspace = true sha3.workspace = true thiserror.workspace = true x509-cert = { workspace = true, default-features = true } [build-dependencies] anyhow.workspace = true +attest-mock = { path = "../attest-mock", optional = true } cfg-if.workspace = true pki-playground = { workspace = true, optional = true } @@ -31,4 +36,4 @@ dice-verifier = { path = "../verifier", features = ["unittest"] } tokio = { workspace = true, features = ["macros", "rt"] } [features] -unittest = ["pki-playground"] +unittest = ["attest-mock", "pki-playground"] diff --git a/verifier/build.rs b/verifier/build.rs index 7dcfc721..3c5ddcef 100644 --- a/verifier/build.rs +++ b/verifier/build.rs @@ -6,13 +6,30 @@ use anyhow::Result; cfg_if::cfg_if! { if #[cfg(feature = "unittest")] { use anyhow::{anyhow, Context}; - use pki_playground::{config, OutputFileExistsBehavior}; use std::{env, path::PathBuf}; } } +#[cfg(feature = "unittest")] +fn mock_data() -> Result<()> { + use attest_mock::{MockCorim, MockData}; + + // output directory where we put generated test inputs + let corim = MockCorim::load("test-corim.kdl")?; + let corim = corim.to_bytes()?; + let mut out = + PathBuf::from(env::var("OUT_DIR").context("Failed to get OUT_DIR")?); + out.push("test-corim.cbor"); + + Ok(std::fs::write(&out, &corim).with_context(|| { + format!("write mock measurement log to file: {}", out.display()) + })?) +} + #[cfg(feature = "unittest")] fn pki_setup() -> Result<()> { + use pki_playground::{config, OutputFileExistsBehavior}; + // output directory where we put generated test inputs let out = PathBuf::from(env::var("OUT_DIR").context("Failed to get OUT_DIR")?); @@ -36,6 +53,9 @@ fn pki_setup() -> Result<()> { } fn main() -> Result<()> { + #[cfg(feature = "unittest")] + mock_data()?; + #[cfg(feature = "unittest")] pki_setup()?; diff --git a/verifier/src/helios_rot.rs b/verifier/src/helios_rot.rs index 2b527ec8..e6158df6 100644 --- a/verifier/src/helios_rot.rs +++ b/verifier/src/helios_rot.rs @@ -2,9 +2,29 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use der::{ + self, + asn1::{BitString, Int, OctetString}, + Sequence, +}; pub use helios_rot::{Attestation, Nonce, Nonce48}; +pub use rats_corim::{Corim, Digest}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use sha2::{ + digest::{ + const_oid::{AssociatedOid, ObjectIdentifier}, + typenum::Unsigned, + OutputSizeUser, + }, + Sha384, +}; +use std::{collections::HashMap, fmt}; use thiserror::Error; -use x509_cert::Certificate; +use x509_cert::{ + der::{Decode, DecodeValue, Header, SliceReader}, + Certificate, PkiPath, +}; #[derive(Debug, Error)] pub enum VerifyAttestationError { @@ -17,13 +37,6 @@ pub enum VerifyAttestationError { } // An attestation from the helios rot is: attestation = sign_alias(nonce) -// We must: -// - get the alias public key from the `Certificate` -// - reconstitute the message signed by the alias key (the nonce) -// When illumos supports runtime measurements the nonce will be combined with -// the serialized representation of the log using a hash function. -// NOTE: verify this w/ luqman / luqman's code ... or just test it -// - verify the attestation / signature over the message pub fn verify_attestation( alias: &Certificate, attestation: &Attestation, @@ -47,9 +60,460 @@ pub fn verify_attestation( Ok(()) } +pub const DICE_TCB_INFO: ObjectIdentifier = + ObjectIdentifier::new_unwrap("2.23.133.5.4.1"); + +// DICE Attestation Architecture §6.1.1: +// FWID ::== SEQUENCE { +#[derive(Debug, Sequence)] +pub struct Fwid { + // hashAlg OBJECT IDENTIFIER, + hash_algorithm: ObjectIdentifier, + + // digest OCTET STRING + digest: OctetString, +} + +// DICE Attestation Architecture §6.1.1: +// FWIDLIST ::== SEQUENCE SIZE (1..MAX) OF FWID +#[derive(Debug, Sequence)] +pub struct FwidList { + fwids: Vec, +} + +// NOTE: This structure represents an x509 extension defined by the TCG. This +// particular version is the one used by the AMD DPE that underlies the Helios +// RoT. It comes from an early draft of the TCG spec and is not compatible +// with published versions of the spec. +#[derive(Debug, Sequence)] +pub struct DiceTcbInfo { + #[asn1(context_specific = "0", tag_mode = "EXPLICIT", optional = "true")] + vendor: Option, + + #[asn1(context_specific = "1", tag_mode = "EXPLICIT", optional = "true")] + model: Option, + + #[asn1(context_specific = "4", tag_mode = "EXPLICIT", optional = "true")] + layer: Option, + + #[asn1(context_specific = "5", tag_mode = "EXPLICIT", optional = "true")] + index: Option, + + #[asn1(context_specific = "6", tag_mode = "IMPLICIT", optional = "true")] + fwids: Option, + + #[asn1(context_specific = "7", tag_mode = "EXPLICIT", optional = "true")] + flags: Option, + + #[asn1(context_specific = "9", tag_mode = "EXPLICIT", optional = "true")] + r#type: Option, +} + +#[derive(Debug, Error)] +pub enum ArrayError { + #[error("Slice is the wrong length")] + TryFromSliceError(std::array::TryFromSliceError), +} + +/// Array is the type we use as a base for types that are constant sized byte +/// buffers. +#[serde_as] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct Array(#[serde_as(as = "[_; N]")] pub [u8; N]); + +impl Array { + pub const LENGTH: usize = N; +} + +impl fmt::Display for Array { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +impl Default for Array { + /// Create and initialize an `Array` to 0's. + fn default() -> Self { + Self([0u8; N]) + } +} + +impl From<[u8; N]> for Array { + /// Create an Array from the provided array. + fn from(item: [u8; N]) -> Self { + Self(item) + } +} + +impl TryFrom<&[u8]> for Array { + type Error = ArrayError; + + /// Attempt to create an `Array` from the slice provided. + fn try_from(item: &[u8]) -> Result { + let array: [u8; N] = + item.try_into().map_err(Self::Error::TryFromSliceError)?; + Ok(Array::(array)) + } +} + +impl TryFrom> for Array { + type Error = ArrayError; + + /// Attempt to create an `Array` from the `Vec` provided. + fn try_from(item: Vec) -> Result { + item[..].try_into() + } +} + +impl AsRef<[u8]> for Array { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + +const SHA384_DIGEST_SIZE: usize = ::OutputSize::USIZE; +pub type Sha384Digest = Array; + +#[derive(Debug, Error)] +pub enum MeasurementError { + #[error("Deserialization failed")] + Deserialize, + #[error("Bad size for measurement: {size}")] + BadSize { size: usize, source: ArrayError }, + #[error("Fwid provided contains unsupported digest value")] + UnsupportedDigest, + #[error("CoRIM Digest contained tagged value")] + TaggedDigest, +} + +/// Measurement is an enum that can hold any of the hash algorithms that we support +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum Measurement { + Sha384(Sha384Digest), +} + +impl Default for Measurement { + fn default() -> Self { + Measurement::Sha384(Sha384Digest::default()) + } +} + +impl fmt::Display for Measurement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Sha384(digest) => { + write!(f, "sha-384;{digest}") + } + } + } +} + +impl TryFrom<&Fwid> for Measurement { + type Error = MeasurementError; + + /// Attempt to create an `Array` from the slice provided. + fn try_from(fwid: &Fwid) -> Result { + // map from fwid.hash_algorithm ObjectIdentifier to Measurement enum + if fwid.hash_algorithm == Sha384::OID { + // pull the associated data from fwid.digest OctetString + let digest = fwid.digest.as_bytes(); + let digest = Sha384Digest::try_from(digest).map_err(|e| { + Self::Error::BadSize { + size: digest.len(), + source: e, + } + })?; + + Ok(Measurement::Sha384(digest)) + } else { + Err(Self::Error::UnsupportedDigest) + } + } +} + +impl TryFrom for Measurement { + type Error = MeasurementError; + + /// Attempt to create a Measurement from the `rats_corim::Digest` provided. + fn try_from(digest: rats_corim::Digest) -> Result { + match digest.alg { + 7 => { + let bytes = match &digest.val { + rats_corim::TaggedBytes::Bytes(v) => v, + rats_corim::TaggedBytes::Tagged(_, _) => { + return Err(Self::Error::TaggedDigest) + } + }; + Ok(Measurement::Sha384(bytes[..].try_into().map_err(|e| { + MeasurementError::BadSize { + size: bytes.len(), + source: e, + } + })?)) + } + _ => Err(Self::Error::UnsupportedDigest), + } + } +} + +/// This is a collection to represent the measurements received from an +/// attestor. These measurements will come from the measurement log and the +/// DiceTcbInfo extension(s) in the attestation cert chain / pki path. +#[derive(Debug, PartialEq)] +pub struct MeasurementList(Vec); + +/// Possible errors produced by the `MeasurmentSet` construction process. +#[derive(Debug, Error)] +pub enum MeasurementListError { + #[error("failed to create reader from extension value")] + ExtensionDecode(#[source] der::Error), + #[error("failed to decode extension header")] + HeaderDecode(#[source] der::Error), + #[error("failed to decode TcbInfo extension")] + DiceTcbInfoDecode(#[source] der::Error), + #[error("failed to create Measurement from DiceTcbInfo extension")] + MeasurementConstruct(#[from] MeasurementError), +} + +impl MeasurementList { + /// Construct a MeasurementList from the provided artifacts. The + /// trustworthiness of these artifacts must be established independently + /// (see `verify_cert_chain` and `verify_attestation`). + pub fn from_artifacts( + pki_path: &PkiPath, + ) -> Result { + let mut measurements = Vec::new(); + + for cert in pki_path { + if let Some(extensions) = &cert.tbs_certificate.extensions { + for ext in extensions { + if ext.extn_id == DICE_TCB_INFO { + let mut reader = + SliceReader::new(ext.extn_value.as_bytes()) + .map_err( + MeasurementListError::ExtensionDecode, + )?; + let header = Header::decode(&mut reader) + .map_err(MeasurementListError::HeaderDecode)?; + + let tcb_info = + DiceTcbInfo::decode_value(&mut reader, header) + .map_err( + MeasurementListError::DiceTcbInfoDecode, + )?; + if let Some(fwid_vec) = &tcb_info.fwids { + for fwid in &fwid_vec.fwids { + let measurement = Measurement::try_from(fwid)?; + measurements.push(measurement); + } + } + } + } + } + } + + Ok(Self(measurements)) + } +} + +impl<'a> std::iter::IntoIterator for &'a MeasurementList { + type Item = &'a Measurement; + type IntoIter = std::slice::Iter<'a, Measurement>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl std::fmt::Display for MeasurementList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "measurement list")?; + for m in &self.0 { + writeln!(f, " {}", m)?; + } + if self.0.is_empty() { + writeln!(f, "(set is empty)")?; + } + Ok(()) + } +} + +// collection that maps a `Measurement` to the CoRIM `mkey` / string +// identifier assigned to this digest +pub struct ReferenceMeasurementMap(HashMap); + +#[derive(Debug, Error)] +pub enum ReferenceMeasurementMapError { + #[error("Digest is not the expected length")] + BadDigest(#[from] MeasurementError), + #[error("No such measurement found in ReferenceMeasurementMap: {0}")] + NotFound(Measurement), + #[error("CoRIM measurement map has no values")] + NoDigest, + // we currently assume that there is a 1:1 correspondence between each + // measurement key and value + #[error("CoRIM measurement map has multiple digests")] + MultipleDigests, + #[error("CoRIM measurement map mkey is not Text")] + KeyNotText, + #[error("CoRIM measurement map has no mkey")] + NoKey, +} + +impl TryFrom<&[Corim]> for ReferenceMeasurementMap { + type Error = ReferenceMeasurementMapError; + + fn try_from(corims: &[Corim]) -> Result { + use rats_corim::TypeChoice; + + let mut set = HashMap::new(); + + // iterate such that we get the label (whatever it's called in CoRIM + // speak) + for corim in corims { + let comid = corim.tags.wrapped.clone().into_iter(); + let reference_triple = + comid.flat_map(|x| x.triples.reference_triple.into_iter()); + let reference_triple = + reference_triple.flat_map(|x| x.wrapped.into_iter()); + let measurement_maps = + reference_triple.flat_map(|x| x.ref_claims.into_iter()); + + for measurement_map in measurement_maps { + let measurement_key = measurement_map.mkey; + let mkey = if let Some(t) = measurement_key { + match t { + TypeChoice::Text(s) => s, + _ => return Err(Self::Error::KeyNotText), + } + } else { + return Err(Self::Error::NoKey); + }; + + let measurement_values = measurement_map.mval; + if let Some(d) = measurement_values.digests { + let digests: Vec = d + .into_iter() + .flat_map(|x| x.wrapped.into_iter()) + .collect(); + if digests.is_empty() { + return Err(Self::Error::NoDigest); + } else if digests.len() > 1 { + return Err(Self::Error::MultipleDigests); + } + for digest in digests { + set.insert(digest.try_into()?, mkey.clone()); + } + } else { + return Err(Self::Error::NoDigest); + }; + } + } + + Ok(Self(set)) + } +} + +impl std::fmt::Display for ReferenceMeasurementMap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Reference measurements")?; + for (key, val) in &self.0 { + writeln!(f, " label: {val}, measurement: {key}")?; + } + if self.0.is_empty() { + writeln!(f, "(ReferenceMeasurementMap is empty)")?; + } + Ok(()) + } +} + +impl ReferenceMeasurementMap { + pub fn get_id(&self, m: &Measurement) -> Option { + //TODO: do this w/o all of the clones + self.0.get(m).cloned() + } +} + +/// Possible errors produced by the measurement verification / appraisal +/// process. +#[derive(Debug, Error)] +pub enum VerifyMeasurementsError { + #[error("Measurements are not a subset of reference measurements: {0}")] + NotSubset(MeasurementList), + #[error("Policy not satisfied")] + PolicyNotSatisfied, +} + +// This is the FnPolicy that most closely matches what we expect to enforce +// initially. This policy is effectively: +// the measurements from the leaf and last intermediate (the first and second +// measurement in the MeasurementList) must correspond to reference +// measurements with the mkeys "phase2" & "hbs" respectively +#[cfg(feature = "unittest")] +fn test_policy( + measurements: &MeasurementList, + corpus: &ReferenceMeasurementMap, +) -> bool { + let mut labeled_measurements = Vec::new(); + + // for each measurement in the MeasurementList + for (i, m) in measurements.into_iter().enumerate() { + // if it's in the corpus, we: + // - get the mkey string + if let Some(l) = corpus.get_id(m) { + // store the mkey & index of the measurement in the MeasurementList + labeled_measurements.push((l, i)); + } + } + + // The list of (mkey, index) tuples that we require the attested + // measurements to match. + let required_measurements = + vec![("phase2".to_string(), 0), ("hbs".to_string(), 1)]; + + if labeled_measurements == required_measurements { + true + } else { + false + } +} + +// our default `FnPolicy` that will always fail till we have mkeys tracked in +// OANA: https://github.com/oxidecomputer/oana/issues/44 +fn fail_policy( + _measurements: &MeasurementList, + _corpus: &ReferenceMeasurementMap, +) -> bool { + false +} + +// our measurement appraisal policy is a function with this signature +type FnPolicy = fn(&MeasurementList, &ReferenceMeasurementMap) -> bool; + +#[cfg(feature = "unittest")] +const POLICY: FnPolicy = test_policy; +#[cfg(not(feature = "unittest"))] +const POLICY: FnPolicy = fail_policy; + +/// This function implements the core of our attestation appraisal policy. +/// The trustworthiness of the parameters provided must be established +/// independently. +pub fn verify_measurements( + measurements: &MeasurementList, + corpus: &ReferenceMeasurementMap, +) -> Result<(), VerifyMeasurementsError> { + if POLICY(measurements, corpus) { + Ok(()) + } else { + Err(VerifyMeasurementsError::PolicyNotSatisfied) + } +} + #[cfg(test)] mod test { - use crate::helios_rot::{self, Nonce}; + use crate::helios_rot::{ + self, MeasurementList, Nonce, ReferenceMeasurementMap, + }; use ::helios_rot::{HeliosRot, HeliosRotMock}; use std::{ env, fs, @@ -142,4 +606,37 @@ mod test { Err(_) => assert!(false), } } + + #[test] + fn appraise_measurements() { + use rats_corim::Corim; + use std::{fs, slice}; + + // load alias cert chain + let out = PathBuf::from(env::var("OUT_DIR").unwrap()); + + let cert_chain = out.join("helios-rot.certlist.pem"); + let cert_chain = fs::read_to_string(&cert_chain) + .expect("read cert chain pem from file"); + let cert_chain = Certificate::load_pem_chain(cert_chain.as_ref()) + .expect("certificate chain from pem"); + + // create `MeasurementList` from cert chain + let measurements = MeasurementList::from_artifacts(&cert_chain) + .expect("measurement set from cert chain"); + println!("MeasurementList: {measurements}"); + + // load the corims + let corim = out.join("test-corim.cbor"); + let corim = Corim::from_file(&corim).expect("load corim from file"); + + // create `ReferenceMeasurementMap + let reference_measurements = + ReferenceMeasurementMap::try_from(slice::from_ref(&corim)) + .expect("ReferenceMeasurementMap from CoRIMs"); + println!("ReferenceMeasurementMap: {reference_measurements}"); + + helios_rot::verify_measurements(&measurements, &reference_measurements) + .expect("Verify measurement set against reference measurements"); + } } diff --git a/verifier/test-corim.kdl b/verifier/test-corim.kdl new file mode 100644 index 00000000..fcb2ca56 --- /dev/null +++ b/verifier/test-corim.kdl @@ -0,0 +1,21 @@ +// This KDL describes a rats_corim::Corim instance from +// https://github.com/oxidecomputer/dice-util. Use the `attest-mock` tool to +// produce a CBOR encoding of it. +// ```shell +// $ attest-mock this-file.kdl corim +// ``` +vendor "test" +tag-id "helios-tag-id" +id "helios-id" + +measurement { + mkey "hbs" + algorithm 7 + digest "2BC703A0D10FC45D31ED776A6D36A5EAC428329A4A69FD44FF99C5337F37A559668E5CDB0DEDB7C05C12D6E639161063" +} + +measurement { + mkey "phase2" + algorithm 7 + digest "ABF643ADBA6635FF09D795E7D9962EE732ED8FE956F7742E28CF38CECC0AFFB5C6D092A91D8E6C45AC5309DBCA1235F6" +}