diff --git a/Cargo.lock b/Cargo.lock index b6f9604c..2dfcbd45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -447,6 +447,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -980,12 +986,29 @@ name = "semver" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +dependencies = [ + "serde", +] [[package]] name = "serde" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" @@ -1016,8 +1039,11 @@ dependencies = [ "anyhow", "cryptography", "der", + "hex", + "serde", "sgx", "testaso", + "toml", ] [[package]] @@ -1075,7 +1101,11 @@ dependencies = [ "cryptography", "der", "flagset", + "hex", + "semver", + "serde", "testaso", + "toml", ] [[package]] @@ -1121,11 +1151,13 @@ dependencies = [ "memoffset", "mime", "rstest", + "serde", "sgx", "sgx_validation", "snp_validation", "testaso", "tokio", + "toml", "tower", "tower-http", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 44dddcd5..f1ed5e67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ anyhow = { version = "^1.0.66", default-features = false } base64 = { version = "^0.13.1", default-features = false } mime = { version = "^0.3.16", default-features = false } confargs = { version = "^0.1.3", default-features = false } +serde = { version = "1.0", features = ["derive"] } +toml = "0.5" [target.'cfg(not(target_os = "wasi"))'.dependencies] tokio = { version = "^1.21.2", features = ["rt-multi-thread", "macros"], default-features = false } diff --git a/crates/sgx_validation/Cargo.toml b/crates/sgx_validation/Cargo.toml index 9291343d..41bf0e00 100644 --- a/crates/sgx_validation/Cargo.toml +++ b/crates/sgx_validation/Cargo.toml @@ -9,7 +9,10 @@ description = "Intel SGX Attestation validation library for Steward" cryptography = { path = "../cryptography" } anyhow = { version = "^1.0.55", default-features = false } der = { version = "0.6", features = ["std"], default-features = false } +hex = "0.4" +serde = { version = "1.0", features = ["derive", "std"] } sgx = { version = "0.5.0", default-features = false } [dev-dependencies] testaso = { version = "0.1", default-features = false } +toml = "0.5" diff --git a/crates/sgx_validation/src/config.rs b/crates/sgx_validation/src/config.rs new file mode 100644 index 00000000..8392ff2a --- /dev/null +++ b/crates/sgx_validation/src/config.rs @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2022 Profian Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize}; +use sgx::parameters::Features; + +#[derive(Clone, Deserialize, Debug, Default, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + /// Values for `mrsigner` in the report body. + /// This is the list of public keys which have signed the Enarx binary. + #[serde(default)] + #[serde(deserialize_with = "from_hex")] + pub enarx_signer: Option>>, + + /// Values for `mrenclave` in the report body. + /// This is the hash of the Enclave environment after the Enarx binary is loaded + /// but before any workload is loaded, so this is a hash of the Enarx binary + /// in memory. + #[serde(default)] + #[serde(deserialize_with = "from_hex")] + pub enarx_hash: Option>>, + + /// Values for `features`. + #[serde(default)] + #[serde(deserialize_with = "from_features")] + pub features: Option, + + /// Minimum value allowed for `cpusvn`. + pub cpu_svn: Option>, + + /// Minimum value for `isv_svn`. + pub enclave_security_version: Option, + + /// Value for `isv_prodid`, do not allow versions below this. + pub enclave_product_id: Option, +} + +fn from_hex<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + let s: Vec<&str> = Deserialize::deserialize(deserializer)?; + + let mut outer_vec = Vec::new(); + for hash_string in s { + outer_vec.push(hex::decode(hash_string).map_err(|_| Error::custom("invalid hex"))?); + } + + Ok(Some(outer_vec)) +} + +fn from_features<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: &str = Deserialize::deserialize(deserializer)?; + + let mut flags = Features::empty(); + + flags |= Features::INIT; // Must be set + flags |= Features::MODE64BIT; // Isn't everything 64-bit? + + for flag in s.to_string().split('|') { + match flag.trim() { + "CET" => { + flags |= Features::CET; + } + "Debug" => { + flags |= Features::DEBUG; + } + "Eint_Key" => { + flags |= Features::EINIT_KEY; + } + "KSS" => { + flags |= Features::KSS; + } + "Provisioning_Key" => { + flags |= Features::PROVISIONING_KEY; + } + _ => return Err(D::Error::custom(format!("unknown flag '{}'", flag))), + } + } + + Ok(Some(flags.bits())) +} + +impl Config { + pub fn features(&self) -> Features { + match self.features { + Some(f) => Features::from_bits_truncate(f), + None => Features::empty(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::config::Config; + + #[test] + fn test_empty_config() { + let config_raw = r#" + "#; + + let config_obj: Config = toml::from_str(config_raw).expect("Couldn't deserialize"); + assert!(config_obj.enarx_signer.is_none()); + assert!(config_obj.enclave_security_version.is_none()); + assert!(config_obj.cpu_svn.is_none()); + } + + #[test] + fn test_list_of_hashes() { + let config_raw = r#" + enarx_signer = ["1234567890", "00112233445566778899"] + "#; + + let config_obj: Config = toml::from_str(config_raw).expect("Couldn't deserialize"); + assert!(config_obj.enarx_signer.is_some()); + assert_eq!(config_obj.enarx_signer.clone().unwrap().len(), 2); + assert_eq!( + config_obj.enarx_signer.clone().unwrap().first().unwrap(), + &hex::decode("1234567890").unwrap() + ); + assert_eq!( + config_obj.enarx_signer.unwrap().get(1).unwrap(), + &hex::decode("00112233445566778899").unwrap() + ); + assert!(config_obj.cpu_svn.is_none()); + } +} diff --git a/crates/sgx_validation/src/icelake.signed.csr b/crates/sgx_validation/src/icelake.signed.csr new file mode 100644 index 00000000..e6bb2502 Binary files /dev/null and b/crates/sgx_validation/src/icelake.signed.csr differ diff --git a/crates/sgx_validation/src/lib.rs b/crates/sgx_validation/src/lib.rs index 20e5952f..49362f92 100644 --- a/crates/sgx_validation/src/lib.rs +++ b/crates/sgx_validation/src/lib.rs @@ -1,19 +1,21 @@ // SPDX-FileCopyrightText: 2022 Profian Inc. // SPDX-License-Identifier: AGPL-3.0-only -mod quote; +pub mod config; +pub mod quote; use cryptography::ext::*; use quote::traits::ParseBytes; use std::fmt::Debug; +use crate::config::Config; use anyhow::{anyhow, Result}; use cryptography::const_oid::ObjectIdentifier; use cryptography::sha2::{Digest, Sha256}; use cryptography::x509::{ext::Extension, request::CertReqInfo, Certificate, TbsCertificate}; use der::{Decode, Encode}; -use sgx::parameters::{Attributes, MiscSelect}; +use sgx::parameters::MiscSelect; #[derive(Clone, Debug)] pub struct Sgx([Certificate<'static>; 1]); @@ -29,7 +31,7 @@ impl Sgx { pub const OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.58270.1.2"); pub const ATT: bool = true; - fn trusted<'c>(&'c self, chain: &'c [Certificate<'c>]) -> Result<&'c TbsCertificate<'c>> { + pub fn trusted<'c>(&'c self, chain: &'c [Certificate<'c>]) -> Result<&'c TbsCertificate<'c>> { let mut signer = &self.0[0].tbs_certificate; for cert in self.0.iter().chain(chain.iter()) { signer = signer.verify_crt(cert)?; @@ -38,7 +40,13 @@ impl Sgx { Ok(signer) } - pub fn verify(&self, cri: &CertReqInfo<'_>, ext: &Extension<'_>, dbg: bool) -> Result { + pub fn verify( + &self, + cri: &CertReqInfo<'_>, + ext: &Extension<'_>, + config: Option<&Config>, + dbg: bool, + ) -> Result { if ext.critical { return Err(anyhow!("sgx extension cannot be critical")); } @@ -80,38 +88,59 @@ impl Sgx { if !dbg { // TODO: Validate that the certification request came from an SGX enclave. - let hash = Sha256::digest(&cri.public_key.to_vec()?); + let hash = Sha256::digest(cri.public_key.to_vec()?); if hash.as_slice() != &rpt.reportdata[..hash.as_slice().len()] { return Err(anyhow!("sgx report data is invalid")); } - if rpt.mrenclave != [0u8; 32] { - return Err(anyhow!("untrusted enarx runtime")); - } - - if rpt.mrsigner != [0u8; 32] { - return Err(anyhow!("untrusted enarx signer")); - } - - if rpt.cpusvn != [0u8; 16] { - return Err(anyhow!("untrusted cpu")); - } - - if rpt.attributes() != Attributes::default() { - return Err(anyhow!("untrusted attributes")); + if config.is_some() { + let config_ref = config.as_ref().unwrap(); + if config_ref.enarx_signer.is_some() { + let mut signed = false; + for signer in config_ref.enarx_signer.as_ref().unwrap() { + if rpt.mrsigner == signer.as_slice() { + signed = true; + break; + } + } + if !signed { + return Err(anyhow!("untrusted enarx signer")); + } + } + + if config_ref.cpu_svn.is_some() { + let conf_version = config_ref.cpu_svn.as_ref().unwrap(); + for index in 0..rpt.cpusvn.len() { + if rpt.cpusvn.get(index).unwrap() < conf_version.get(index).unwrap() { + return Err(anyhow!("untrusted cpu")); + } + } + } + + if config_ref.enclave_product_id.is_some() + && rpt.enclave_product_id() != config_ref.enclave_product_id.unwrap() + { + return Err(anyhow!("untrusted enclave product id")); + } + + if config_ref.enclave_security_version.is_some() + && rpt.enclave_security_version() < config_ref.enclave_security_version.unwrap() + { + return Err(anyhow!("untrusted enclave")); + } + + if rpt + .attributes() + .features() + .intersects(config_ref.features()) + { + return Err(anyhow!("untrusted features")); + } } if rpt.misc_select() != MiscSelect::default() { return Err(anyhow!("untrusted misc select")); } - - if rpt.enclave_product_id() != u16::MAX { - return Err(anyhow!("untrusted enclave product id")); - } - - if rpt.enclave_security_version() < u16::MAX { - return Err(anyhow!("untrusted enclave")); - } } Ok(false) diff --git a/crates/sgx_validation/src/main.rs b/crates/sgx_validation/src/main.rs new file mode 100644 index 00000000..88f0e454 --- /dev/null +++ b/crates/sgx_validation/src/main.rs @@ -0,0 +1,46 @@ +use cryptography::x509::attr::Attribute; +use cryptography::x509::request::{CertReq, ExtensionReq}; +use cryptography::x509::Certificate; +use der::Decode; +use sgx_validation::quote::traits::ParseBytes; +use sgx_validation::quote::Quote; +use sgx_validation::Sgx; +use std::env; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +fn main() { + let args: Vec = env::args().collect(); + let fname = Path::new(args.get(1).expect("CSR file not specified")); + let mut file = File::open(fname).expect("no such file"); + let mut contents = Vec::new(); + file.read_to_end(&mut contents) + .expect("failed to read file"); + + let cr = CertReq::from_der(&contents).expect("failed to decode DER"); + let cri = cr.info; + #[allow(unused_variables)] + for Attribute { oid, values } in cri.attributes.iter() { + for any in values.iter() { + let ereq: ExtensionReq<'_> = any.decode_into().unwrap(); + for ext in Vec::from(ereq) { + let (quote, bytes): (Quote<'_>, _) = + ext.extn_value.parse().expect("failed to parse"); + let chain = quote.chain().unwrap(); + let chain = chain + .iter() + .map(|c| Certificate::from_der(c)) + .collect::, _>>() + .unwrap(); + + // Validate the report. + let sgx = Sgx::default(); + let pck = sgx.trusted(&chain).unwrap(); + let report = quote.verify(pck).unwrap(); + println!("{:?}", report); + sgx.verify(&cri, &ext, None, false).unwrap(); + } + } + } +} diff --git a/crates/snp_validation/Cargo.toml b/crates/snp_validation/Cargo.toml index a33087b4..aaabf793 100644 --- a/crates/snp_validation/Cargo.toml +++ b/crates/snp_validation/Cargo.toml @@ -9,7 +9,11 @@ description = "AMD SEV-SNP Attestation validation library for Steward" cryptography = { path = "../cryptography" } anyhow = { version = "^1.0.55", default-features = false } der = { version = "0.6", features = ["std"], default-features = false } -flagset = { version = "0.4.3", default-features = false} +flagset = { version = "0.4.3", default-features = false } +hex = "0.4" +serde = { version = "1.0", features = ["derive"] } +semver = { version = "1.0", features = ["serde"] } [dev-dependencies] testaso = { version = "0.1", default-features = false } +toml = "0.5" diff --git a/crates/snp_validation/src/config.rs b/crates/snp_validation/src/config.rs new file mode 100644 index 00000000..de2ba35b --- /dev/null +++ b/crates/snp_validation/src/config.rs @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: 2022 Profian Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use crate::{PlatformInfoFlags, PolicyFlags}; +use flagset::FlagSet; +use semver::Version; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize}; +use std::str::FromStr; + +#[derive(Clone, Deserialize, Debug, Default, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + /// Values for `author_key_digest` in the report body. + /// This is the list of public keys which have signed the signing key of the Enarx binary. + #[serde(default)] + #[serde(deserialize_with = "from_hex")] + pub enarx_signer: Option>>, + + /// Values for `id_key_digest` in the report body. + /// This is the list of public keys which have signed the Enarx binary. + #[serde(default)] + #[serde(deserialize_with = "from_hex")] + pub id_key_digest: Option>>, + + /// Minimum value for `policy.abi_major`.`policy.abi_minor` + #[serde(default)] + #[serde(deserialize_with = "from_version_string")] + pub minimum_abi: Option, + + #[serde(default)] + #[serde(deserialize_with = "from_policy_string")] + pub policy_flags: Option, + + #[serde(default)] + #[serde(deserialize_with = "from_platform_string")] + pub platform_info_flags: Option, +} + +/// This is a work-around for the `semver` crate which requires a patch value. This isn't +/// applicable here, since the AMD attestation report Policy does not specify this. +fn from_version_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: &str = Deserialize::deserialize(deserializer)?; + + let version = match s.split('.').count() { + 3 => Version::from_str(s).map_err(|_| Error::custom("invalid version string"))?, + 2 => { + let new_version = format!("{}.0", s); + Version::from_str(&new_version).map_err(|_| Error::custom("invalid version string"))? + } + _ => { + return Err(Error::custom("invalid version string")); + } + }; + + Ok(Some(version)) +} + +fn from_hex<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + let s: Vec<&str> = Deserialize::deserialize(deserializer)?; + + let mut outer_vec = Vec::new(); + for hash_string in s { + outer_vec.push(hex::decode(hash_string).map_err(|_| Error::custom("invalid hex"))?); + } + + Ok(Some(outer_vec)) +} + +fn from_policy_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: &str = Deserialize::deserialize(deserializer)?; + + let mut flags = FlagSet::::from(PolicyFlags::Reserved); + + for flag in s.to_string().split('|') { + match flag.trim() { + "Debug" => { + flags |= PolicyFlags::Debug; + } + "MigrateMA" => { + return Err(D::Error::custom("SNP migration not supported")); + } + "SingleSocket" => { + flags |= PolicyFlags::SingleSocket; + } + "SMT" => { + flags |= PolicyFlags::SMT; + } + _ => return Err(D::Error::custom(format!("unknown policy flag '{}'", flag))), + } + } + + Ok(Some(flags.bits())) +} + +fn from_platform_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: &str = Deserialize::deserialize(deserializer)?; + + let flag = match s.trim() { + "SME" => PlatformInfoFlags::SME, + "TSME" => PlatformInfoFlags::TSME, + _ => { + return Err(D::Error::custom(format!( + "unknown platform info flag '{}'", + s + ))) + } + }; + + Ok(Some(flag)) +} + +impl From for Version { + fn from(value: Config) -> Self { + match value.minimum_abi { + Some(v) => v, + None => Version::new(0, 0, 0), + } + } +} + +#[cfg(test)] +mod tests { + use crate::config::Config; + use crate::PolicyFlags; + use flagset::FlagSet; + + #[test] + fn test_empty_config() { + let config_raw = r#" + "#; + + let config_obj: Config = toml::from_str(config_raw).expect("Couldn't deserialize"); + assert!(config_obj.policy_flags.is_none()); + assert!(config_obj.platform_info_flags.is_none()); + } + + #[test] + fn test_flags() { + let config_raw = r#" + policy_flags = "SingleSocket | Debug" + platform_info_flags = "SME" + "#; + + let config_obj: Config = toml::from_str(config_raw).expect("Couldn't deserialize"); + assert!(config_obj.policy_flags.is_some()); + assert!(config_obj.platform_info_flags.is_some()); + + let flags = FlagSet::::new(config_obj.policy_flags.unwrap()).unwrap(); + assert_eq!( + flags, + PolicyFlags::SingleSocket | PolicyFlags::Debug | PolicyFlags::Reserved + ); + } + + #[test] + fn test_semver() { + let config_raw = r#" + minimum_abi = "1.0" + "#; + + let config_obj: Config = toml::from_str(config_raw).expect("Couldn't deserialize"); + assert!(config_obj.minimum_abi.is_some()); + // The `semver` crate requires a patch version, which isn't applicable here. + assert_eq!(config_obj.minimum_abi.unwrap().to_string(), "1.0.0"); + } +} diff --git a/crates/snp_validation/src/lib.rs b/crates/snp_validation/src/lib.rs index e7c046bb..e67db996 100644 --- a/crates/snp_validation/src/lib.rs +++ b/crates/snp_validation/src/lib.rs @@ -1,10 +1,13 @@ // SPDX-FileCopyrightText: 2022 Profian Inc. // SPDX-License-Identifier: AGPL-3.0-only +pub mod config; + use cryptography::ext::*; use std::{fmt::Debug, mem::size_of}; +use crate::config::Config; use anyhow::{anyhow, Context, Result}; use cryptography::const_oid::db::rfc5912::ECDSA_WITH_SHA_384; use cryptography::const_oid::ObjectIdentifier; @@ -16,6 +19,8 @@ use cryptography::x509::{PkiPath, TbsCertificate}; use der::asn1::UIntRef; use der::{Decode, Encode, Sequence}; use flagset::{flags, FlagSet}; +use semver::{Version, VersionReq}; +use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Sequence)] pub struct Evidence<'a> { @@ -26,6 +31,7 @@ pub struct Evidence<'a> { } flags! { + #[derive(Deserialize, Serialize)] pub enum PolicyFlags: u8 { /// Indicates if only one socket is permitted SingleSocket = 1 << 4, @@ -39,9 +45,13 @@ flags! { SMT = 1 << 0, } + /// These items are mutually exclusive + #[derive(Deserialize, Serialize)] pub enum PlatformInfoFlags: u8 { + /// Secure Memory Encryption + SME = 1 << 0, + /// Transparent Secure Memory Encryption TSME = 1 << 1, - SMT = 1 << 0, } } @@ -58,18 +68,24 @@ pub struct Policy { rsvd: [u8; 5], } +impl From for Version { + fn from(value: Policy) -> Self { + Version::new(value.abi_major as u64, value.abi_minor as u64, 0) + } +} + #[repr(C, packed)] #[derive(Copy, Clone, Debug)] pub struct PlatformInfo { /// Bit fields indicating enabled features - pub flags: FlagSet, + pub flag: PlatformInfoFlags, /// Reserved rsvd: [u8; 7], } #[repr(C, packed)] #[derive(Copy, Clone, Debug)] -struct Body { +pub struct Body { /// The version of the attestation report, currently 2 pub version: u32, /// Guest Security Version Number (SVN) @@ -178,7 +194,7 @@ impl Es384 { #[repr(C, packed)] #[derive(Copy, Clone)] -union Signature { +pub union Signature { bytes: [u8; 512], es384: Es384, } @@ -186,7 +202,7 @@ union Signature { /// The attestation report from the trusted environment on an AMD system #[repr(C, packed)] #[derive(Copy, Clone)] -struct Report { +pub struct Report { pub body: Body, pub signature: Signature, } @@ -236,7 +252,13 @@ impl Snp { Err(anyhow!("snp vcek is untrusted")) } - pub fn verify(&self, cri: &CertReqInfo<'_>, ext: &Extension<'_>, dbg: bool) -> Result { + pub fn verify( + &self, + cri: &CertReqInfo<'_>, + ext: &Extension<'_>, + config: Option<&Config>, + dbg: bool, + ) -> Result { if ext.critical { return Err(anyhow!("snp extension cannot be critical")); } @@ -300,6 +322,12 @@ impl Snp { return Err(anyhow!("snp guest policy migration flag was set")); } + if report.body.policy.abi_major > report.body.current_major { + return Err(anyhow!("snp policy has higher abi major than firmware")); + } else if report.body.policy.abi_minor > report.body.current_minor { + return Err(anyhow!("snp policy has higher abi minor than firmware")); + } + // Check reserved fields if report.body.rsvd1 != 0 || report.body.rsvd3 != 0 || report.body.rsvd4 != 0 { return Err(anyhow!("snp report reserved fields were set")); @@ -330,34 +358,16 @@ impl Snp { } // Check fields not set by Enarx - for value in report.body.author_key_digest { - if value != 0 { - return Err(anyhow!( - "snp report author_key_digest field not set by Enarx" - )); - } - } - for value in report.body.host_data { if value != 0 { return Err(anyhow!("snp report host_data field not set by Enarx")); } } - for value in report.body.id_key_digest { - if value != 0 { - return Err(anyhow!("snp report id_key_digest field not set by Enarx")); - } - } - if report.body.vmpl != 0 { return Err(anyhow!("snp report vmpl field not set by Enarx")); } - if report.body.guest_svn != 0 { - return Err(anyhow!("snp report guest_svn field not set by Enarx")); - } - // Check field set by Enarx for value in report.body.report_id_ma { if value != 255 { @@ -367,9 +377,55 @@ impl Snp { } } + if config.is_some() { + let config_ref = config.as_ref().unwrap(); + + if config_ref.minimum_abi.is_some() { + let req_ver = config_ref.minimum_abi.as_ref().unwrap(); + let req = VersionReq::parse(&format!(">={}", &req_ver)).unwrap(); + if !req.matches(&report.body.policy.into()) { + return Err(anyhow!("snp minimum abi not met")); + } + } + + if config_ref.enarx_signer.is_some() { + let signers = config_ref.enarx_signer.as_ref().unwrap(); + let mut signed = false; + for signer in signers { + if signer == &report.body.author_key_digest { + signed = true; + break; + } + } + if !signed { + return Err(anyhow!("snp untrusted enarx author_key")); + } + } + + if config_ref.id_key_digest.is_some() { + let signers = config_ref.id_key_digest.as_ref().unwrap(); + let mut signed = false; + for signer in signers { + if signer == &report.body.id_key_digest { + signed = true; + break; + } + } + if !signed { + return Err(anyhow!("snp untrusted enarx id_key")); + } + } + + if config_ref.platform_info_flags.is_some() + && config_ref.platform_info_flags.unwrap() != report.body.plat_info.flag + { + return Err(anyhow!("snp unexpected memory mode")); + } + } + if !dbg { // Validate that the certification request came from an SNP VM. - let hash = Sha384::digest(&cri.public_key.to_vec()?); + let hash = Sha384::digest(cri.public_key.to_vec()?); if hash.as_slice() != &report.body.report_data[..hash.as_slice().len()] { return Err(anyhow!("snp report.report_data is invalid")); } diff --git a/crates/snp_validation/src/main.rs b/crates/snp_validation/src/main.rs new file mode 100644 index 00000000..19dbdf4f --- /dev/null +++ b/crates/snp_validation/src/main.rs @@ -0,0 +1,34 @@ +use cryptography::x509::attr::Attribute; +use cryptography::x509::request::{CertReq, ExtensionReq}; +use der::Decode; +use snp_validation::{Evidence, Report, Snp}; +use std::env; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +fn main() { + let args: Vec = env::args().collect(); + let fname = Path::new(args.get(1).expect("CSR file not specified")); + let mut file = File::open(fname).expect("no such file"); + let mut contents = Vec::new(); + file.read_to_end(&mut contents) + .expect("failed to read file"); + + let cr = CertReq::from_der(&contents).expect("failed to decode DER"); + let cri = cr.info; + #[allow(unused_variables)] + for Attribute { oid, values } in cri.attributes.iter() { + for any in values.iter() { + let ereq: ExtensionReq<'_> = any.decode_into().unwrap(); + for ext in Vec::from(ereq) { + let evidence = Evidence::from_der(ext.extn_value).unwrap(); + let array = evidence.report.try_into().unwrap(); + let report = Report::cast(array); + println!("{:?}", report); + let snp = Snp::default(); + snp.verify(&cri, &ext, None, false).unwrap(); + } + } + } +} diff --git a/crates/snp_validation/src/milan.signed.csr b/crates/snp_validation/src/milan.signed.csr new file mode 100644 index 00000000..f3c30c26 Binary files /dev/null and b/crates/snp_validation/src/milan.signed.csr differ diff --git a/src/main.rs b/src/main.rs index f502aa39..c853864a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,7 @@ use cryptography::x509::{Certificate, TbsCertificate}; use der::asn1::{GeneralizedTime, Ia5StringRef, UIntRef}; use der::{Decode, Encode, Sequence}; use hyper::StatusCode; +use serde::{Deserialize, Serialize}; use tower_http::trace::{ DefaultOnBodyChunk, DefaultOnEos, DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer, @@ -84,6 +85,18 @@ struct Args { #[arg(long, env = "STEWARD_SAN")] san: Option, + + #[arg(long)] + config: Option, +} + +#[derive(Clone, Deserialize, Debug, Default, Serialize)] +struct ConfigurationFile { + #[serde(rename = "sgx")] + sgx_config: Option, + + #[serde(rename = "snp")] + snp_config: Option, } #[derive(Debug)] @@ -92,6 +105,7 @@ struct State { key: Zeroizing>, crt: Vec, san: Option, + config: ConfigurationFile, } /// ASN.1 @@ -113,6 +127,7 @@ impl State { san: Option, key: impl AsRef, crt: impl AsRef, + config: Option, ) -> anyhow::Result { // Load the key file. let key = std::io::BufReader::new(std::fs::File::open(key)?); @@ -120,13 +135,14 @@ impl State { // Load the crt file. let crt = std::io::BufReader::new(std::fs::File::open(crt)?); - Self::read(san, key, crt) + Self::read(san, key, crt, config) } pub fn read( san: Option, mut key: impl BufRead, mut crt: impl BufRead, + config: Option, ) -> anyhow::Result { let key = match rustls_pemfile::read_one(&mut key)? { Some(rustls_pemfile::Item::PKCS8Key(buf)) => Zeroizing::new(buf), @@ -142,7 +158,17 @@ impl State { PrivateKeyInfo::from_der(key.as_ref())?; Certificate::from_der(crt.as_ref())?; - Ok(State { crt, san, key }) + let config_obj = match config { + Some(c) => toml::from_str(&std::fs::read_to_string(Path::new(&c))?)?, + None => ConfigurationFile::default(), + }; + + Ok(State { + crt, + san, + key, + config: config_obj, + }) } pub fn generate(san: Option, hostname: &str) -> anyhow::Result { @@ -199,7 +225,12 @@ impl State { // Self-sign the certificate. let crt = tbs.sign(&pki)?; - Ok(Self { key, crt, san }) + Ok(Self { + key, + crt, + san, + config: ConfigurationFile::default(), + }) } } @@ -220,7 +251,7 @@ async fn main() -> anyhow::Result<()> { .map(Args::parse_from)?; let state = match (args.key, args.crt, args.host) { (None, None, Some(host)) => State::generate(args.san, &host)?, - (Some(key), Some(crt), _) => State::load(args.san, key, crt)?, + (Some(key), Some(crt), _) => State::load(args.san, key, crt, args.config)?, _ => { eprintln!("Either:\n* Specify the public key `--crt` and private key `--key`, or\n* Specify the host `--host`.\n\nRun with `--help` for more information."); return Err(anyhow!("invalid configuration")); @@ -309,6 +340,7 @@ fn attest_request( sans: SubjectAltName<'_>, cr: CertReq<'_>, validity: &Validity, + state: &State, ) -> Result, StatusCode> { let info = cr.verify().map_err(|e| { debug!("failed to verify certificate info: {e}"); @@ -336,8 +368,14 @@ fn attest_request( // Validate the extension. let (copy, att) = match ext.extn_id { Kvm::OID => (Kvm::default().verify(&info, &ext, dbg), Kvm::ATT), - Sgx::OID => (Sgx::default().verify(&info, &ext, dbg), Sgx::ATT), - Snp::OID => (Snp::default().verify(&info, &ext, dbg), Snp::ATT), + Sgx::OID => ( + Sgx::default().verify(&info, &ext, state.config.sgx_config.as_ref(), dbg), + Sgx::ATT, + ), + Snp::OID => ( + Snp::default().verify(&info, &ext, state.config.snp_config.as_ref(), dbg), + Snp::ATT, + ), oid => { debug!("extension `{oid}` is unsupported"); return Err(StatusCode::BAD_REQUEST); @@ -445,7 +483,14 @@ async fn attest( let name = Ia5StringRef::new(name).or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; sans.push(GeneralName::DnsName(name)); } - attest_request(&issuer, &isskey, SubjectAltName(sans), cr, &validity) + attest_request( + &issuer, + &isskey, + SubjectAltName(sans), + cr, + &validity, + &state, + ) }) .collect::, _>>() .and_then(|issued| { @@ -492,14 +537,14 @@ mod tests { fn certificates_state() -> State { #[cfg(not(target_os = "wasi"))] - return State::load(None, "testdata/ca.key", "testdata/ca.crt") + return State::load(None, "testdata/ca.key", "testdata/ca.crt", None) .expect("failed to load state"); #[cfg(target_os = "wasi")] { let crt = std::io::BufReader::new(include_bytes!("../testdata/ca.crt").as_slice()); let key = std::io::BufReader::new(include_bytes!("../testdata/ca.key").as_slice()); - State::read(None, key, crt).expect("failed to load state") + State::read(None, key, crt, None).expect("failed to load state") } } @@ -869,4 +914,208 @@ mod tests { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } } + + mod config { + use crate::ConfigurationFile; + use cryptography::x509::attr::Attribute; + use cryptography::x509::request::{CertReq, ExtensionReq}; + use cryptography::x509::Certificate; + use der::Decode; + use sgx_validation::quote::traits::ParseBytes; + use sgx_validation::quote::Quote; + use snp_validation::Report; + use snp_validation::{Evidence, Snp}; + + const STEWARD_CONFIG: &str = include_str!("../testdata/steward.toml"); + const ICELAKE_CSR: &[u8] = + include_bytes!("../crates/sgx_validation/src/icelake.signed.csr"); + const MILAN_CSR: &[u8] = include_bytes!("../crates/snp_validation/src/milan.signed.csr"); + + fn validate_sgx_config( + csr: &CertReq<'_>, + conf: &sgx_validation::config::Config, + ) -> anyhow::Result<()> { + let sgx = sgx_validation::Sgx::default(); + + #[allow(unused_variables)] + for Attribute { oid, values } in csr.info.attributes.iter() { + for any in values.iter() { + let ereq: ExtensionReq<'_> = any.decode_into()?; + for ext in Vec::from(ereq) { + let (quote, bytes): (Quote<'_>, _) = ext.extn_value.parse()?; + let chain = quote.chain()?; + let chain = chain + .iter() + .map(|c| Certificate::from_der(c)) + .collect::, _>>()?; + + // Validate the report. + let pck = sgx.trusted(&chain)?; + let report = quote.verify(pck)?; + sgx.verify(&csr.info, &ext, Some(&conf), false)?; + } + } + } + Ok(()) + } + + fn validate_snp_config( + csr: &CertReq<'_>, + conf: &snp_validation::config::Config, + ) -> anyhow::Result<()> { + let snp = Snp::default(); + #[allow(unused_variables)] + for Attribute { oid, values } in csr.info.attributes.iter() { + for any in values.iter() { + let ereq: ExtensionReq<'_> = any.decode_into()?; + for ext in Vec::from(ereq) { + let evidence = Evidence::from_der(ext.extn_value)?; + let array = evidence.report.try_into()?; + let report = Report::cast(array); + snp.verify(&csr.info, &ext, Some(conf), false)?; + } + } + } + Ok(()) + } + + #[test] + fn test_config_empty() { + let config_raw = r#" + "#; + let config_obj: ConfigurationFile = + toml::from_str(config_raw).expect("Couldn't deserialize"); + + assert!(config_obj.sgx_config.is_none()); + assert!(config_obj.snp_config.is_none()); + } + + #[test] + fn test_config() { + let config_raw = r#" + [snp] + policy_flags = "SingleSocket | Debug" + enarx_signer = ["1234567890", "00112233445566778899"] + minimum_abi = "1.0" + + [sgx] + enarx_signer = ["1234567890", "00112233445566778899"] + "#; + let config_obj: ConfigurationFile = + toml::from_str(config_raw).expect("Couldn't deserialize"); + + assert!(config_obj.sgx_config.is_some()); + assert!(config_obj.snp_config.is_some()); + assert_eq!( + config_obj.sgx_config.unwrap().enarx_signer.unwrap().len(), + 2 + ); + } + + #[test] + fn test_sgx_signed_canned_csr() { + let csr_object = CertReq::from_der(ICELAKE_CSR).unwrap(); + let config_obj: ConfigurationFile = + toml::from_str(STEWARD_CONFIG).expect("Couldn't deserialize"); + validate_sgx_config(&csr_object, &config_obj.sgx_config.unwrap()).unwrap(); + } + + #[test] + fn test_sgx_signed_csr_bad_config_signer() { + let config_raw = r#" + [snp] + policy_flags = "SingleSocket | Debug" + enarx_signer = ["1234567890", "00112233445566778899"] + minimum_abi = "1.0" + + [sgx] + enarx_signer = ["1234567890", "00112233445566778899"] + "#; + + let csr_object = CertReq::from_der(ICELAKE_CSR).unwrap(); + let config_obj: ConfigurationFile = + toml::from_str(config_raw).expect("Couldn't deserialize"); + + match validate_sgx_config(&csr_object, &config_obj.sgx_config.unwrap()) { + Ok(_) => { + assert!(false); + } + Err(_) => {} // should fail + } + } + + #[test] + fn test_sgx_signed_csr_bad_config_enclave_version() { + let config_raw = r#" + [snp] + policy_flags = "SingleSocket | Debug" + enarx_signer = ["1234567890", "00112233445566778899"] + minimum_abi = "1.0" + + [sgx] + enclave_security_version = 9999 + "#; + + let csr_object = CertReq::from_der(ICELAKE_CSR).unwrap(); + let config_obj: ConfigurationFile = + toml::from_str(config_raw).expect("Couldn't deserialize"); + + match validate_sgx_config(&csr_object, &config_obj.sgx_config.unwrap()) { + Ok(_) => { + assert!(false); + } + Err(_) => {} // should fail + } + } + + #[test] + fn test_snp_signed_canned_csr() { + let csr_object = CertReq::from_der(MILAN_CSR).unwrap(); + let config_obj: ConfigurationFile = + toml::from_str(STEWARD_CONFIG).expect("Couldn't deserialize"); + validate_snp_config(&csr_object, &config_obj.snp_config.unwrap()).unwrap(); + } + + #[test] + fn test_snp_signed_canned_csr_bad_author_key() { + let csr_object = CertReq::from_der(MILAN_CSR).unwrap(); + + let config_raw = r#" + [snp] + policy_flags = "SingleSocket | Debug" + enarx_signer = ["1234567890", "00112233445566778899"] + + [sgx] + enclave_security_version = 9999 + "#; + let config_obj: ConfigurationFile = + toml::from_str(config_raw).expect("Couldn't deserialize"); + + match validate_snp_config(&csr_object, &config_obj.snp_config.unwrap()) { + Ok(_) => { + unreachable!(); + } + Err(_) => {} // should fail + } + } + + #[test] + fn test_snp_signed_canned_csr_bad_abi_version() { + let csr_object = CertReq::from_der(MILAN_CSR).unwrap(); + + let config_raw = r#" + [snp] + minimum_abi = "254.0" + "#; + let config_obj: ConfigurationFile = + toml::from_str(config_raw).expect("Couldn't deserialize"); + + match validate_snp_config(&csr_object, &config_obj.snp_config.unwrap()) { + Ok(_) => { + unreachable!(); + } + Err(_) => {} // should fail + } + } + } } diff --git a/testdata/steward.toml b/testdata/steward.toml new file mode 100644 index 00000000..7f5c8eab --- /dev/null +++ b/testdata/steward.toml @@ -0,0 +1,12 @@ +[sgx] +enarx_signer = ["c8dc9fe36caaeef871e6512c481092754c57c2ea999f128282ccb563d1602774"] +cpu_svn = [6, 6, 14, 13, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +enclave_security_version = 0 +enclave_product_id = 0 + +[snp] +enarx_signer = ["5b2181f5e2294fa0709d22b3f85d9d88b287b897c6b7289004802b53bbf09bc50f5469f98a6d6718d5f9c918d3d3c16f"] +id_key_digest = ["966a25a22ee44283aa51bfb3682c990fd9e0a7457c5f60f4ac4eb5c41715478c4b206b0e01dc11aae8628f5aa29e0560"] +minimum_abi = "1.51" +policy_flags = "SMT" +platform_info_flags = "TSME"