Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure detection + attestation report fetch #16

Merged
merged 9 commits into from
Aug 8, 2023
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ hex = "0.4"
x509-parser = { version="^0.14", features=["verify"] }
asn1-rs = "*"
rand = "*"
tss-esapi = "7.2"
5 changes: 4 additions & 1 deletion docs/snpguest.1.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ GLOBAL OPTIONS
COMMANDS
--------
*snpguest report*::
usage: snpguest report $ATT_REPORT_PATH $REQUEST_FILE [-v, --vmpl] $VMPL [-r, --random]
usage: snpguest report $ATT_REPORT_PATH $REQUEST_FILE [-v, --vmpl] $VMPL [-r, --random] [-p, --platform]

Requests an attestation report from the host and writes it in a file with the provided request data and vmpl.
Will write the contents of the attestation report in binary format into the specified report path.
A path for the attestation report must be provided.
User can pass 64 bytes of data in any file format into $REQUEST_FILE in order to use that data to request the attestation report.
The user can use the --random flag to generate and use random data for request data.
For Microsoft Hyper-V guests, a user can use the --platform flag to use the request data that was pre-generated
from the platform. Currently, for Hyper-V guests, --platform is required, as there is no ability to write
request data for the attestation report.
If the user uses the --random flag, then the data will be written into the file they provided in $REQUEST_FILE.
VMPL is an optional parameter and it defaults to 1.

Expand Down
7 changes: 3 additions & 4 deletions src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ mod report_display {

#[derive(StructOpt)]
pub struct Args {
#[structopt(
help = "Path to attestation report to display."
)]
#[structopt(help = "Path to attestation report to display.")]
pub att_report_path: PathBuf,
}

