From 5f845d8a66a105d00ca36de94cc094a7d10a45b7 Mon Sep 17 00:00:00 2001 From: Tyler Fanelli Date: Sun, 15 Dec 2024 00:18:50 -0500 Subject: [PATCH] attest: Hash negotiation params, write evidence Given the negotiation parameters from the proxy, SVSM can now hash the parameters and embed them in the attestation evidence. It can also read the negotiation key specified and create key accordingly. As SEV-SNP is the only supported TEE architecture at the moment, there's also a few changes required to serialize a SEV-SNP attestation report into bytes. Co-developed-by: Stefano Garzarella Signed-off-by: Tyler Fanelli --- Cargo.lock | 46 ++++++++ Cargo.toml | 5 +- igvmmeasure/Cargo.toml | 2 +- kernel/Cargo.toml | 8 +- kernel/src/attest.rs | 202 +++++++++++++++++++++++++++++++++- kernel/src/greq/pld_report.rs | 18 +-- libaproxy/src/attestation.rs | 2 +- 7 files changed, 268 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b2209735..e2a612b12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -532,6 +532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", + "base64ct", "crypto-bigint", "digest", "ff", @@ -542,6 +543,8 @@ dependencies = [ "pkcs8", "rand_core", "sec1", + "serde_json", + "serdect", "subtle", "zeroize", ] @@ -1507,6 +1510,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + [[package]] name = "prettyplease_verus" version = "0.1.15" @@ -1559,6 +1571,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1574,6 +1596,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cc2191ec1fd850e3ede4cf09ccfd40a33df561111f73e96e1b7c3f9eee31328" +[[package]] +name = "rdrand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" +dependencies = [ + "rand_core", +] + [[package]] name = "reqwest" version = "0.12.9" @@ -1737,6 +1768,7 @@ dependencies = [ "der", "generic-array", "pkcs8", + "serdect", "subtle", "zeroize", ] @@ -1820,6 +1852,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1935,6 +1977,7 @@ name = "svsm" version = "0.1.0" dependencies = [ "aes-gcm", + "base64", "bitfield-struct 0.6.2", "bitflags 2.6.0", "bootlib", @@ -1950,7 +1993,10 @@ dependencies = [ "libaproxy", "libtcgtpm", "log", + "p384", "packit", + "rand_chacha", + "rdrand", "rustc_version", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 81eefaeb7..e2572f3ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ userinit = { path = "user/init" } # crates.io aes-gcm = { version = "0.10.3", default-features = false } arbitrary = "1.3.0" +base64 = { version = "0.22.1", default-features = false } bitfield-struct = "0.6.2" bitflags = "2.4" clap = { version = "4.4.14", default-features = false } @@ -59,7 +60,9 @@ intrusive-collections = "0.9.6" kbs-types = { version = "0.9.2", default-features = false } libfuzzer-sys = "0.4" log = "0.4.17" -p384 = { version = "0.13.0" } +p384 = { version = "0.13.0", default-features = false } +rand_chacha = { version = "0.3.1", default-features = false } +rdrand = { version = "0.8.3", default-features = false } serde = { version = "1.0.215", default-features = false } serde_json = { version = "1.0", default-features = false } sha2 = { version = "0.10.8", default-features = false } diff --git a/igvmmeasure/Cargo.toml b/igvmmeasure/Cargo.toml index 57005f259..d3ed5af2d 100644 --- a/igvmmeasure/Cargo.toml +++ b/igvmmeasure/Cargo.toml @@ -10,7 +10,7 @@ clap = { workspace = true, default-features = true, features = ["derive"] } sha2.workspace = true igvm.workspace = true igvm_defs.workspace = true -p384.workspace = true +p384 = { workspace = true, default-features = true } zerocopy.workspace = true # igvm_defs still uses 0.7, so we need to import the zerocopy 0.7 traits to use them. zerocopy07 = { package = "zerocopy", version = "0.7" } diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index b18dd63b1..8b45d72e7 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -26,6 +26,7 @@ elf.workspace = true syscall.workspace = true aes-gcm = { workspace = true, features = ["aes", "alloc"] } +base64 = { workspace = true, optional = true, features = ["alloc"] } bitfield-struct.workspace = true bitflags.workspace = true gdbstub = { workspace = true, optional = true } @@ -34,7 +35,10 @@ igvm_defs = { workspace = true, features = ["unstable"] } intrusive-collections.workspace = true kbs-types = { workspace = true, optional = true, features = ["alloc"] } log = { workspace = true, features = ["max_level_info", "release_max_level_info"] } +p384 = { workspace = true, optional = true, features = ["alloc", "arithmetic", "ecdh", "jwk"] } packit.workspace = true +rand_chacha = { workspace = true, optional = true } +rdrand = { workspace = true, optional = true } libtcgtpm = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["alloc", "derive"] } serde_json = { workspace = true, optional = true, features = ["alloc"] } @@ -53,7 +57,9 @@ verify_external = { workspace = true, optional = true} test.workspace = true [features] -attest = ["dep:kbs-types", "dep:libaproxy", "dep:serde", "dep:serde_json"] +attest = ["dep:base64", "dep:kbs-types", "dep:libaproxy", "dep:p384", + "dep:rand_chacha", "dep:rdrand", "dep:serde", "dep:serde_json", + "dep:sha2"] default = [] enable-gdb = ["dep:gdbstub", "dep:gdbstub_arch"] vtpm = ["dep:libtcgtpm"] diff --git a/kernel/src/attest.rs b/kernel/src/attest.rs index 385aded76..6a6dcb94f 100644 --- a/kernel/src/attest.rs +++ b/kernel/src/attest.rs @@ -9,13 +9,24 @@ extern crate alloc; use crate::{ error::SvsmError, + greq::{ + pld_report::{SnpReportRequest, SnpReportResponse}, + services::get_regular_report, + }, io::{Read, Write, DEFAULT_IO_DRIVER}, serial::SerialPort, }; use alloc::{string::ToString, vec, vec::Vec}; +use base64::prelude::*; +use core::fmt; use kbs_types::Tee; use libaproxy::*; +use p384::{ecdh, NistP384}; +use rand_chacha::{rand_core::SeedableRng, ChaChaRng}; +use rdrand::RdSeed; use serde::Serialize; +use sha2::{Digest, Sha384, Sha512}; +use zerocopy::{FromBytes, IntoBytes}; /// The attestation driver that communicates with the proxy via some communication channel (serial /// port, virtio-vsock, etc...). @@ -46,9 +57,9 @@ impl TryFrom for AttestationDriver<'_> { impl AttestationDriver<'_> { /// Attest SVSM's launch state by communicating with the attestation proxy. pub fn attest(&mut self) -> Result, SvsmError> { - let _negotiation = self.negotiation()?; + let negotiation = self.negotiation()?; - todo!(); + Ok(self.attestation(negotiation)?) } /// Send a negotiation request to the proxy. Proxy should reply with Negotiation parameters @@ -66,6 +77,27 @@ impl AttestationDriver<'_> { serde_json::from_slice(&payload).or(Err(AttestationError::NegotiationDeserialize)) } + /// Send an attestation request to the proxy. Proxy should reply with attestation response + /// containing the status (success/fail) and an optional secret returned from the server upon + /// successful attestation. + fn attestation( + &mut self, + negotiation: NegotiationResponse, + ) -> Result, AttestationError> { + // Generate TEE key and evidence for serialization to proxy. + let key = self.tee_key_generate(&negotiation)?; + let evidence = self.evidence(negotiation, &key)?; + + let request = AttestationRequest { + evidence: BASE64_URL_SAFE.encode(evidence), + key: AttestationKey::try_from(&key)?, + }; + + self.write(request)?; + + todo!(); + } + /// Read attestation data from the serial port. fn read(&mut self) -> Result, AttestationError> { let len = { @@ -100,19 +132,185 @@ impl AttestationDriver<'_> { Ok(()) } + + /// Generate the TEE attestation key. + fn tee_key_generate( + &self, + negotiation: &NegotiationResponse, + ) -> Result { + let key = match negotiation.key_type { + NegotiationKey::Ecdh384Sha256Aes128 => TeeKey::ec(384)?, + }; + + Ok(key) + } + + /// Hash negotiation parameters and fetch TEE evidence. + fn evidence( + &self, + negotiation: NegotiationResponse, + key: &TeeKey, + ) -> Result, AttestationError> { + let mut hash = match negotiation.hash { + NegotiationHash::SHA384 => self.hash(&negotiation, key, Sha384::new())?, + NegotiationHash::SHA512 => self.hash(&negotiation, key, Sha512::new())?, + }; + + let evidence = match self.tee { + Tee::Snp => { + // SEV-SNP REPORT_DATA is 64 bytes in size. If a SHA384 was selected in the + // negotiation parameters, that array is 48 bytes in size and must be padded. + hash.resize(64, 0); + + let mut user_data = [0u8; 64]; + user_data.copy_from_slice(&hash); + + let request = SnpReportRequest { + user_data, + vmpl: 0, + flags: 1, // Sign with VCEK. + rsvd: [0u8; 24], + }; + + let mut buf = request.as_bytes().to_vec(); + // The buffer currently contains the the SnpReportRequest structure. However, SVSM + // will fill this buffer in with the SnpReportResponse when fetching the report. + // Ensure the array is large enough to contain the response (which is much larger + // than the request, as it contains the attestation report). + buf.resize(2048, 0); + + let bytes = { + let len = + get_regular_report(&mut buf).or(Err(AttestationError::SnpGetReport))?; + + // We have the length of the response. The rest of the response is unused. + // Parse the SnpReportResponse from the slice of the buf containing the + // response (that is, &buf[0..len]). + let resp = SnpReportResponse::ref_from_bytes(&buf[..len]) + .or(Err(AttestationError::SnpGetReport))?; + + // Get the attestation report as bytes for serialization in the + // AttestationRequest. + resp.report().as_bytes().to_vec() + }; + + bytes + } + // We check for supported TEE architectures in the AttestationDriver's constructor. + _ => unreachable!(), + }; + + Ok(evidence) + } + + /// Hash the negotiation parameters from the attestation server for inclusion in the + /// attestation evidence. + fn hash( + &self, + n: &NegotiationResponse, + key: &TeeKey, + mut sha: impl Digest, + ) -> Result, AttestationError> { + for p in &n.params { + match p { + NegotiationParam::Base64StdBytes(s) => { + let decoded = BASE64_STANDARD + .decode(s) + .or(Err(AttestationError::NegotiationParamDecode))?; + + sha.update(decoded); + } + #[allow(irrefutable_let_patterns)] + NegotiationParam::EcPublicKeySec1Bytes => { + if let TeeKey::Ecdh384Sha256Aes128(ec) = key { + sha.update(ec.public_key().to_sec1_bytes()); + } else { + return Err(AttestationError::ProxyRead); + } + } + } + } + + Ok(sha.finalize().to_vec()) + } +} + +/// TEE key used to decrypt secrets sent from the attestation server. +pub enum TeeKey { + Ecdh384Sha256Aes128(ecdh::EphemeralSecret), +} + +impl TeeKey { + /// Generate an Elliptic Curve key as the TEE key. + fn ec(curve: usize) -> Result { + let mut rdseed = RdSeed::new().or(Err(AttestationError::TeeKeyGenerate))?; + let mut rng = ChaChaRng::from_rng(&mut rdseed).or(Err(AttestationError::TeeKeyGenerate))?; + + let key = match curve { + 384 => ecdh::EphemeralSecret::random(&mut rng), + _ => unreachable!(), + }; + + Ok(Self::Ecdh384Sha256Aes128(key)) + } +} + +impl fmt::Debug for TeeKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + Self::Ecdh384Sha256Aes128(_) => write!(f, "EC384"), + } + } +} + +impl TryFrom<&TeeKey> for AttestationKey { + type Error = AttestationError; + + fn try_from(key: &TeeKey) -> Result { + match key { + TeeKey::Ecdh384Sha256Aes128(k) => { + let jwk = k.public_key().to_jwk(); + let crv = jwk.crv().to_string(); + let epoint = jwk + .to_encoded_point::() + .or(Err(AttestationError::TeeKeyEncode))?; + + let x = epoint.x().ok_or(AttestationError::TeeKeyEncode)?; + let y = epoint.y().ok_or(AttestationError::TeeKeyEncode)?; + + Ok(AttestationKey::EC { + crv, + x_b64url: BASE64_URL_SAFE.encode(x), + y_b64url: BASE64_URL_SAFE.encode(y), + }) + } + } + } } /// Possible errors when attesting TEE evidence. #[derive(Clone, Copy, Debug)] pub enum AttestationError { + /// Error deserializing the attestation response from JSON bytes. + AttestationDeserialize, + /// Unsuccessful attestation. + Failed, /// Error deserializing the negotiation response from JSON bytes. NegotiationDeserialize, + /// Error decoding a negotiation parameter. + NegotiationParamDecode, /// Error serializing the negotiation request to JSON bytes. NegotiationSerialize, /// Error reading from the attestation proxy transport channel. ProxyRead, /// Error writing over the attestation proxy transport channel. ProxyWrite, + /// Error fetching the SEV-SNP attestation report. + SnpGetReport, + /// Error encoding the TEE public key to JSON. + TeeKeyEncode, + /// Error generating the TEE key. + TeeKeyGenerate, /// Unsupported TEE architecture. UnsupportedTee, } diff --git a/kernel/src/greq/pld_report.rs b/kernel/src/greq/pld_report.rs index 967f625e3..fa9d3a541 100644 --- a/kernel/src/greq/pld_report.rs +++ b/kernel/src/greq/pld_report.rs @@ -8,7 +8,7 @@ use core::mem::size_of; -use zerocopy::{FromBytes, Immutable, KnownLayout}; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use crate::protocols::errors::SvsmReqError; @@ -17,22 +17,22 @@ pub const USER_DATA_SIZE: usize = 64; /// MSG_REPORT_REQ payload format (AMD SEV-SNP spec. table 20) #[repr(C, packed)] -#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable)] +#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable, IntoBytes)] pub struct SnpReportRequest { /// Guest-provided data to be included in the attestation report /// REPORT_DATA (512 bits) - user_data: [u8; USER_DATA_SIZE], + pub user_data: [u8; USER_DATA_SIZE], /// The VMPL to put in the attestation report - vmpl: u32, + pub vmpl: u32, /// 31:2 - Reserved /// 1:0 - KEY_SEL. Selects which key to use for derivation /// 0: If VLEK is installed, sign with VLEK. Otherwise, sign with VCEK /// 1: Sign with VCEK /// 2: Sign with VLEK /// 3: Reserved - flags: u32, + pub flags: u32, /// Reserved, must be zero - rsvd: [u8; 24], + pub rsvd: [u8; 24], } impl SnpReportRequest { @@ -105,7 +105,7 @@ impl SnpReportResponse { /// component in the trusted computing base (TCB) of the SNP firmware. /// (AMD SEV-SNP spec. table 3) #[repr(C, packed)] -#[derive(Clone, Copy, Debug, FromBytes, Immutable)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] struct TcbVersion { /// Version of the Microcode, SNP firmware, PSP and boot loader raw: u64, @@ -113,7 +113,7 @@ struct TcbVersion { /// Format for an ECDSA P-384 with SHA-384 signature (AMD SEV-SNP spec. table 115) #[repr(C, packed)] -#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable)] +#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable, IntoBytes)] struct Signature { /// R component of this signature r: [u8; 72], @@ -125,7 +125,7 @@ struct Signature { /// ATTESTATION_REPORT format (AMD SEV-SNP spec. table 21) #[repr(C, packed)] -#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable)] +#[derive(Clone, Copy, Debug, FromBytes, KnownLayout, Immutable, IntoBytes)] pub struct AttestationReport { /// Version number of this attestation report version: u32, diff --git a/libaproxy/src/attestation.rs b/libaproxy/src/attestation.rs index 6d68a6a11..5a884da05 100644 --- a/libaproxy/src/attestation.rs +++ b/libaproxy/src/attestation.rs @@ -6,7 +6,7 @@ // Author: Tyler Fanelli extern crate alloc; -use alloc::string::String; +use alloc::{string::String, vec::Vec}; use serde::{Deserialize, Serialize}; /// The format of the public key that is used to encrypt secrets sent to SVSM upon successful