From 9a9d9acb91a4f4d5401a27321c946f1fb237c3fa 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 Co-authored-by: Roman Volosatovs Signed-off-by: Richard Zak --- Cargo.lock | 43 +++ Cargo.toml | 7 +- crates/cryptography/Cargo.toml | 1 + crates/sgx_validation/Cargo.toml | 3 + crates/sgx_validation/src/config.rs | 164 +++++++++++ crates/sgx_validation/src/icelake.signed.csr | Bin 0 -> 4969 bytes crates/sgx_validation/src/lib.rs | 117 +++++--- crates/snp_validation/Cargo.toml | 6 +- crates/snp_validation/src/config.rs | 143 +++++++++ crates/snp_validation/src/lib.rs | 239 +++++++++------ crates/snp_validation/src/milan.signed.csr | Bin 0 -> 2850 bytes crates/validation_common/Cargo.toml | 13 + crates/validation_common/src/lib.rs | 208 +++++++++++++ src/main.rs | 289 ++++++++++++++++++- steward.toml.example | 36 +++ testdata/steward.toml | 14 + 16 files changed, 1142 insertions(+), 141 deletions(-) create mode 100644 crates/sgx_validation/src/config.rs create mode 100644 crates/sgx_validation/src/icelake.signed.csr create mode 100644 crates/snp_validation/src/config.rs create mode 100644 crates/snp_validation/src/milan.signed.csr create mode 100644 crates/validation_common/Cargo.toml create mode 100644 crates/validation_common/src/lib.rs create mode 100644 steward.toml.example create mode 100644 testdata/steward.toml diff --git a/Cargo.lock b/Cargo.lock index dc48fae0..63dc6fe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,6 +243,7 @@ dependencies = [ "rsa", "rustls-pemfile", "sec1", + "serde", "sha2", "signature", "spki", @@ -447,6 +448,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" @@ -981,12 +988,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" @@ -1017,8 +1041,11 @@ dependencies = [ "anyhow", "cryptography", "der", + "serde", "sgx", "testaso", + "toml", + "validation_common", ] [[package]] @@ -1076,7 +1103,11 @@ dependencies = [ "cryptography", "der", "flagset", + "semver", + "serde", "testaso", + "toml", + "validation_common", ] [[package]] @@ -1122,16 +1153,19 @@ dependencies = [ "memoffset", "mime", "rstest", + "serde", "sgx", "sgx_validation", "snp_validation", "testaso", "tokio", + "toml", "tower", "tower-http", "tracing", "tracing-subscriber", "uuid", + "validation_common", "zeroize", ] @@ -1354,6 +1388,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "validation_common" +version = "0.2.0" +dependencies = [ + "hex", + "serde", + "toml", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b44d95b6..0e882a53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,16 +30,20 @@ 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"], default-features = false } +toml = { version = "0.5", default-features = false } [target.'cfg(not(target_os = "wasi"))'.dependencies] tokio = { version = "^1.23.0", features = ["rt-multi-thread", "macros"], default-features = false } [dev-dependencies] +validation_common = { path = "crates/validation_common" } tower = { version = "^0.4.11", features = ["util"], default-features = false } axum = { version = "^0.5.17", default-features = false } http = { version = "^0.2.6", default-features = false } memoffset = { version = "0.7.1", default-features = false } rstest = { version = "0.16", default-features = false } +sgx = { version = "0.6.0", default-features = false } testaso = { version = "0.1", default-features = false } [profile.release] @@ -51,7 +55,8 @@ strip = true [workspace] resolver = '2' members = [ + 'crates/cryptography', 'crates/sgx_validation', 'crates/snp_validation', - 'crates/cryptography', + 'crates/validation_common', ] diff --git a/crates/cryptography/Cargo.toml b/crates/cryptography/Cargo.toml index 15b687d4..080e9fca 100644 --- a/crates/cryptography/Cargo.toml +++ b/crates/cryptography/Cargo.toml @@ -13,6 +13,7 @@ rand = { version = "0.8", features = ["std"], default-features = false } rsa = {version = "0.7.2", features = ["std"], default-features = false } rustls-pemfile = {version = "1.0.1", default-features = false } sec1 = { version = "0.3", features = ["std", "pkcs8"], default-features = false } +serde = { version = "1.0", features = ["derive", "std"], default-features = false } sha2 = { version = "^0.10.2", default-features = false } signature = {version = "1.6", default-features = false } spki = { version = "0.6", default-features = false } diff --git a/crates/sgx_validation/Cargo.toml b/crates/sgx_validation/Cargo.toml index 9d648499..b85e1cc2 100644 --- a/crates/sgx_validation/Cargo.toml +++ b/crates/sgx_validation/Cargo.toml @@ -7,9 +7,12 @@ description = "Intel SGX Attestation validation library for Steward" [dependencies] cryptography = { path = "../cryptography" } +validation_common = { path = "../validation_common" } anyhow = { version = "^1.0.55", default-features = false } der = { version = "0.6", features = ["std"], default-features = false } +serde = { version = "1.0", features = ["derive", "std"], default-features = false } sgx = { version = "0.6.0", default-features = false } [dev-dependencies] testaso = { version = "0.1", default-features = false } +toml = { version = "0.5", default-features = false } diff --git a/crates/sgx_validation/src/config.rs b/crates/sgx_validation/src/config.rs new file mode 100644 index 00000000..0d5a06ea --- /dev/null +++ b/crates/sgx_validation/src/config.rs @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2022 Profian Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use serde::{Deserialize, Deserializer}; +use sgx::parameters::{Features, MiscSelect}; +use validation_common::Measurements; + +#[derive(Clone, Deserialize, Debug, Eq, PartialEq)] +pub enum SgxFeatures { + CET, + Debug, + EInitKey, + KSS, + ProvisioningKey, +} + +#[derive(Clone, Deserialize, Debug, Eq, PartialEq)] +pub enum SgxMiscSelect { + EXINFO, +} + +#[derive(Clone, Deserialize, Debug, Default, Eq, PartialEq)] +pub struct Config { + /// Values for `mrsigner` in the report body, as `Measurements::signer()` + /// This is the list of public keys which have signed the Enarx binary. + /// Values for `mrenclave` in the report body, as `Measurements::hash()` + /// 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, flatten)] + pub measurements: Measurements<32>, + + /// Values for `features`. + /// Checked against `sgx::parameters::attributes::Attributes::features()` + /// These are CPU features reported by the CPU firmware, and most features are not + /// relevant to security concerns, but are used for code execution. Only the features + /// relevant for security are parsed here. + #[serde(default)] + #[serde(deserialize_with = "from_features")] + pub features: Features, + + /// Minimum value for `isv_svn`. + /// Checked against `sgx::report::ReportBody::enclave_security_version()` + /// This is the security version of the enclave. + pub enclave_security_version: Option, + + /// Value for `isv_prodid`, do not allow other ids. + /// Checked against `sgx::report::ReportBody::enclave_product_id()` + pub enclave_product_id: Option, + + /// Extra enclave creation parameters + /// Checked against `sgx::report::ReportBody::misc_select()` + #[serde(default)] + #[serde(deserialize_with = "from_misc_select")] + pub misc_select: MiscSelect, +} + +fn from_features<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: Vec = Deserialize::deserialize(deserializer)?; + + let mut flags = Features::empty(); + + // Must be set according to Intel SGX documentation, this indicates permission + // to create SGX enclaves. + flags |= Features::INIT; + + // Required by Enarx, as Wasmtime requires 64-bit, and modern systems are all 64-bit anyway + flags |= Features::MODE64BIT; + + for flag in s { + match flag { + SgxFeatures::CET => { + flags |= Features::CET; + } + SgxFeatures::Debug => { + flags |= Features::DEBUG; + } + SgxFeatures::EInitKey => { + flags |= Features::EINIT_KEY; + } + SgxFeatures::KSS => { + flags |= Features::KSS; + } + SgxFeatures::ProvisioningKey => { + flags |= Features::PROVISIONING_KEY; + } + } + } + + Ok(flags) +} + +fn from_misc_select<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: Vec = Deserialize::deserialize(deserializer)?; + + let mut flags = MiscSelect::default(); + + for flag in s { + match flag { + SgxMiscSelect::EXINFO => { + flags |= MiscSelect::EXINFO; + } + } + } + + Ok(flags) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use validation_common::Digest; + + #[test] + fn empty_config() { + assert!(toml::from_str::("").is_err()); + } + + #[test] + fn serde() { + const SIGNER: &str = + r#"signer = ["2eba0f494f428e799c22d6f12778aebea4dc8d991f9e63fd3cddd57ac6eb5dd9"]"#; + + let signer: HashSet<_> = vec![Digest([ + 0x2e, 0xba, 0x0f, 0x49, 0x4f, 0x42, 0x8e, 0x79, 0x9c, 0x22, 0xd6, 0xf1, 0x27, 0x78, + 0xae, 0xbe, 0xa4, 0xdc, 0x8d, 0x99, 0x1f, 0x9e, 0x63, 0xfd, 0x3c, 0xdd, 0xd5, 0x7a, + 0xc6, 0xeb, 0x5d, 0xd9, + ])] + .into_iter() + .collect(); + + let config: Config = toml::from_str(&format!( + r#" +{SIGNER} +features = ["Debug"] +misc_select = ["EXINFO"] +"#, + )) + .expect("Couldn't deserialize"); + + assert_eq!(config.measurements.signer, signer); + assert_eq!( + config.features.bits(), + (Features::DEBUG | Features::INIT | Features::MODE64BIT).bits() + ); + assert!(config.misc_select.contains(MiscSelect::EXINFO)); + } + + #[test] + fn too_short() { + let config: Result = toml::from_str( + r#" + signer = ["41c179d5c0d5bc4915752ccf9bbd2baa574716832235ef5bb998fadcda1e46"] + "#, + ); + assert!(config.is_err()); + } +} 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 anyhow::{anyhow, Result}; +use crate::config::Config; +use anyhow::{bail, ensure, 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}; #[derive(Clone, Debug)] pub struct Sgx([Certificate<'static>; 1]); @@ -29,7 +30,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,16 +39,18 @@ impl Sgx { Ok(signer) } - pub fn verify(&self, cri: &CertReqInfo<'_>, ext: &Extension<'_>, dbg: bool) -> Result { - if ext.critical { - return Err(anyhow!("sgx extension cannot be critical")); - } + pub fn verify( + &self, + cri: &CertReqInfo<'_>, + ext: &Extension<'_>, + config: Option<&Config>, + dbg: bool, + ) -> Result { + ensure!(!ext.critical, "sgx extension cannot be critical"); // Decode the quote. let (quote, bytes): (quote::Quote<'_>, _) = ext.extn_value.parse()?; - if !bytes.is_empty() { - return Err(anyhow!("unknown trailing bytes in sgx quote")); - } + ensure!(bytes.is_empty(), "unknown trailing bytes in sgx quote"); // Parse the certificate chain. let chain = quote.chain()?; @@ -74,43 +77,65 @@ impl Sgx { // than whatever Intel chose for the PCK. // // Additionally, we do this check early to be defensive. - if cri.public_key.algorithm != pck.subject_public_key_info.algorithm { - return Err(anyhow!("sgx pck algorithm mismatch")); - } + ensure!( + cri.public_key.algorithm == pck.subject_public_key_info.algorithm, + "sgx pck algorithm mismatch" + ); if !dbg { - // TODO: Validate that the certification request came from an SGX enclave. - 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 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")); + // Validate that the certification request came from an SGX enclave. + let hash = Sha256::digest(cri.public_key.to_vec()?); + ensure!( + hash.as_slice() == &rpt.reportdata[..hash.as_slice().len()], + "sgx report data is invalid" + ); + + if let Some(config) = config { + if !config.measurements.signer.is_empty() { + let signed = config.measurements.signer.contains(&rpt.mrsigner); + ensure!(signed, "sgx untrusted enarx signer"); + } + + if !config.measurements.hash.is_empty() { + let approved = config.measurements.hash.contains(&rpt.mrenclave); + ensure!(approved, "sgx untrusted enarx hash"); + } + + if !config.measurements.hash_blacklist.is_empty() { + let denied = config.measurements.hash_blacklist.contains(&rpt.mrenclave); + ensure!(!denied, "sgx untrusted enarx hash"); + } + + if let Some(product_id) = config.enclave_product_id { + ensure!( + rpt.enclave_product_id() == product_id, + "sgx untrusted enclave product id", + ); + } + + if let Some(version) = config.enclave_security_version { + ensure!( + rpt.enclave_security_version() >= version, + "sgx untrusted enclave security version" + ); + } + + if !config.features.is_empty() + && !rpt + .attributes() + .features() + .difference(config.features) + .is_empty() + { + bail!("sgx untrusted features"); + } + + if !config.misc_select.is_empty() { + ensure!( + rpt.misc_select().difference(config.misc_select).is_empty(), + "sgx untrusted misc select" + ); + } } } diff --git a/crates/snp_validation/Cargo.toml b/crates/snp_validation/Cargo.toml index a33087b4..17ecb7ef 100644 --- a/crates/snp_validation/Cargo.toml +++ b/crates/snp_validation/Cargo.toml @@ -7,9 +7,13 @@ description = "AMD SEV-SNP Attestation validation library for Steward" [dependencies] cryptography = { path = "../cryptography" } +validation_common = { path = "../validation_common" } 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 } +serde = { version = "1.0", features = ["derive"], default-features = false } +semver = { version = "1.0", features = ["serde"], default-features = false } [dev-dependencies] testaso = { version = "0.1", default-features = false } +toml = { version = "0.5", default-features = false } diff --git a/crates/snp_validation/src/config.rs b/crates/snp_validation/src/config.rs new file mode 100644 index 00000000..0e830651 --- /dev/null +++ b/crates/snp_validation/src/config.rs @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2022 Profian Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use crate::{PlatformInfoFlags, PolicyFlags}; +use flagset::FlagSet; +use semver::VersionReq; +use serde::{Deserialize, Deserializer}; +use std::collections::HashSet; +use validation_common::{Digest, Measurements}; + +#[derive(Clone, Deserialize, Debug, Default, Eq, PartialEq)] +pub enum SnpPolicyFlags { + Debug, + SingleSocket, + #[default] + SMT, +} + +#[derive(Clone, Deserialize, Debug, Default, Eq, PartialEq)] +pub struct Config { + /// Values for `author_key_digest` in the report body, as `Measurements::signer()` + /// This is the list of public keys which have signed the signing key of the Enarx binary. + /// Values for `measurement` in the report body, as `Measurements::hash()` + /// 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(flatten, default)] + pub measurements: Measurements<48>, + + /// Values for `id_key_digest` in the report body. + /// This is the list of public keys which have signed the Enarx binary. + #[serde(default)] + pub id_key_digest: HashSet>, + + /// Prohibited values for `id_key_digest` in the report body. + /// This is the list of public keys which have signed the Enarx binary. + #[serde(default)] + pub id_key_digest_blacklist: HashSet>, + + /// Minimum value for `policy.abi_major`.`policy.abi_minor` + #[serde(default)] + pub abi: VersionReq, + + #[serde(default)] + #[serde(deserialize_with = "from_policy_string")] + pub policy_flags: Option, + + #[serde(default)] + pub platform_info_flags: Option, +} + +fn from_policy_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: Vec = Deserialize::deserialize(deserializer)?; + + let mut flags = FlagSet::::from(PolicyFlags::Reserved); + + for flag in s { + match flag { + SnpPolicyFlags::Debug => { + flags |= PolicyFlags::Debug; + } + SnpPolicyFlags::SingleSocket => { + flags |= PolicyFlags::SingleSocket; + } + SnpPolicyFlags::SMT => { + flags |= PolicyFlags::SMT; + } + } + } + + Ok(Some(flags.bits())) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use validation_common::Digest; + + const SIGNER: &str = r#"signer = ["6d193a53817dfc0c7834e6c84687414c08de0b2839aa6593272bd628d1e23c3470f22e80ddec3262f0079ae4747d36aa"]"#; + const DIGEST: Digest<48> = Digest([ + 0x6d, 0x19, 0x3a, 0x53, 0x81, 0x7d, 0xfc, 0x0c, 0x78, 0x34, 0xe6, 0xc8, 0x46, 0x87, 0x41, + 0x4c, 0x08, 0xde, 0x0b, 0x28, 0x39, 0xaa, 0x65, 0x93, 0x27, 0x2b, 0xd6, 0x28, 0xd1, 0xe2, + 0x3c, 0x34, 0x70, 0xf2, 0x2e, 0x80, 0xdd, 0xec, 0x32, 0x62, 0xf0, 0x07, 0x9a, 0xe4, 0x74, + 0x7d, 0x36, 0xaa, + ]); + + #[test] + fn empty_config() { + assert!(toml::from_str::("").is_err()); + } + + #[test] + fn flags() { + let config: Config = toml::from_str(&format!( + r#" + {SIGNER} + policy_flags = ["SingleSocket", "Debug"] + platform_info_flags = "SME" + "#, + )) + .expect("Couldn't deserialize"); + + assert_eq!(config.platform_info_flags.unwrap(), PlatformInfoFlags::SME); + + assert_eq!( + FlagSet::::new(config.policy_flags.unwrap()).unwrap(), + PolicyFlags::SingleSocket | PolicyFlags::Debug | PolicyFlags::Reserved + ); + + assert_eq!(config.measurements.signer.len(), 1); + assert!(config.measurements.signer.contains(&DIGEST)); + } + + #[test] + fn semver() { + let config: Config = toml::from_str(&format!( + r#" + {SIGNER} + abi = "1.0" + "#, + )) + .expect("Couldn't deserialize"); + assert_eq!(config.abi.to_string(), "^1.0"); + assert_eq!( + config.measurements.signer, + HashSet::from_iter(vec![DIGEST].into_iter()) + ); + } + + #[test] + fn too_short() { + let config: Result = toml::from_str( + r#" + signer = ["7e95e98b98abd5bf71ff3f8c7cd0678a2ea46b3438c6684f49469f5c5a442cb0fb6ad33fa000e04d4ae4635051dd68"] + "#, + ); + assert!(config.is_err()); + } +} diff --git a/crates/snp_validation/src/lib.rs b/crates/snp_validation/src/lib.rs index e7c046bb..9f989870 100644 --- a/crates/snp_validation/src/lib.rs +++ b/crates/snp_validation/src/lib.rs @@ -1,13 +1,16 @@ // SPDX-FileCopyrightText: 2022 Profian Inc. // SPDX-License-Identifier: AGPL-3.0-only -use cryptography::ext::*; +pub mod config; + +use self::config::Config; use std::{fmt::Debug, mem::size_of}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{bail, ensure, Context, Result}; use cryptography::const_oid::db::rfc5912::ECDSA_WITH_SHA_384; use cryptography::const_oid::ObjectIdentifier; +use cryptography::ext::TbsCertificateExt; use cryptography::sec1::pkcs8::AlgorithmIdentifier; use cryptography::sha2::{Digest, Sha384}; use cryptography::x509::ext::Extension; @@ -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,14 @@ flags! { SMT = 1 << 0, } + /// Indication of memory mode, either SME or TSME. + #[derive(Deserialize, Serialize, Default)] pub enum PlatformInfoFlags: u8 { + /// Transparent Secure Memory Encryption TSME = 1 << 1, - SMT = 1 << 0, + /// Secure Memory Encryption + #[default] + SME = 1 << 0, } } @@ -58,18 +69,24 @@ pub struct Policy { rsvd: [u8; 5], } +impl From for Version { + fn from(value: Policy) -> Self { + Version::new(value.abi_major as _, value.abi_minor as _, 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) @@ -106,7 +123,7 @@ struct Body { pub author_key_digest: [u8; 48], /// The report ID of this guest pub report_id: [u8; 32], - // The report ID of this guest's migration agent + /// The report ID of this guest's migration agent pub report_id_ma: [u8; 32], /// Represents the bootloader, SNP firmware, and patch level of the CPU pub reported_tcb: u64, @@ -178,7 +195,7 @@ impl Es384 { #[repr(C, packed)] #[derive(Copy, Clone)] -union Signature { +pub union Signature { bytes: [u8; 512], es384: Es384, } @@ -186,7 +203,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, } @@ -233,13 +250,17 @@ impl Snp { } } - Err(anyhow!("snp vcek is untrusted")) + bail!("snp vcek is untrusted") } - pub fn verify(&self, cri: &CertReqInfo<'_>, ext: &Extension<'_>, dbg: bool) -> Result { - if ext.critical { - return Err(anyhow!("snp extension cannot be critical")); - } + pub fn verify( + &self, + cri: &CertReqInfo<'_>, + ext: &Extension<'_>, + config: Option<&Config>, + dbg: bool, + ) -> Result { + ensure!(!ext.critical, "snp extension cannot be critical"); // Decode the evidence. let evidence = Evidence::from_der(ext.extn_value)?; @@ -249,7 +270,7 @@ impl Snp { // Force certs to have the same key type as the VCEK. // - // A note about this check is in order. We don't want to build ext + // A note about this check is in order. We don't want to build crypto // algorithm negotiation into this protocol. Not only is it complex // but it is also subject to downgrade attacks. For example, if the // weakest link in the certificate chain is a P384 key and the @@ -261,9 +282,10 @@ impl Snp { // than whatever AMD chose for the VCEK. // // Additionally, we do this check early to be defensive. - if cri.public_key.algorithm != vcek.subject_public_key_info.algorithm { - return Err(anyhow!("snp vcek algorithm mismatch")); - } + ensure!( + cri.public_key.algorithm == vcek.subject_public_key_info.algorithm, + "snp vcek algorithm mismatch" + ); // Extract the report and its signature. let array = evidence @@ -287,96 +309,145 @@ impl Snp { // TODO: additional field validations. // Should only be version 2 - if report.body.version != 2 { - return Err(anyhow!("snp report is an unknown version")); - } + ensure!( + report.body.version == 2, + "snp report has an unknown version" + ); // Check policy - if !report.body.policy.flags.contains(PolicyFlags::Reserved) { - return Err(anyhow!("snp guest policy mandatory reserved flag not set")); - } - - if report.body.policy.flags.contains(PolicyFlags::MigrateMA) { - return Err(anyhow!("snp guest policy migration flag was set")); + ensure!( + report.body.policy.flags.contains(PolicyFlags::Reserved), + "snp guest policy mandatory reserved flag not set" + ); + + // Enarx doesn't support migration to another machine + ensure!( + !report.body.policy.flags.contains(PolicyFlags::MigrateMA), + "snp guest policy migration flag was set" + ); + + // Only check major.minor, as there isn't a revision or patch version in the attestation. + if report.body.policy.abi_major > report.body.current_major { + bail!("snp policy has higher abi major than firmware"); + } else if report.body.policy.abi_minor > report.body.current_minor { + bail!("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")); - } + ensure!( + report.body.rsvd1 == 0, + "snp report reserved field 1 was set" + ); + + ensure!( + report.body.rsvd2 == [0; 24], + "snp report reserved field 2 was set" + ); + + ensure!( + report.body.rsvd3 == 0, + "snp report reserved field 3 was set" + ); + + ensure!( + report.body.rsvd4 == 0, + "snp report reserved field 4 was set" + ); + + ensure!( + report.body.rsvd5 == [0; 168], + "snp report reserved field 5 was set" + ); + + ensure!( + report.body.policy.rsvd == [0; 5], + "snp report policy reserved fields were set" + ); + + ensure!( + report.body.plat_info.rsvd == [0; 7], + "snp report platform_info reserved fields were set" + ); + + ensure!(report.body.sig_algo == 1, "snp signature algorithm not 1"); - for value in report.body.rsvd2 { - if value != 0 { - return Err(anyhow!("snp report reserved fields were set")); - } - } + // Check fields not set by Enarx + ensure!(report.body.family_id == [0; 16], "snp family id was set"); - for value in report.body.rsvd5 { - if value != 0 { - return Err(anyhow!("snp report reserved fields were set")); - } - } + ensure!(report.body.image_id == [0; 16], "snp image id was set"); - for value in report.body.policy.rsvd { - if value != 0 { - return Err(anyhow!("snp report policy reserved fields were set")); - } - } + // Check fields set by Enarx + ensure!( + report.body.host_data == [0; 32], + "snp report host_data field should not be set by Enarx" + ); - for value in report.body.plat_info.rsvd { - if value != 0 { - return Err(anyhow!("snp report platform_info reserved fields were set")); - } - } + ensure!(report.body.vmpl == 0, "snp report vmpl field invalid value"); - // 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" - )); + if let Some(config) = config { + ensure!( + config.abi.matches(&report.body.policy.into()), + "snp minimum abi not met" + ); + + if !config.measurements.signer.is_empty() { + ensure!(report.body.author_key_en == 1, "snp author key unset"); + + let approved = config + .measurements + .signer + .contains(&report.body.author_key_digest); + ensure!(approved, "snp untrusted enarx author_key_digest"); } - } - for value in report.body.host_data { - if value != 0 { - return Err(anyhow!("snp report host_data field not set by Enarx")); + if !config.id_key_digest.is_empty() { + let approved = config.id_key_digest.contains(&report.body.id_key_digest); + ensure!( + approved, + "snp untrusted enarx id_key_digest not in list of allowed key digests" + ); } - } - 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 !config.id_key_digest_blacklist.is_empty() { + let denied = config + .id_key_digest_blacklist + .contains(&report.body.id_key_digest); + ensure!(!denied, "snp untrusted enarx id_key_digest in blacklist"); } - } - if report.body.vmpl != 0 { - return Err(anyhow!("snp report vmpl field not set by Enarx")); - } + if !config.measurements.hash.is_empty() { + let allowed = config.measurements.hash.contains(&report.body.measurement); + ensure!(allowed, "snp untrusted enarx measurement"); + } - if report.body.guest_svn != 0 { - return Err(anyhow!("snp report guest_svn field not set by Enarx")); - } + if !config.measurements.hash_blacklist.is_empty() { + let denied = config + .measurements + .hash_blacklist + .contains(&report.body.measurement); + ensure!(!denied, "snp untrusted enarx hash"); + } - // Check field set by Enarx - for value in report.body.report_id_ma { - if value != 255 { - return Err(anyhow!( - "snp report report_id_ma field not the value set by Enarx" - )); + if let Some(flags) = config.platform_info_flags { + ensure!( + flags == report.body.plat_info.flag, + "snp unexpected platform info flags" + ); } } if !dbg { // Validate that the certification request came from an SNP VM. - 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")); - } - - if report.body.policy.flags.contains(PolicyFlags::Debug) { - return Err(anyhow!("snp guest policy permits debugging")); - } + let hash = Sha384::digest(cri.public_key.to_vec()?); + ensure!( + hash.as_slice() == &report.body.report_data[..hash.as_slice().len()], + "snp report.report_data is invalid" + ); + + ensure!( + !report.body.policy.flags.contains(PolicyFlags::Debug), + "snp guest policy permits debugging" + ); } Ok(false) 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/crates/validation_common/Cargo.toml b/crates/validation_common/Cargo.toml new file mode 100644 index 00000000..21e9ff68 --- /dev/null +++ b/crates/validation_common/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "validation_common" +version = "0.2.0" +edition = "2021" +license = "AGPL-3.0" +description = "Common attestation validation library for Steward" + +[dependencies] +hex = { version = "0.4.3", features = ["alloc"], default-features = false } +serde = { version = "1.0", features = ["derive"], default-features = false } + +[dev-dependencies] +toml = { version = "0.5", default-features = false } \ No newline at end of file diff --git a/crates/validation_common/src/lib.rs b/crates/validation_common/src/lib.rs new file mode 100644 index 00000000..2ee099e8 --- /dev/null +++ b/crates/validation_common/src/lib.rs @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2022 Profian Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use serde::{Deserialize, Deserializer}; +use std::borrow::Borrow; +use std::collections::HashSet; +use std::fmt::{Display, Formatter}; +use std::ops::Deref; + +/// Digest generic in hash size `N` +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct Digest(pub [u8; N]); + +impl<'de, const N: usize> Deserialize<'de> for Digest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + + let dig: String = Deserialize::deserialize(deserializer)?; + let dig = hex::decode(dig).map_err(|e| Error::custom(format!("invalid hex: {e}")))?; + let dig = dig.try_into().map_err(|v: Vec<_>| { + Error::custom(format!( + "expected digest to have length of {N}, got {}", + v.len() + )) + })?; + Ok(Digest(dig)) + } +} + +impl AsRef<[u8; N]> for Digest { + fn as_ref(&self) -> &[u8; N] { + &self.0 + } +} + +impl Borrow<[u8; N]> for Digest { + fn borrow(&self) -> &[u8; N] { + &self.0 + } +} + +impl Deref for Digest { + type Target = [u8; N]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for Digest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +struct UnvalidatedMeasurements { + #[serde(default)] + signer: HashSet>, + + #[serde(default)] + hash: HashSet>, + + #[serde(default)] + hash_blacklist: HashSet>, +} + +impl TryFrom> for Measurements { + type Error = String; + + fn try_from( + UnvalidatedMeasurements { + signer, + hash, + hash_blacklist, + }: UnvalidatedMeasurements, + ) -> Result { + if signer.is_empty() && hash.is_empty() && hash_blacklist.is_empty() { + Err("one of `signer`, `hash`, or `hash_blacklist` must be specified".to_string()) + } else if let Some(offending) = hash.intersection(&hash_blacklist).next() { + Err(format!( + "same hash `{offending}` in both `hash` and `hash_blacklist`" + )) + } else { + Ok(Self { + signer, + hash, + hash_blacklist, + }) + } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] +#[serde(deny_unknown_fields, try_from = "UnvalidatedMeasurements")] +pub struct Measurements { + /// Allowed signing key digests + pub signer: HashSet>, + + /// Allowed Enarx binary digests. + /// 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. + pub hash: HashSet>, + + /// Denied Enarx binary digests. + pub hash_blacklist: HashSet>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let signer: HashSet<_> = HashSet::from([ + Digest([0x12, 0x34, 0x56, 0x78]), + Digest([0x00, 0x11, 0x22, 0x33]), + ]); + + let hash: HashSet<_> = HashSet::from([ + Digest([0x00, 0x11, 0x22, 0x33]), + Digest([0x42, 0xff, 0xff, 0xff]), + ]); + + const SIGNER: &str = r#"signer = ["12345678", "00112233"]"#; + const HASH: &str = r#"hash = ["00112233", "42ffffff"]"#; + const BLACKLIST: &str = r#"hash = ["00112233"]"#; + + assert!(toml::from_str::>(SIGNER).is_err()); + + assert!(toml::from_str::>(&format!( + r#" +{HASH} +{BLACKLIST} +"# + )) + .is_err()); + + assert!(toml::from_str::>(&format!( + r#" +{HASH} +"# + )) + .is_err()); + + assert!(toml::from_str::>(&format!( + r#" +{SIGNER} +"# + )) + .is_err()); + + assert!(toml::from_str::>(&format!( + r#" +{HASH} +"# + )) + .is_err()); + + assert_eq!( + toml::from_str::>(&format!( + r#" +{SIGNER} +"# + )) + .expect("failed to parse config"), + Measurements { + signer: signer.clone(), + hash: Default::default(), + hash_blacklist: Default::default(), + }, + ); + + assert_eq!( + toml::from_str::>(&format!( + r#" +{HASH} +"# + )) + .expect("failed to parse config"), + Measurements { + signer: Default::default(), + hash: hash.clone(), + hash_blacklist: Default::default(), + }, + ); + + assert_eq!( + toml::from_str::>(&format!( + r#" +{SIGNER} +{HASH} +"# + )) + .expect("failed to parse config"), + Measurements { + hash, + signer, + hash_blacklist: Default::default(), + }, + ); + } +} diff --git a/src/main.rs b/src/main.rs index f502aa39..27974d7a 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; use tower_http::trace::{ DefaultOnBodyChunk, DefaultOnEos, DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer, @@ -84,6 +85,15 @@ struct Args { #[arg(long, env = "STEWARD_SAN")] san: Option, + + #[arg(long)] + config: Option, +} + +#[derive(Clone, Deserialize, Debug, Default, Eq, PartialEq)] +struct Config { + sgx: Option, + snp: Option, } #[derive(Debug)] @@ -92,6 +102,7 @@ struct State { key: Zeroizing>, crt: Vec, san: Option, + config: Config, } /// ASN.1 @@ -113,6 +124,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 +132,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 +155,19 @@ impl State { PrivateKeyInfo::from_der(key.as_ref())?; Certificate::from_der(crt.as_ref())?; - Ok(State { crt, san, key }) + let config = if let Some(path) = config { + let config = std::fs::read_to_string(path).context("failed to read config file")?; + toml::from_str(&config).context("failed to parse config")? + } else { + Config::default() + }; + + Ok(State { + crt, + san, + key, + config, + }) } pub fn generate(san: Option, hostname: &str) -> anyhow::Result { @@ -199,7 +224,12 @@ impl State { // Self-sign the certificate. let crt = tbs.sign(&pki)?; - Ok(Self { key, crt, san }) + Ok(Self { + key, + crt, + san, + config: Default::default(), + }) } } @@ -220,7 +250,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 +339,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 +367,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.as_ref(), dbg), + Sgx::ATT, + ), + Snp::OID => ( + Snp::default().verify(&info, &ext, state.config.snp.as_ref(), dbg), + Snp::ATT, + ), oid => { debug!("extension `{oid}` is unsupported"); return Err(StatusCode::BAD_REQUEST); @@ -445,7 +482,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 +536,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 +913,231 @@ mod tests { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } } + + // Unit tests for configuration + mod config { + use crate::Config; + use cryptography::x509::attr::Attribute; + use cryptography::x509::request::{CertReq, ExtensionReq}; + use cryptography::x509::Certificate; + use der::Decode; + use sgx::parameters::MiscSelect; + use sgx_validation::quote::traits::ParseBytes; + use sgx_validation::quote::Quote; + use snp_validation::{Evidence, PolicyFlags, Report, Snp}; + use std::collections::HashSet; + use validation_common::{Digest, Measurements}; + + const DEFAULT_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 assert_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 assert_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: Config = toml::from_str("").expect("Couldn't deserialize"); + + assert!(config.sgx.is_none()); + assert!(config.snp.is_none()); + } + + #[test] + fn test_config() { + let config: Config = toml::from_str( + r#" + [snp] + policy_flags = ["SingleSocket", "Debug", "SMT"] + signer = ["e368c18e60842db9325778532dd81594d732078bf01aa91686be40333da639e08733b910bd057bdda50d715968b075ce"] + abi = ">1.0" + + [sgx] + signer = ["2eba0f494f428e799c22d6f12778aebea4dc8d991f9e63fd3cddd57ac6eb5dd9"] + "#, + ) + .expect("Couldn't deserialize"); + + let snp = snp_validation::config::Config { + measurements: Measurements { + signer: HashSet::from([Digest([ + 0xe3, 0x68, 0xc1, 0x8e, 0x60, 0x84, 0x2d, 0xb9, 0x32, 0x57, 0x78, 0x53, + 0x2d, 0xd8, 0x15, 0x94, 0xd7, 0x32, 0x07, 0x8b, 0xf0, 0x1a, 0xa9, 0x16, + 0x86, 0xbe, 0x40, 0x33, 0x3d, 0xa6, 0x39, 0xe0, 0x87, 0x33, 0xb9, 0x10, + 0xbd, 0x05, 0x7b, 0xdd, 0xa5, 0x0d, 0x71, 0x59, 0x68, 0xb0, 0x75, 0xce, + ])]), + hash: Default::default(), + hash_blacklist: Default::default(), + }, + id_key_digest: Default::default(), + id_key_digest_blacklist: Default::default(), + abi: ">1.0".parse().unwrap(), + policy_flags: Some( + (PolicyFlags::Reserved + | PolicyFlags::SingleSocket + | PolicyFlags::Debug + | PolicyFlags::SMT) + .bits(), + ), + platform_info_flags: None, + }; + + let sgx = sgx_validation::config::Config { + measurements: Measurements { + signer: HashSet::from([Digest([ + 0x2e, 0xba, 0x0f, 0x49, 0x4f, 0x42, 0x8e, 0x79, 0x9c, 0x22, 0xd6, 0xf1, + 0x27, 0x78, 0xae, 0xbe, 0xa4, 0xdc, 0x8d, 0x99, 0x1f, 0x9e, 0x63, 0xfd, + 0x3c, 0xdd, 0xd5, 0x7a, 0xc6, 0xeb, 0x5d, 0xd9, + ])]), + hash: Default::default(), + hash_blacklist: Default::default(), + }, + features: Default::default(), + enclave_security_version: None, + enclave_product_id: None, + misc_select: MiscSelect::default(), + }; + + let steward = Config { + sgx: Some(sgx), + snp: Some(snp), + }; + + assert_eq!(config, steward); + } + + #[test] + fn test_sgx_signed_canned_csr() { + let csr = CertReq::from_der(ICELAKE_CSR).unwrap(); + let config: Config = toml::from_str(DEFAULT_CONFIG).expect("Couldn't deserialize"); + assert_sgx_config(&csr, &config.sgx.unwrap()).unwrap(); + } + + #[test] + fn test_sgx_signed_csr_bad_config_signer() { + let csr = CertReq::from_der(ICELAKE_CSR).unwrap(); + let config: Config = toml::from_str( + r#" + [sgx] + signer = ["2eba0f494f428e799c22d6f12778aebea4dc8d991f9e63fd3cddd57ac6eb5dd9"] + "#, + ) + .expect("Couldn't deserialize"); + + assert!(assert_sgx_config(&csr, &config.sgx.unwrap()).is_err()); + } + + #[test] + fn test_sgx_signed_csr_bad_config_enclave_version() { + let csr = CertReq::from_der(ICELAKE_CSR).unwrap(); + let config: Config = toml::from_str( + r#" + [sgx] + signer = ["c8dc9fe36caaeef871e6512c481092754c57c2ea999f128282ccb563d1602774"] + enclave_security_version = 9999 + "#, + ) + .expect("Couldn't deserialize"); + + assert!(assert_sgx_config(&csr, &config.sgx.unwrap()).is_err()); + } + + #[test] + fn test_snp_signed_canned_csr() { + let csr = CertReq::from_der(MILAN_CSR).unwrap(); + let config: Config = toml::from_str(DEFAULT_CONFIG).expect("Couldn't deserialize"); + assert!(assert_snp_config(&csr, &config.snp.unwrap()).is_ok()); + } + + #[test] + fn test_snp_signed_canned_csr_bad_author_key() { + let csr = CertReq::from_der(MILAN_CSR).unwrap(); + + let config: Config = toml::from_str( + r#" + [snp] + policy_flags = ["SingleSocket", "Debug"] + signer = ["e368c18e60842db9325778532dd81594d732078bf01aa91686be40333da639e08733b910bd057bdda50d715968b075ce"] + "#, + ) + .expect("Couldn't deserialize"); + + assert!(assert_snp_config(&csr, &config.snp.unwrap()).is_err()); + } + + #[test] + fn test_snp_signed_canned_csr_bad_abi_version() { + let csr = CertReq::from_der(MILAN_CSR).unwrap(); + + let config: Config = toml::from_str( + r#" + [snp] + signer = ["5b2181f5e2294fa0709d22b3f85d9d88b287b897c6b7289004802b53bbf09bc50f5469f98a6d6718d5f9c918d3d3c16f"] + abi = ">254.0" + "#, + ) + .expect("Couldn't deserialize"); + + assert!(assert_snp_config(&csr, &config.snp.unwrap()).is_err()); + } + + #[test] + fn test_snp_signed_canned_csr_blacklisted_id_key() { + let csr = CertReq::from_der(MILAN_CSR).unwrap(); + + let config: Config = toml::from_str(r#" + [snp] + signer = ["5b2181f5e2294fa0709d22b3f85d9d88b287b897c6b7289004802b53bbf09bc50f5469f98a6d6718d5f9c918d3d3c16f"] + id_key_digest_blacklist = ["966a25a22ee44283aa51bfb3682c990fd9e0a7457c5f60f4ac4eb5c41715478c4b206b0e01dc11aae8628f5aa29e0560"] + "#).expect("Couldn't deserialize"); + + assert!(assert_snp_config(&csr, &config.snp.unwrap()).is_err()); + } + } } diff --git a/steward.toml.example b/steward.toml.example new file mode 100644 index 00000000..340588d2 --- /dev/null +++ b/steward.toml.example @@ -0,0 +1,36 @@ +# The configuration is separated by technology, Intel's SGX and AMD's SNP. + +# For both, `signer`, `hash`, and `hash_blacklist` refer to Enarx running in a Keep. +# Signer is the public key which signed the Enarx binary. +# Hash is the hash of the Keep's memory with Enarx, but before a workload. This isn't the hash of the binary itself. +# Hash_blacklist is the hash of Enarx that is to be denied. +# At a minimum, one of these MUST be specified. +# All of these values are a list of the hashes. SGX uses SHA-256, SNP uses SHA-384. Hash lengths are enforced. + +[snp] +signer = [""] +hash = [""] +hash_blacklist = [""] + +# Additional SGX features which may be required. Missing are `INIT` and `MODE64BIT`, since they are required. Optional. +features = ["ProvisioningKey", "EInitKey", "KSS"] + +# Minimum Enclave security versions to accept, optional. +enclave_security_version = 0 + +# The required Enclave product ID to require, optional. +enclave_product_id = 0 + +[sgx] +signer = [""] +hash = [""] +hash_blacklist = [""] + +# The minimum abi version to require, optional. +abi = ">=1.51" + +# SNP policy flags to require, optional. +policy_flags = ["SMT"] + +# Platform Info flags to require, currently either SME or TSME. Optional. +platform_info_flags = "SME" \ No newline at end of file diff --git a/testdata/steward.toml b/testdata/steward.toml new file mode 100644 index 00000000..aaeb661f --- /dev/null +++ b/testdata/steward.toml @@ -0,0 +1,14 @@ +[sgx] +signer = ["c8dc9fe36caaeef871e6512c481092754c57c2ea999f128282ccb563d1602774"] +hash = ["e106565074be5ef3897711472617a0a000bae5c577f69a42202e3f76a07980f3"] +features = ["Debug", "ProvisioningKey", "EInitKey", "KSS"] +enclave_security_version = 0 +enclave_product_id = 0 + +[snp] +signer = ["5b2181f5e2294fa0709d22b3f85d9d88b287b897c6b7289004802b53bbf09bc50f5469f98a6d6718d5f9c918d3d3c16f"] +id_key_digest = ["966a25a22ee44283aa51bfb3682c990fd9e0a7457c5f60f4ac4eb5c41715478c4b206b0e01dc11aae8628f5aa29e0560"] +hash = ["6ff9ac4c61adc4cd2d86264230afc386358a5b41221dd236de92a3b45cb8a590bf719388994711ab9fe2b192bebc18a2"] +abi = ">=1.51" +policy_flags = ["SMT"] +platform_info_flags = "SME"