// Print attestation report in console
pub fn display_attestation_report(args: Args, quiet: bool) -> Result<()> {
let att_report = report::read_report(args.att_report_path).context("Could not open attestation report")?;
let att_report = report::read_report(args.att_report_path)
.context("Could not open attestation report")?;

if !quiet {
println!("{}", att_report);
Expand Down
116 changes: 116 additions & 0 deletions src/hyperv/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: Apache-2.0

use super::*;

use std::arch::x86_64::__cpuid;

const CPUID_GET_HIGHEST_FUNCTION: u32 = 0x80000000;
const CPUID_PROCESSOR_INFO_AND_FEATURE_BITS: u32 = 0x1;

const CPUID_FEATURE_HYPERVISOR: u32 = 1 << 31;

const CPUID_HYPERV_SIG: &str = "Microsoft Hv";
const CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS: u32 = 0x40000000;
const CPUID_HYPERV_FEATURES: u32 = 0x40000003;
const CPUID_HYPERV_MIN: u32 = 0x40000005;
const CPUID_HYPERV_MAX: u32 = 0x4000ffff;
const CPUID_HYPERV_ISOLATION: u32 = 1 << 22;
const CPUID_HYPERV_CPU_MANAGEMENT: u32 = 1 << 12;
const CPUID_HYPERV_ISOLATION_CONFIG: u32 = 0x4000000C;
const CPUID_HYPERV_ISOLATION_TYPE_MASK: u32 = 0xf;
const CPUID_HYPERV_ISOLATION_TYPE_SNP: u32 = 2;

pub fn present() -> bool {
let mut cpuid = unsafe { __cpuid(CPUID_PROCESSOR_INFO_AND_FEATURE_BITS) };
if (cpuid.ecx & CPUID_FEATURE_HYPERVISOR) == 0 {
return false;
}

cpuid = unsafe { __cpuid(CPUID_GET_HIGHEST_FUNCTION) };
if cpuid.eax < CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS {
return false;
}

cpuid = unsafe { __cpuid(CPUID_HYPERV_VENDOR_AND_MAX_FUNCTIONS) };
if cpuid.eax < CPUID_HYPERV_MIN || cpuid.eax > CPUID_HYPERV_MAX {
return false;
}

let mut sig: Vec<u8> = vec![];
sig.append(&mut cpuid.ebx.to_le_bytes().to_vec());
sig.append(&mut cpuid.ecx.to_le_bytes().to_vec());
sig.append(&mut cpuid.edx.to_le_bytes().to_vec());

if sig != CPUID_HYPERV_SIG.as_bytes() {
return false;
}

cpuid = unsafe { __cpuid(CPUID_HYPERV_FEATURES) };

let isolated: bool = (cpuid.ebx & CPUID_HYPERV_ISOLATION) != 0;
let managed: bool = (cpuid.ebx & CPUID_HYPERV_CPU_MANAGEMENT) != 0;

if !isolated || managed {
return false;
}

cpuid = unsafe { __cpuid(CPUID_HYPERV_ISOLATION_CONFIG) };
let mask = cpuid.ebx & CPUID_HYPERV_ISOLATION_TYPE_MASK;
let snp = CPUID_HYPERV_ISOLATION_TYPE_SNP;

if mask != snp {
return false;
}

true
}

pub mod report {
use super::*;

use anyhow::{anyhow, Context};
use serde::{Deserialize, Serialize};
use sev::firmware::guest::AttestationReport;
use tss_esapi::{
abstraction::nv,
handles::NvIndexTpmHandle,
interface_types::{resource_handles::NvAuth, session_handles::AuthSession},
tcti_ldr::{DeviceConfig, TctiNameConf},
};

const VTPM_HCL_REPORT_NV_INDEX: u32 = 0x01400001;

#[repr(C)]
#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
struct Hcl {
rsv1: [u32; 8],
report: AttestationReport,
rsv2: [u32; 5],
}

pub fn get(vmpl: u32) -> Result<AttestationReport> {
if vmpl > 0 {
return Err(anyhow!("Azure vTPM attestation report requires VMPL 0"));
}
let bytes = tpm2_read().context("unable to read attestation report bytes from vTPM")?;

hcl_report(&bytes)
}

fn tpm2_read() -> Result<Vec<u8>> {
let handle = NvIndexTpmHandle::new(VTPM_HCL_REPORT_NV_INDEX)
.context("unable to initialize TPM handle")?;
let mut ctx = tss_esapi::Context::new(TctiNameConf::Device(DeviceConfig::default()))?;
ctx.set_sessions((Some(AuthSession::Password), None, None));

nv::read_full(&mut ctx, NvAuth::Owner, handle)
.context("unable to read non-volatile vTPM data")
}

fn hcl_report(bytes: &[u8]) -> Result<AttestationReport> {
let hcl: Hcl =
bincode::deserialize(bytes).context("unable to deserialize bytes from vTPM")?;

Ok(hcl.report)
}
}
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ mod fetch;
mod report;
mod verify;

mod hyperv;

use certs::CertificatesArgs;
use display::DisplayCmd;
use fetch::FetchCmd;
Expand Down Expand Up @@ -51,9 +53,10 @@ fn main() -> Result<()> {
env_logger::init();

let snpguest = SnpGuest::from_args();
let hv = hyperv::present();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to check for Hyper-V every single time the tool is used? Why wouldn't we just check whenever people request a report?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be parity with other commands.

Copy link
Contributor

@larrydewey larrydewey Jul 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I think it would make more sense to only check for Hyper-V when we are potentially going to interact with the PSP. All other functionality from the tool does not rely on architecture. The only other locations I could potentially think this check would be useful is for enabling/disabling --random and/or extended certs, as Hyper-V disallows the use of these flags

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example I currently see is certs. In non-Azure, certs are fetched from the KDS. However, in Azure they are fetched from another MS-defined endpoint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense


let status = match snpguest.cmd {
SnpGuestCmd::Report(args) => report::get_report(args),
SnpGuestCmd::Report(args) => report::get_report(args, hv),
SnpGuestCmd::Certificates(args) => certs::get_ext_certs(args),
tylerfanelli marked this conversation as resolved.
Show resolved Hide resolved
SnpGuestCmd::Fetch(subcmd) => fetch::cmd(subcmd),
SnpGuestCmd::Verify(subcmd) => verify::cmd(subcmd, snpguest.quiet),
Expand Down
172 changes: 105 additions & 67 deletions src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
use super::*;

use std::{
fs,
fs::File,
io::{BufWriter, Read, Write},
fs::{self, File, OpenOptions},
io::{Read, Write},
path::PathBuf,
};

use anyhow::{anyhow, Result};
use rand::{thread_rng, RngCore};

use sev::firmware::guest::{AttestationReport, Firmware};

// Read a bin-formatted attestation report.
Expand All @@ -27,23 +26,7 @@ pub fn read_report(att_report_path: PathBuf) -> Result<AttestationReport, anyhow
pub fn create_random_request() -> [u8; 64] {
let mut data = [0u8; 64];
thread_rng().fill_bytes(&mut data);
data
}

// Write data into given file. Split it into 16 byte lines.
pub fn write_hex<W: Write>(file: &mut BufWriter<W>, data: &[u8]) -> Result<()> {
let mut line_counter = 0;
for val in data {
// Make it blocks for easier read
if line_counter.eq(&16) {
writeln!(file).context("Failed to write data to file")?;
line_counter = 0;
}
// Write byte into file
write!(file, "{:02x}", val).context("Failed to write data to file")?;
line_counter += 1;
}
Ok(())
data
}

#[derive(StructOpt)]
Expand All @@ -69,59 +52,114 @@ pub struct ReportArgs {
help = "Provide file with data for attestation-report request. If provided with random flag, then the random data will be written in the provided path."
)]
pub request_file: PathBuf,

#[structopt(
long,
short,
help = "Expect that the 64-byte report data will already be provided by the platform provider."
)]
pub platform: bool,
}

