diff --git a/Cargo.lock b/Cargo.lock index 5242788..f0e0f9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "attest-time" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "clap-verbosity", + "ctrlc", + "dice-verifier", + "env_logger", + "log", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -234,11 +247,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -259,6 +272,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.15.4" @@ -286,6 +308,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -354,6 +382,17 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-verbosity" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7bf75a8e0407a558bd7e8e7919baa352e21fb0c1c7702a63c853f2277c4c63" +dependencies = [ + "clap", + "log", + "serde", +] + [[package]] name = "clap_builder" version = "4.5.51" @@ -457,6 +496,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +dependencies = [ + "dispatch2", + "nix 0.30.1", + "windows-sys 0.61.2", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -661,6 +711,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1084,9 +1146,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.170" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libipcc" @@ -1218,6 +1280,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1233,6 +1307,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "object" version = "0.36.7" @@ -1466,7 +1555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.10.0", "serde", "serde_derive", ] @@ -1523,7 +1612,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -1690,7 +1779,7 @@ dependencies = [ "cfg-if", "libudev", "mach2", - "nix", + "nix 0.24.3", "regex", "winapi", ] @@ -1890,7 +1979,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2228,6 +2317,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2373,7 +2471,7 @@ version = "0.42.0" source = "git+https://github.com/oxidecomputer/yubihsm.rs?branch=v0.42.0-with-audit#ab1d0ac182ae949567d988ddb2fc168ea45e4556" dependencies = [ "aes", - "bitflags 2.4.2", + "bitflags 2.10.0", "cbc", "cmac", "ecdsa", diff --git a/Cargo.toml b/Cargo.toml index 05bbcf6..ff6a2bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "attest-data", "attest-mock", + "attest-time", "barcode", "dice-cert-tmpl", "dice-mfg", @@ -17,8 +18,10 @@ anyhow = { version = "1.0.100", default-features = false } attest.path = "attest" chrono = { version = "0.4.42", default-features=false } clap = { version = "4.5.51", features = ["derive", "env"] } +clap-verbosity = "2.1.0" const-oid = { version = "0.9.6", default-features = false } corncobs = "0.1" +ctrlc = "3.5.1" der = { version = "0.7.10", default-features = false } ecdsa = { version = "0.16", default-features = false } ed25519-dalek = { version = "2.1", default-features = false } diff --git a/attest-time/Cargo.toml b/attest-time/Cargo.toml new file mode 100644 index 0000000..e3905c2 --- /dev/null +++ b/attest-time/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "attest-time" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { workspace = true, features = ["std"] } +clap.workspace = true +clap-verbosity.workspace = true +ctrlc.workspace = true +dice-verifier = { path = "../verifier" } +env_logger.workspace = true +log.workspace = true + +[features] +ipcc = ["dice-verifier/ipcc"] diff --git a/attest-time/README.md b/attest-time/README.md new file mode 100644 index 0000000..37b28f3 --- /dev/null +++ b/attest-time/README.md @@ -0,0 +1,5 @@ +To get meaningful results this tool should be run through the IPCC interface as +this is the communication path that will be used on a deployed system. We do +not however enable IPCC by default due to the side effects caused by cargo +unifying features across the workspace. IPCC support must be explicitly enabled +for `attest-time` when building it: `--features ipcc`. diff --git a/attest-time/build.rs b/attest-time/build.rs new file mode 100644 index 0000000..00f77a6 --- /dev/null +++ b/attest-time/build.rs @@ -0,0 +1,16 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +/// This path is where Oxide specific libraries live on helios systems. +/// This is needed for linking with libipcc +#[cfg(all(feature = "ipcc", target_os = "illumos"))] +static OXIDE_PLATFORM: &str = "/usr/platform/oxide/lib/amd64/"; + +fn main() { + #[cfg(all(feature = "ipcc", target_os = "illumos"))] + { + println!("cargo:rustc-link-arg=-Wl,-R{}", OXIDE_PLATFORM); + println!("cargo:rustc-link-search={}", OXIDE_PLATFORM); + } +} diff --git a/attest-time/src/bin/m3vs.rs b/attest-time/src/bin/m3vs.rs new file mode 100644 index 0000000..8162d7c --- /dev/null +++ b/attest-time/src/bin/m3vs.rs @@ -0,0 +1,179 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// This code implements Welford's method for calculating mean and variance +// from streaming data as described here: +// https://jonisalonen.com/2013/deriving-welfords-method-for-computing-variance/ +// The TLDR is: +// variance(samples): +// M := 0 +// S := 0 +// for k from 1 to N: +// x := samples[k] +// oldM := M +// M := M + (x-M)/k +// S := S + (x-M)*(x-oldM) +// return S/(N-1) +// +// where: +// - M is the mean +// - S is the squared distance from the mean + +use anyhow::{Context, Result, anyhow}; +use clap::Parser; +use std::{ + fs::File, + io::{self, BufRead, BufReader, Read}, + path::PathBuf, +}; + +/// Read input samples formatted as a single sample per line. Each sample is +/// an positive integer that can fit in a u32. This is the same format emitted +/// by `attest-time`. +#[derive(Debug, Parser)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// An optional input file. If omitted stdin will be read. + input: Option, +} + +/// This strcture holds data used to generate some measures of central +/// tendency and dispersion +struct Data { + /// `count` is a float because it's mostly used as the denominator in the + /// mean and variance calculation + count: u32, + /// collection used to collect input data + /// TODO: we can get rid of this once we're confident in our impl of the + /// Welford's algorithms + durations: Vec, + /// `max` is the running max for the dataset + max: u32, + /// `mean` holds the running mean calculated w/ Welford's method + mean: f64, + /// the running min for the dataset + min: u32, + /// `distance_2` is the running squared distance from the mean calculated + /// w/ Welford's method + distance_2: f64, +} + +/// impl `Default` manually to set initial value for `min` +impl Default for Data { + fn default() -> Self { + Self { + count: 0, + distance_2: 0.0, + max: 0, + mean: 0.0, + min: u32::MAX, + durations: Vec::new(), + } + } +} + +/// my naive and expensive mean calculation +fn mean(durations: &Vec, count: u32) -> Result { + let mut total: u128 = 0; + for v in durations { + // data in the durations collection is u32, this conversion is safe + total += *v as u128; + } + + let mean = total / count as u128; + u32::try_from(mean).context("mean u128 to u32") +} + +/// my naive and expensive variance calculation +fn variance(durations: &Vec, count: u32, mean: u32) -> Result { + // accumulate sum of the squared difference between each sample and the mean + // this is the numerator in the classic variance equation + let mut variance: u128 = 0; + for v in durations { + let diff = *v as i128 - mean as i128; + let square = i128::pow(diff, 2); + variance += square as u128; + } + + let variance = variance / count as u128 - 1; + Ok(variance as u32) +} + +fn main() -> Result<()> { + let args = Args::parse(); + + // if args.input file not provided use stdin + let reader: Box = match args.input { + Some(i) => Box::new( + File::open(&i) + .with_context(|| format!("open file: {}", &i.display()))?, + ), + None => Box::new(io::stdin()), + }; + let reader = BufReader::new(reader); + + let mut data = Data::default(); + + for line in reader.lines() { + let line = line.context("read line")?; + let words: Vec<&str> = line.split_whitespace().collect(); + if words.len() != 1 { + return Err(anyhow!("malformed line")); + } + + let micros: u32 = words[0].parse().context("parse u32 from str")?; + + if micros < data.min { + data.min = micros; + } + + if micros > data.max { + data.max = micros; + } + + data.count = data + .count + .checked_add(1) + .ok_or(anyhow!("too many samples: count overflow"))?; + + data.durations.push(micros); + + let micros = f64::from(micros); + let old_mean = data.mean; + data.mean = data.mean + (micros - data.mean) / (data.count as f64); + data.distance_2 += (micros - data.mean) * (micros - old_mean); + } + + println!("sample count: {}", data.count); + println!("min: {}", data.min); + println!("max: {}", data.max); + + // streaming mean, variance, & standard deviation + { + println!("welford's mean: {}", data.mean); + + // final calculation from welford's method for variance + // return S/(n-1) + let variance = data.distance_2 / f64::from(data.count - 1); + println!("welford's variance: {variance}"); + + println!("welford's standard deviation: {}", variance.sqrt()); + } + + // classic mean, variance, & standard deviation + { + let mean = mean(&data.durations, data.count) + .context("calculate mean from dataset")?; + println!("mean: {mean}"); + + let variance = variance(&data.durations, data.count, mean) + .context("calculate variance from dataset")?; + println!("variance: {variance}"); + + let sqrt = f64::from(variance).sqrt(); + println!("standard deviation: {sqrt}"); + } + + Ok(()) +} diff --git a/attest-time/src/bin/sds-from-mean.rs b/attest-time/src/bin/sds-from-mean.rs new file mode 100644 index 0000000..c1245bb --- /dev/null +++ b/attest-time/src/bin/sds-from-mean.rs @@ -0,0 +1,107 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{Context, Result}; +use clap::Parser; +use clap_verbosity::{InfoLevel, Verbosity}; +use log::debug; + +use std::{ + collections::BTreeMap, + convert::TryFrom, + fs::File, + io::{self, BufRead, BufReader, Read}, + path::PathBuf, +}; + +/// For each sample read from the provided file / `stdin`, calculate it's +/// distance from the mean in units of the standard deviation. It then +/// categorizes each sample into a z-score band and reports the number +/// of samples that fall into each band to `stdout`. +// Ex: +// 0 10 +// 1 6 +// 3 9 +// +// The numbers in the first column identify the band where: +// +// - `0` is for samples that fall within 1 standard deviation of the mean +// - `1` '' between 1 and 2 '' +// - `3` '' between 3 and 4 '' +// +// The numbers in the second column is the number of samples within each band +// +// Samples are read from the provided file or `stdin`. +// Samples must be formatted as one u32 as base10 string per line. +#[derive(Debug, Parser)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Dump debug output + #[command(flatten)] + verbose: Verbosity, + + /// Calculate distance from this mean. + #[clap(long)] + mean: u32, + + /// Stepping for distance calculation. + #[clap(long)] + std_dev: u32, + + /// Path to file holding samples (or stdin if omitted). + #[clap(long)] + input: Option, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + // if args.input file not provided use stdin + let reader: Box = match args.input { + Some(i) => Box::new( + File::open(&i) + .with_context(|| format!("open file: {}", &i.display()))?, + ), + None => Box::new(io::stdin()), + }; + let reader = BufReader::new(reader); + + env_logger::Builder::new() + .filter_level(args.verbose.log_level_filter()) + .init(); + + // we only accept the `mean` input by the user as a u32 but we do signed + // arithmetic with it so this conversion is necessary + let mean = i32::try_from(args.mean).context("mean to i32")?; + + // same for the `std-dev` but there's no checked conversion: + // https://internals.rust-lang.org/t/tryfrom-for-f64/9793 + let std_dev = args.std_dev as f32; + + let mut std_dev_map = BTreeMap::new(); + + for line in reader.lines() { + let line = line.context("read line")?; + let sample: i32 = line.parse().context("parse u32 from str")?; + + let diff_abs = (mean - sample).abs(); + let std_devs = (diff_abs as f32 / std_dev).trunc() as u32; + + debug!( + "{sample} is {std_devs} std devs ({std_dev}) from the mean ({mean})" + ); + + if let Some(val) = std_dev_map.get_mut(&std_devs) { + *val += 1; + } else { + std_dev_map.insert(std_devs, 1); + } + } + + for (std_devs, count) in std_dev_map.iter() { + println!("{std_devs} {count}"); + } + + Ok(()) +} diff --git a/attest-time/src/main.rs b/attest-time/src/main.rs new file mode 100644 index 0000000..bd44f63 --- /dev/null +++ b/attest-time/src/main.rs @@ -0,0 +1,166 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{Context, Result}; +use clap::{Parser, ValueEnum}; +#[cfg(feature = "ipcc")] +use dice_verifier::ipcc::AttestIpcc; +use dice_verifier::{ + Attest, Nonce, + hiffy::{AttestHiffy, AttestTask}, +}; +use std::{ + fmt, + io::{self, Write}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::SystemTime, +}; + +#[derive(Clone, Debug, ValueEnum)] +enum Interface { + #[cfg(feature = "ipcc")] + Ipcc, + Hiffy, +} + +#[derive(Clone, Debug, ValueEnum)] +enum Unit { + Milli, + Micro, + Nano, +} + +impl fmt::Display for Unit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Unit::Milli => write!(f, "ms"), + Unit::Micro => write!(f, "µs"), + Unit::Nano => write!(f, "ns"), + } + } +} + +#[derive(Clone, Debug, ValueEnum)] +enum Commands { + Attest, + GetCertChains, + GetMeasurementLogs, +} + +#[derive(Debug, Parser)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Command from the vm_attest_trait::AttestMock to sample. + #[clap(value_enum, long, default_value_t = Commands::Attest)] + command: Commands, + + /// Number of samples to collect. If `None` then collect samples until + /// canceled. + #[clap(long)] + count: Option, + + /// Interface used for communication with the Attest task. + #[cfg_attr(feature = "ipcc", clap(value_enum, long, default_value_t = Interface::Ipcc))] + #[cfg_attr(not(feature = "ipcc"), clap(value_enum, long, default_value_t = Interface::Hiffy))] + interface: Interface, + + /// Unit of time used for each sample + #[clap(value_enum, long, default_value_t = Unit::Nano)] + units: Unit, +} + +/// This program gets an attestation through the mock VM attestation API. We +/// get a timestamp from the system before and after. The caller can use this +/// to roughtly determine the performance characteristics of this API / the +/// underlying machinery. +fn main() -> Result<()> { + let args = Args::parse(); + + // set to `false` when terminated w/ Ctrl-C + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .context("set Ctrl-C handler")?; + + // use stdout & `writeln!` so we can handle errors that would panic + // `println!` + let mut stdout = io::stdout().lock(); + + let attest: Box = match args.interface { + #[cfg(feature = "ipcc")] + Interface::Ipcc => { + Box::new(AttestIpcc::new().context("create OxAttestIpcc")?) + } + Interface::Hiffy => Box::new(AttestHiffy::new(AttestTask::Rot)), + }; + + // we do not care about the nonce, all 0's will require the same amount of + // work from the underlying impl + let nonce = Nonce { 0: [0u8; 32] }; + + // time calls to `VmInstanceAttestMock::attest`, output duration in µs + let mut count: usize = 0; + while running.load(Ordering::SeqCst) { + let time = SystemTime::now(); + + match args.command { + Commands::Attest => { + let _ = attest + .attest(&nonce) + .context("get attestation from Attest impl")?; + } + Commands::GetCertChains => { + let _ = attest + .get_certificates() + .context("get cert chains from Attest impl")?; + } + Commands::GetMeasurementLogs => { + let _ = attest + .get_measurement_log() + .context("get measurement logs from Attest impl")?; + } + } + + let elapsed = time + .elapsed() + .context("get elapsed time after attestation")?; + + let duration = match args.units { + Unit::Milli => elapsed.as_millis(), + Unit::Micro => elapsed.as_micros(), + Unit::Nano => elapsed.as_nanos(), + }; + + // `writeln` returns `BrokenPipe` if we pipe output to another process + // and it closes its stdin (usually Ctrl-C). In this case we suppress + // the error and exit quietly + match writeln!(stdout, "{duration}") { + Ok(_) => stdout.flush().context("flush stdout")?, + Err(e) => { + if e.kind() != std::io::ErrorKind::BrokenPipe { + running.store(false, Ordering::SeqCst); + eprintln!("stdout closed"); + } + } + } + + // break the loop if the caller has provided a `--count` & we've + // reached it + count = count.checked_add(1).context("add new 1 to count")?; + if let Some(max_count) = args.count + && max_count <= count + { + break; + } + } + + stdout.flush().context("flush stdout")?; + + Ok(()) +}