From fd05e1e1f4f61deefb00acbfad091f4afdf9cb5f Mon Sep 17 00:00:00 2001 From: Philip Tricca Date: Fri, 28 Jul 2023 18:24:48 -0700 Subject: [PATCH] Add `yhsm-audit` for working with yubihsm2 audit log objects & encodings. --- Cargo.lock | 184 +++++++++++++++++++++++++++++++- Cargo.toml | 14 +++ yhsm-audit/Cargo.toml | 19 ++++ yhsm-audit/src/lib.rs | 37 +++++++ yhsm-audit/src/main.rs | 231 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 yhsm-audit/Cargo.toml create mode 100644 yhsm-audit/src/lib.rs create mode 100644 yhsm-audit/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 9732a7c..5717b5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,21 @@ dependencies = [ "mach", ] +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aead" version = "0.5.2" @@ -73,6 +88,9 @@ name = "anyhow" version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +dependencies = [ + "backtrace", +] [[package]] name = "autocfg" @@ -80,6 +98,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base16ct" version = "0.1.1" @@ -271,6 +304,12 @@ name = "const-oid" version = "0.10.0-pre" source = "git+https://github.com/RustCrypto/formats#9c3ad4366d7f6132d770fe1ead839fbca295cff9" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -438,6 +477,19 @@ dependencies = [ "syn 2.0.27", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.107", +] + [[package]] name = "dice-cert-check" version = "0.1.0" @@ -487,7 +539,7 @@ dependencies = [ "string-error", "tempfile", "x509-cert 0.2.4", - "yubihsm", + "yubihsm 0.42.0 (git+https://github.com/oxidecomputer/yubihsm.rs)", "zerocopy", "zeroize", ] @@ -701,6 +753,12 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + [[package]] name = "group" version = "0.12.1" @@ -902,6 +960,18 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libusb1-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -953,6 +1023,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "nix" version = "0.24.3" @@ -973,6 +1052,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -1243,6 +1331,17 @@ dependencies = [ "signature 2.0.0", ] +[[package]] +name = "ron" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff" +dependencies = [ + "base64", + "bitflags 1.3.2", + "serde", +] + [[package]] name = "rpassword" version = "7.2.0" @@ -1264,6 +1363,31 @@ dependencies = [ "winapi", ] +[[package]] +name = "rusb" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44a8c36914f9b1a3be712c1dfa48c9b397131f9a75707e570a391735f785c5d1" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.36.8" @@ -1342,6 +1466,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + [[package]] name = "serde" version = "1.0.177" @@ -1635,6 +1765,12 @@ dependencies = [ "serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1911,6 +2047,35 @@ dependencies = [ "spki 0.7.2", ] +[[package]] +name = "yubihsm" +version = "0.42.0" +source = "git+https://github.com/oxidecomputer/yubihsm.rs?branch=v0.42.0-with-audit#ab1d0ac182ae949567d988ddb2fc168ea45e4556" +dependencies = [ + "aes", + "bitflags 2.2.1", + "cbc", + "cmac", + "ecdsa 0.16.7", + "ed25519 2.1.0", + "hmac", + "log", + "p256 0.13.2", + "p384 0.13.0", + "pbkdf2", + "rand_core", + "rusb", + "serde", + "serde_json", + "sha2", + "signature 2.0.0", + "subtle", + "thiserror", + "time 0.3.21", + "uuid", + "zeroize", +] + [[package]] name = "yubihsm" version = "0.42.0" @@ -1939,6 +2104,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "yubihsm-audit" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "derive_more", + "env_logger", + "hex", + "log", + "ron", + "serde", + "serde_json", + "sha2", + "yubihsm 0.42.0 (git+https://github.com/oxidecomputer/yubihsm.rs?branch=v0.42.0-with-audit)", +] + [[package]] name = "zerocopy" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index 34f7b1c..066a540 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,18 @@ members = [ "dice-cert-tmpl", "dice-mfg", "dice-mfg-msgs", + "yhsm-audit", ] + +[workspace.dependencies] +anyhow = { version = "1", features = ["backtrace"] } +clap = { version = "4", features = ["derive"] } +derive_more = "0.99" +env_logger = "0.10" +hex = "0.4" +log = { version = "0.4", features = ["std"] } +ron = "0.8" +serde = "1" +serde_json = { version = "1", features = ["std", "alloc"] } +sha2 = "0.10" +yubihsm = { git = "https://github.com/oxidecomputer/yubihsm.rs", branch="v0.42.0-with-audit", features = ["default", "usb"] } diff --git a/yhsm-audit/Cargo.toml b/yhsm-audit/Cargo.toml new file mode 100644 index 0000000..1ceeb6d --- /dev/null +++ b/yhsm-audit/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "yubihsm-audit" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +clap.workspace = true +derive_more.workspace = true +env_logger.workspace = true +hex.workspace = true +log.workspace = true +ron.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +yubihsm.workspace = true diff --git a/yhsm-audit/src/lib.rs b/yhsm-audit/src/lib.rs new file mode 100644 index 0000000..6745923 --- /dev/null +++ b/yhsm-audit/src/lib.rs @@ -0,0 +1,37 @@ +use clap::ValueEnum; +use std::fmt; + +#[derive(Clone, Debug, ValueEnum)] +pub enum Kind { + LogEntries, + LogEntry, +} + +impl fmt::Display for Kind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Kind::LogEntries => write!(f, "LogEntries"), + Kind::LogEntry => write!(f, "LogEntry"), + } + } +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum Encoding { + // The binary serializer from upstream is not exposed publicly. + // We maintain a patch here: + // https://github.com/oxidecomputer/yubihsm.rs/tree/v0.42.0-with-audit + Bin, + Json, + Ron, +} + +impl fmt::Display for Encoding { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Encoding::Bin => write!(f, "bin"), + Encoding::Json => write!(f, "json"), + Encoding::Ron => write!(f, "ron"), + } + } +} diff --git a/yhsm-audit/src/main.rs b/yhsm-audit/src/main.rs new file mode 100644 index 0000000..f3f149e --- /dev/null +++ b/yhsm-audit/src/main.rs @@ -0,0 +1,231 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use env_logger::Builder; +use log::{debug, LevelFilter}; +use ron::ser::{self, PrettyConfig}; +use serde::{de::DeserializeOwned, Serialize}; +use sha2::{Digest, Sha256}; +use std::{ + borrow::BorrowMut, + env, + fs::File, + io::{self, Read, Write}, + path::PathBuf, + str, +}; +use yubihsm::{ + audit::{LogEntries, LogEntry}, + serialization as yh_ser, +}; + +use yubihsm_audit::{Encoding, Kind}; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +/// Perform various operations on the types that represent the YubiHSM2 +/// audit log. +struct Args { + /// Encoding of data provided as input. + #[clap(long, default_value_t = Encoding::Json, value_enum)] + inform: Encoding, + + /// Path to input data file. If omitted input is read from `stdin`. + #[clap(long)] + input: Option, + + /// command + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Calculate the truncated Sha256 hash of the provide LogEntry data. + Hash, + + /// Split a LogEntries structure into the component LogEntry structures. + /// These are written to individual files named according to the input + /// file and the LogEntry 'item' field. + Split { + #[clap(long, default_value_t = Encoding::Json, value_enum)] + outform: Encoding, + + /// Directory where the serialized encoding of LogEntry structures + /// are written. The default is $(pwd). + #[clap(long)] + workdir: Option, + + /// Prefix used for output files. Output will be in the form: + /// {prefix}-{item}.json where: + /// - `prefix` is the string provided here + /// - `item` is the item number from the LogEntry + #[clap(long, default_value_t = String::from("logentry"))] + prefix: String, + }, + + /// Transform YubiHSM 2 `LogEntries` and `LogEntry` types between + /// encodings. + Xfrm { + /// The YubiHSM type that will be deserialized from the input file. + #[clap(long, default_value_t = Kind::LogEntry, value_enum)] + kind: Kind, + + /// Encoding of output data. + #[clap(long, default_value_t = Encoding::Json, value_enum)] + outform: Encoding, + + /// Path to location where output data is written. If omitted output + /// is written to `stdout`. + #[clap(long)] + output: Option, + }, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + let mut builder = Builder::from_default_env(); + + builder.filter(None, LevelFilter::Debug).init(); + + // if args.input file not provided use stdin + let mut reader: Box = match args.input { + Some(i) => Box::new(File::open(i)?), + None => Box::new(io::stdin()), + }; + + match args.command { + Command::Hash => do_hash(&mut reader, args.inform), + Command::Split { + outform, + workdir, + prefix, + } => do_split(&mut reader, args.inform, outform, workdir, prefix), + Command::Xfrm { + kind, + outform, + output, + } => do_xfrm(&mut reader, args.inform, kind, outform, output), + } +} + +// Calculate the sha256 digest of a LogEtnry. +fn do_hash(reader: &mut R, inform: Encoding) -> Result<()> { + // Holds the data fed into the hash function. + // https://developers.yubico.com/YubiHSM2/Commands/Get_Log_Entries.html + // calls this `Ei.Data`. + let mut buf: Vec = vec![0u8; 32]; + + match inform { + // if it's already encoded as binary just hash it + Encoding::Bin => reader.read_exact(&mut buf)?, + // else we must serialize it to binary before we hash it + e => { + // filling the vec by way of the `Write` trait will append all + // data & we've declared a vec w/ 32 elements: they must be cleared + buf.clear(); + let entry: LogEntry = deserialize(reader, &e)?; + serialize(&entry, &mut buf, &Encoding::Bin)? + } + }; + + debug!("buf: {:?}", &buf); + debug!("hashing {:?}", &buf[..16]); + + let mut hasher = Sha256::new(); + hasher.update(&buf[..16]); + + // get hash string for truncated sha256 sum + let digest = hex::encode(hasher.finalize()); + + println!("{}", digest); + + Ok(()) +} + +fn do_split( + reader: &mut R, + inform: Encoding, + outform: Encoding, + workdir: Option, + prefix: String, +) -> Result<()> { + let log_entries: LogEntries = deserialize(reader, &inform)?; + + // can my closure return an error here? + let mut workdir = match workdir { + Some(p) => p, + None => env::current_dir()?, + }; + + for entry in log_entries.entries { + workdir.push(format!("{}-{}.json", prefix, entry.item)); + let mut writer = Box::new(File::create(&workdir)?); + serialize(entry, &mut writer, &outform)?; + workdir.pop(); + } + + Ok(()) +} + +fn do_xfrm( + reader: &mut R, + inform: Encoding, + kind: Kind, + outform: Encoding, + output: Option, +) -> Result<()> { + // if args.output file not provided use stdout + let mut writer: Box = match output { + Some(o) => Box::new(File::create(o)?), + None => Box::new(io::stdout()), + }; + + match kind { + Kind::LogEntries => { + let l: LogEntries = deserialize(reader, &inform)?; + serialize(l, &mut writer, &outform) + } + Kind::LogEntry => { + let l: LogEntry = deserialize(reader, &inform)?; + serialize(l, &mut writer, &outform) + } + } +} + +fn deserialize( + reader: &mut R, + inform: &Encoding, +) -> Result { + let mut bytes: Vec = Vec::new(); + reader.read_to_end(&mut bytes)?; + + match inform { + Encoding::Bin => Ok(yh_ser::deserialize(&bytes)?), + Encoding::Json => Ok(serde_json::from_slice(&bytes)?), + Encoding::Ron => Ok(ron::de::from_bytes(&bytes)?), + } +} + +fn serialize( + t: T, + writer: &mut W, + outform: &Encoding, +) -> Result<()> { + match outform { + Encoding::Bin => { + let out = yh_ser::serialize(&t)?; + writer.write_all(&out)?; + } + Encoding::Json => { + serde_json::to_writer_pretty(writer.borrow_mut(), &t)? + } + Encoding::Ron => ser::to_writer_pretty( + writer.borrow_mut(), + &t, + PrettyConfig::default(), + )?, + } + + Ok(writer.flush()?) +}