impl ReportArgs {
pub fn verify(&self, hyperv: bool) -> Result<()> {
if self.random && self.platform {
return Err(anyhow!(
"--random and --platform both enabled (not allowed). Consult man page."
));
}

if self.random && hyperv {
return Err(anyhow!(
"--random enabled yet Hyper-V guest detected (not allowed). Consult man page."
));
}

if self.platform && !hyperv {
return Err(anyhow!("--platform enabled yet Hyper-V guest not detected (not allowed). Consult man page."));
}

Ok(())
}
}

// Request attestation report and write it into a file
pub fn get_report(args: ReportArgs) -> Result<()> {
let mut sev_fw: Firmware = Firmware::open().context("failed to open SEV firmware device.")?;

let request_data = match args.random {
true => {
let request_buf = create_random_request();

// Overwrite data if file already exists
let request_file = if args.request_file.exists() {
std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(args.request_file)
.context("Unable to overwrite request file contents")?
} else {
fs::File::create(args.request_file).context("Unable to create request file.")?
};
write_hex(&mut BufWriter::new(request_file), &request_buf)
.context("Failed to write request data in request file")?;
request_buf
},
false => {
let mut request_file =
File::open(args.request_file).context("Could not open the report request file.")?;
let mut request_buf: [u8; 64] = [0; 64];
request_file
.read(&mut request_buf)
.context("Could not read report request file.")?;
request_buf
}
pub fn get_report(args: ReportArgs, hv: bool) -> Result<()> {
args.verify(hv)?;

let data: Option<[u8; 64]> = if args.random {
Some(create_random_request())
} else if args.platform {
None
} else {
/*
* Read from the request file.
*/
let mut bytes = [0u8; 64];
let mut file = File::open(&args.request_file)?;
file.read_exact(&mut bytes)
.context("unable to read 64 bytes from REQUEST_FILE")?;

Some(bytes)
};

// Get attestation report
let att_report = sev_fw
.get_report(None, Some(request_data), args.vmpl)
.context("Failed to get report.")?;

// Write attestation report into desired file
let mut attestation_file = if args.att_report_path.exists() {
std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(args.att_report_path)
.context("Unable to overwrite attestation report file contents")?
let report = if hv {
hyperv::report::get(args.vmpl.unwrap_or(0))?
} else {
fs::File::create(args.att_report_path)
.context("Unable to create attestation report file contents")?
let mut fw = Firmware::open().context("unable to open /dev/sev")?;
fw.get_report(None, data, args.vmpl)
.context("unable to fetch attestation report")?
};
bincode::serialize_into(&mut attestation_file, &att_report)
.context("Could not serialize attestation report into file.")?;

/*
* Serialize and write attestation report.
*/
let mut file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&args.att_report_path)?;

write!(&mut file, "{}", report).context(format!(
"unable to write attestation report to {}",
args.att_report_path.display()
))?;

/*
* Write reports report data (only for --random or --platform).
*/
if args.random {
reqdata_write(args.request_file, &report).context("unable to write random request data")?;
} else if args.platform {
reqdata_write(args.request_file, &report)
.context("unable to write platform request data")?;
}

Ok(())
}

fn reqdata_write(name: PathBuf, report: &AttestationReport) -> Result<()> {
let mut file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(name)
.context("unable to create or write to request data file")?;

write_hex(&mut file, &report.report_data).context("unable to write report data to REQUEST_FILE")
}

pub fn write_hex(file: &mut File, data: &[u8]) -> Result<()> {
let mut line_counter = 0;
for val in data {
// Make it blocks for easier read
if line_counter.eq(&16) {
writeln!(file)?;
line_counter = 0;
}

write!(file, "{:02x}", val)?;
line_counter += 1;
}
Ok(())
}
2 changes: 1 addition & 1 deletion src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ mod certificate_chain {
pub fn validate_cc(args: Args, quiet: bool) -> Result<()> {
let ark_path = find_cert_in_dir(args.certs_dir.clone(), "ark")?;
let ask_path = find_cert_in_dir(args.certs_dir.clone(), "ask")?;
let vcek_path = find_cert_in_dir(args.certs_dir.clone(), "vcek")?;
let vcek_path = find_cert_in_dir(args.certs_dir, "vcek")?;

// Get a cert chain from directory
let cert_chain: Chain = CertPaths {
Expand Down