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