From 554ee0f0ace62407cc66a163fadb325db328eaa4 Mon Sep 17 00:00:00 2001 From: Richard Zak Date: Tue, 8 Nov 2022 09:58:04 -0500 Subject: [PATCH] feat: configuration for attestation Signed-off-by: Richard Zak --- Cargo.lock | 32 +++ Cargo.toml | 2 + crates/sgx_validation/Cargo.toml | 3 + crates/sgx_validation/src/config.rs | 130 ++++++++++++ crates/sgx_validation/src/icelake.signed.csr | Bin 0 -> 4969 bytes crates/sgx_validation/src/lib.rs | 79 +++++--- crates/sgx_validation/src/main.rs | 47 +++++ crates/snp_validation/Cargo.toml | 6 +- crates/snp_validation/src/config.rs | 179 +++++++++++++++++ crates/snp_validation/src/lib.rs | 106 +++++++--- crates/snp_validation/src/main.rs | 37 ++++ crates/snp_validation/src/milan.signed.csr | Bin 0 -> 2850 bytes src/main.rs | 200 ++++++++++++++++++- testdata/steward.toml | 12 ++ 14 files changed, 781 insertions(+), 52 deletions(-) create mode 100644 crates/sgx_validation/src/config.rs create mode 100644 crates/sgx_validation/src/icelake.signed.csr create mode 100644 crates/sgx_validation/src/main.rs create mode 100644 crates/snp_validation/src/config.rs create mode 100644 crates/snp_validation/src/main.rs create mode 100644 crates/snp_validation/src/milan.signed.csr create mode 100644 testdata/steward.toml 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..29292653 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ 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"] } [target.'cfg(not(target_os = "wasi"))'.dependencies] tokio = { version = "^1.21.2", features = ["rt-multi-thread", "macros"], default-features = false } @@ -41,6 +42,7 @@ http = { version = "^0.2.6", default-features = false } memoffset = { version = "0.7.1", default-features = false } rstest = { version = "0.15", default-features = false } testaso = { version = "0.1", default-features = false } +toml = "0.5" [profile.release] incremental = 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..8cfcbc58 --- /dev/null +++ b/crates/sgx_validation/src/config.rs @@ -0,0 +1,130 @@ +// 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 `features`. + #[serde(default)] + #[serde(deserialize_with = "from_features")] + pub features: Option, + + /// Value allowed for `cpusvn`. + pub cpu_svn: Option>, + + /// Value for `isv_svn`, do not allow versions below this. + 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 = flags | Features::CET; + } + "Debug" => { + flags = flags | Features::DEBUG; + } + "Eint_Key" => { + flags = flags | Features::EINIT_KEY; + } + "KSS" => { + flags = flags | Features::KSS; + } + "Provisioning_Key" => { + flags = flags | Features::PROVISIONING_KEY; + } + _ => return Err(D::Error::custom(format!("unknown flag '{}'", flag))), + } + } + + Ok(Some(flags.bits())) +} + +impl Into for &Config { + fn into(self) -> Features { + match self.features { + Some(f) => Features::from_bits_truncate(f), + None => Features::empty(), + } + } +} + +impl Config { + pub fn features(&self) -> Features { + self.into() + } +} + +#[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 0000000000000000000000000000000000000000..e6bb2502c35dda757bdac82afda5022ef2a0500c GIT binary patch literal 4969 zcmc&&d5jy?8Talco9yJ?0D%x7swhP);PJ8RH6+V0iXdyj$+x7)m$4^#m&wu^R$Inl`YVqM|ckbwVfBk;^wlDAd@bB?^qGS-!+WyLNBA?ZwSY zU-Q=*<2pNM&Ny6v{`#T(lE@83M!Ju>q8rugS-yZ86Z|J$^cuiW_BvOV~F<);pxF229awpcEEcFDWg!RebWoHw7| zYX4|QxA5R9aLHRQxjAeP^ZC@$+wI$4AAWGjqw8n))!fVcX zb>2$r7tzJB(36+my6}ycZk}_&2j9+GKIP9{)SG>m&wAqbTi@Ken11ZH@87)d`qE(D5AO7bD!U#CEPi0_jm9f0M^;;u$M5>Ic+Rw`7j}`W zUS4_@>^$8}C%3nuZ22+Qly_z>>t6r%(|`SF#*Uq@MW0-C@5l3wEcd9Fe7<7y@`Km7 zPUpM(cWa*iY}Gx>Zo6p5@~76N*FJG@D&_M2bAQL}S69Eh`umMnb=F>a?}-2T75}g< zc&VoF7E4=u$E3-fQ>J!d)27duIcxTu?z!ijd)~bB zFPMMff{QM`WU?hX(_*=*aY2e|m)3y6LZGp-lt}kv zhFK<@bJzUE9FqZ5zg@HQx(hc5O=SlP5!TG$6dG?}ags{&1Ym$oMt~8KLcMq-WN0#; zP+1z1QXL90hQ=c~cReOSQV3E}9%{|;gl34?_zQyFU5_*Fx*8UXF}t%ylR$!+Pk|uL z6EpSO)NVB!$KKo@DgjVpy`n$q0XV%^cVqyqD|9e%I8Hh>+!r;jO6qTicsgpg29ZA zAHYaRkH0|19GrpfRD#i~u?!{ikR6T6Fubyzt|S;r6YRBAJV(_C6L5_sVrbh@^gh_K zE`~BVMK)oChdcx8B}U?%ZlF;NL5w{cln3A}fDB&8f{COkR}HeK+8-I@3mzXYrS+1P z;f6?;6Xpu#5Q36#|yL;H{@a2<) z8{r%t4MmEookqXzir1lMqz8WlNgdthNiuwKu51ftTN{|Kxt%lIxIAf3iF&3>~%o!9YqG;MV$mE8aQ?m1BndeNpNZuM}%sb z07Em=s0IM52}G!sOQ~#{>7j6xw-f<2R5$|t1_e~_gr#Y50tldqV-pS9QNu($dkVZj zpa@1iO?;tziWiI%aO40-n}nZW7{#5Z!26Hl6PJ(zM-3AqN>K6V}uP|7&El)0jio!{5VY@C3J$AsIe)) zi+avxtEK!&Td2nywNt%=M3AzZT*)>-J8V)YOyl9OJ5=|$K$-$W)v%R~RZ^07*u)0X z3SBPtiJY7-`|{CBk@Cfb!Z5Bu6R$&l5!Mti%153kYE8?j1&`1(DCaWvalumnr&Pmn zyPN6p6a^~J*#d%TYIK-z_sTpd94)yJ@C3#)S`A5}+NcN!;3-*0MO`Vgw9gYFpcJ#a zaikWoRUujh0{bBf(JBzwnDUJ&1W3{bUh|W{#%CxbY_h=zVH9B|Z%)K`52&#jpkd>o zGZRA>1iY-tObuyQq#DoRnt)R+OD!B-Du&iqjprd6G+K{&Q7|a2sh%DZ30g^A&seOL*4OMR(#i5?_! zk_kDUN@`*hlBq1+B61TuhIsu<1P>o2Q=@y5bM9&a5y_hq^>_v(ni3>9DOwX`J>*Vc zW(Kva32Ic=SRN4=LrL8%ZpSCE zXaGY5tQIaS*)(qSr27<=2{~liYPVOS;UundWX2{Zu{`JrW=LB!-5V!ZLXFuZkRy9* z*}7|5^&6}D_ zQsg1Gz?j)QEEy*@t5Nb;^NE^OalH2&ZC1Hz%pshT^bO8qKZf)T&X1+u80n!iOa>_@ z%30K)QihL|Ydl>*n9$1yS+jBbf?1niDH!9CV`F{xS%*FzIo9Z(&Ng3L(-k|@d?otw zl);m%8u4eW=|HGVm{DpV?ZRySv|sXybSlJTVtJ@=gGxTDIx7w~lO // 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}; @@ -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")); } @@ -85,33 +93,54 @@ impl Sgx { 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..ef12ac5e --- /dev/null +++ b/crates/sgx_validation/src/main.rs @@ -0,0 +1,47 @@ +use cryptography; +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..fb221625 --- /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 signature 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: 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 = flags | PolicyFlags::Debug; + } + "MigrateMA" => { + return Err(D::Error::custom(format!("SNP migration not supported"))); + } + "SingleSocket" => { + flags = flags | PolicyFlags::SingleSocket; + } + "SMT" => { + flags = 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 Into for Config { + fn into(self) -> Version { + match self.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..af3bf9f8 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; +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 Into for Policy { + fn into(self) -> Version { + Version::new(self.abi_major as u64, self.abi_major 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,14 @@ 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 +360,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,6 +379,52 @@ 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 curr_ver = report.body.policy.into(); + if req_ver.lt(&curr_ver) { + 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.is_some() { + let signers = config_ref.id_key.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()?); diff --git a/crates/snp_validation/src/main.rs b/crates/snp_validation/src/main.rs new file mode 100644 index 00000000..defedd92 --- /dev/null +++ b/crates/snp_validation/src/main.rs @@ -0,0 +1,37 @@ +use cryptography; +use cryptography::x509::attr::Attribute; +use cryptography::x509::request::{CertReq, ExtensionReq}; +use der::Decode; +use snp_validation; +use snp_validation::Report; +use snp_validation::{Evidence, 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 0000000000000000000000000000000000000000..f3c30c26db8e59a91433fada15e70e5e0ab8ca7e GIT binary patch literal 2850 zcmXqL;+8XL;#$JQ$Y8)=P-Y;&#;(=oan6>Bk&RWmk%d8tIf;SgM8&ei`$ae7_eAQJ z3Eh;vS;PI|>?DI}mM&+1U-;s>RYaBPKl|Qmdma>>k9jMzYR?1No3Hc_-1~KG%F;HA zT^7Frb*t0bZH_z>mtM*HQ-W2HWsOF&;{#8IPcbut`pk5{KV8tor3`e0JR2v_?zZo| zjGTOiOyzzGs%6J`nxhACm^;SNsBD@jyv&Pgmvgel?RVRCjflsAwCspsMm zb4)2q%u7y9QSi-7F3MMMNiEAvPAxVR0){nE0W%M0uxpsEZ)Q$no`IY=uaS{~rIDqf zk%@(=Srm|KiNrOig4s-K#|awnLmbBea$K0Rt2eH+A+*5mOHuusAC7Abxfbf3^Dmi` zU=w?Ha&&~$R-4pA3%E|-zO5hhf5Opp!4E6KOK#@sDbDuEd+&c&e75GA3EkY!Pxqaf zuHPgt9+Yn;x96J1p+EZb%bzU!tm3!IYLDLQmRXCN7{v^l7(t=M2~GxcDi|4AfKkO@ zAdV1WV&M|w0EeNjlYxOCG{pdwGK1AJp$IWSv?Hrx!4zV}6k@{^V#gF>1_`mD*vJ7A ziZ$>>IGBaSA!bv;&O^RkbM{NEZnfoGz0Ty$^7b2L>!$y>zTi3g#fEe2SN?9gRhuEQ z>3g(_mgd~Qjg1Gx3wykSSFU|Nuj!J{UTP(I<|bfZR%uuLGWhe-cpG2qrMqvnSc~$d z4&O~KJ@YHaCG_#ENtbd?I~;O!Vtt|;adyFHvxhw|GJoBOUU6FWto|d*Ya;ezU zxpPEvCrnxK%V}x$>=zr|PVKr~7v!PLzIXrp!e8En)obl|HrXmVeCv>LH40m|r{<}b zWe>v)p3arZKcD+GC^b%>-*hkI(*`~tJ$@y{4GFfIar-MR{1kN!PB0Ytu^nr3cKmcY zVWZiTpx+Aw-d^F>uj^c;&UW?06sai&0ST7rSx=Yo^_)H)cuip^zf0`OEl>ORd@%ai zGr=cMzUbgdzN4KP@_qOBJ7sFCwEBo#uREjsV)Wg2|^xM}=OqEw`O*dT2zG5%6H`(CLTaKmIt(G1Y?Qym4 zQLdcbyVLBir68~Q&+D6yRjgFMvZwXnwd*ETGo81)&P!FhyCk8??|VW?^NKkE>EBHj ze9`t5x81ch!70;Lvrl94=4<@(xui}r3QKqMvn}SbnXG;$GT`6WW3us7=hTBkr_x52SDmU`WP74VnJ*WfYGijcm1zRe~x_GSkctlX0xwO)pDjS z-=~rd-!5ewGGeGN-2cI%Rq?GtOu?(7{i&yQAi5D`{?9c&iEEFX)ooLAGFX4O&9p1p zQAzfa*}X}Nx5VsNI$?j| zn-7+|*2E`#S>w0$h`6YGkGDcLALAXtRWFkIqZZ9$O^8-({Q5}Ke?h@qrOiKL=XPvr z-!c8zc8v)v4cfuGKg>SLACmdAD>q%@>d%uBmoFd8|KhgwKHtKA^Thj;q}BdTdKT*G z!?|VI_IECplQIRq2&Vog0AS&W-SrF%lol8;^O=nq89*4u$7SDO6aO0&^!cBM?Sz!@ zjh8)BHwD=nFUyGZ*DB$%bUMl1=e(y^{aMeg-LoPO^sa8{JCW6b%l&M2Nll`b{9S#% zbFRC+yMN+S@PcaL)e8KRH3I9ZG}3*~Uk{0{o?Fb7S+$@@ENJ5$T;`05(>DYRxY#(r zjZ+q8X66hAgH$F1o6oOww4UEJzdF%Pfj93i`$Da~e;6v7ZXJ&A`5Vd|S{IhpTle-O zU+X!?Ls@UGRZnIzWZ1OR*y{7)S8| literal 0 HcmV?d00001 diff --git a/src/main.rs b/src/main.rs index f502aa39..2ec96824 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, @@ -86,6 +87,15 @@ struct Args { san: Option, } +#[derive(Clone, Deserialize, Debug, Default, Serialize)] +struct ConfigurationFile { + #[serde(rename = "sgx")] + sgx_config: Option, + + #[serde(rename = "snp")] + snp_config: Option, +} + #[derive(Debug)] #[cfg_attr(test, derive(Clone))] struct State { @@ -336,8 +346,8 @@ 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, None, dbg), Sgx::ATT), + Snp::OID => (Snp::default().verify(&info, &ext, None, dbg), Snp::ATT), oid => { debug!("extension `{oid}` is unsupported"); return Err(StatusCode::BAD_REQUEST); @@ -869,4 +879,190 @@ 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"] + minimum_abi = "254.9" + + [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(_) => { + assert!(false); + } + Err(_) => {} // should fail + } + } + } } diff --git a/testdata/steward.toml b/testdata/steward.toml new file mode 100644 index 00000000..b8e5797a --- /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 = ["966a25a22ee44283aa51bfb3682c990fd9e0a7457c5f60f4ac4eb5c41715478c4b206b0e01dc11aae8628f5aa29e0560"] +minimum_abi = "1.51" +policy_flags = "SMT" +platform_info_flags = "TSME"