diff --git a/Cargo.lock b/Cargo.lock index c337fc9..1b13406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,7 @@ name = "ms-tcg-tpm-sys" version = "0.0.0" dependencies = [ "bitfield-struct", + "cc", "cmake", "fs-err", "openssl-sys", diff --git a/Cargo.toml b/Cargo.toml index b7177ca..b8d2f22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ postcard = { version = "1.1", default-features = false, features = ["use-std"] } serde = { version = "1.0", features = ["derive"] } [build-dependencies] +cc = "1" cmake = "0.1" fs-err = "3.3" diff --git a/README.md b/README.md index e3b99a6..f8967b7 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,30 @@ cargo run -p test-harness -- ./tpm.nvmem feature. - `test-harness/` - A small sample binary that initializes the TPM, sends a few commands, and persists state to an on-disk `.nvmem` blob. +- `fuzz/` - `cargo-fuzz` targets covering the crate's untrusted-input + boundaries. See [`fuzz/README.md`](fuzz/README.md). + +## Fuzzing + +The `fuzz/` directory holds [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz) +targets for the inputs a vTPM doesn't control: guest-issued commands, saved-state +blobs, and persisted nvmem blobs. + +```sh +cargo install cargo-fuzz +rustup toolchain install nightly + +cargo +nightly fuzz run fuzz_tpm +``` + +Fuzzing needs the TPM's C code instrumented, not just the Rust wrapper around +it, so `build.rs` builds it with clang and the matching sanitizer and coverage +flags whenever Cargo reports that this crate is being built for a fuzzer. That +requires clang to be installed, but nothing needs to be configured by hand, and +it doesn't affect normal builds. + +See [`fuzz/README.md`](fuzz/README.md) for the list of targets and for details +on corpus layout, reproducing crashes, and adjusting the instrumentation. ## Relationship to `tpm-rs` diff --git a/build.rs b/build.rs index 5189451..e5c5f2d 100644 --- a/build.rs +++ b/build.rs @@ -20,6 +20,7 @@ fn main() -> Result<(), Box> { for archive in &source_archives { println!("cargo:rerun-if-changed={}", archive.display()); } + fuzzing::warn_if_prebuilt(); source_archives } // Archives built in-tree live in `OUT_DIR`, and watching those would @@ -129,6 +130,8 @@ mod tpm { cmake_config.define("CMAKE_MSVC_RUNTIME_LIBRARY", "MultiThreadedDLL"); } + crate::fuzzing::configure(&mut cmake_config)?; + match backend { Backend::OpenSsl => { crate::openssl::configure(&mut cmake_config, &tpm_src_dir, &out_dir)?; @@ -428,6 +431,200 @@ mod symbols { } } +/// Instrumenting the TPM's C code when this crate is built for a fuzzer. +mod fuzzing { + use crate::util; + use std::ffi::OsStr; + use std::ffi::OsString; + use std::path::Path; + use std::path::PathBuf; + + /// Whether Cargo is building this crate for a fuzzer. + /// + /// `cargo fuzz` puts `--cfg fuzzing` in `RUSTFLAGS`, which Cargo surfaces + /// to build scripts as this variable. + fn enabled() -> bool { + println!("cargo:rerun-if-env-changed=CARGO_CFG_FUZZING"); + std::env::var_os("CARGO_CFG_FUZZING").is_some() + } + + /// Instrument the TPM build to match how Cargo is building the Rust side. + /// + /// Rust's `-Zsanitizer` and libFuzzer's coverage instrumentation only cover + /// Rust code, which for this crate is a thin wrapper around the C library + /// that does the actual work. Left uninstrumented, the fuzzer would be + /// driving the code that parses commands blind, and AddressSanitizer would + /// only see what its allocator interceptors catch rather than the memory + /// errors inside that code. + pub(crate) fn configure( + cmake_config: &mut cmake::Config, + ) -> Result<(), Box> { + if !enabled() { + return Ok(()); + } + + // An explicitly empty value opts out of instrumenting the C code. + let flags = match util::env("TCG_TPM_FUZZ_CFLAGS") { + Some(flags) => flags, + None => default_flags(), + }; + let flags: Vec<&OsStr> = flags + .to_str() + .ok_or("TCG_TPM_FUZZ_CFLAGS is not valid UTF-8")? + .split_whitespace() + .map(OsStr::new) + .collect(); + if flags.is_empty() { + return Ok(()); + } + + if let Some(compiler) = compiler()? { + drop_stale_cmake_cache(&compiler)?; + cmake_config.define("CMAKE_C_COMPILER", &compiler); + } + + for flag in flags { + cmake_config.cflag(flag); + } + + Ok(()) + } + + /// CMake refuses to reconfigure an existing build tree with a different + /// compiler, which would turn something as ordinary as installing a newer + /// clang into a confusing build failure. Drop the cache so that CMake + /// configures from scratch instead. + fn drop_stale_cmake_cache(compiler: &Path) -> Result<(), Box> { + // Where `cmake::Config` puts the build tree, given it inherits `OUT_DIR`. + let cache = PathBuf::from(std::env::var("OUT_DIR")?).join("build/CMakeCache.txt"); + let Ok(contents) = fs_err::read_to_string(&cache) else { + return Ok(()); + }; + + let cached = contents.lines().find_map(|line| { + line.strip_prefix("CMAKE_C_COMPILER:")? + .split_once('=') + .map(|(_, value)| Path::new(value)) + }); + + if cached.is_some_and(|cached| cached != compiler) { + fs_err::remove_file(&cache)?; + } + + Ok(()) + } + + /// Warn that `TCG_TPM_LIB_DIR` libraries are used as-is, since whoever + /// built them is the one who decides whether they're instrumented. + pub(crate) fn warn_if_prebuilt() { + if enabled() { + println!( + "cargo:warning=fuzzing against the pre-built TPM libraries in TCG_TPM_LIB_DIR; \ + unless they were built with sanitizer and coverage instrumentation, the fuzzer \ + will not see inside them" + ); + } + } + + /// The instrumentation to build the TPM with, mirroring what `cargo fuzz` + /// asks `rustc` for. + fn default_flags() -> OsString { + // Gives the C code the SanitizerCoverage instrumentation libFuzzer + // needs, without letting clang link in a `main` of its own. + let mut flags = String::from("-fsanitize=fuzzer-no-link"); + + // `-Zsanitizer=...` reaches build scripts as this variable, already + // comma-separated the way clang wants it. + println!("cargo:rerun-if-env-changed=CARGO_CFG_SANITIZE"); + if let Some(sanitizers) = std::env::var_os("CARGO_CFG_SANITIZE") + && !sanitizers.is_empty() + { + flags.push_str(" -fsanitize="); + flags.push_str(&sanitizers.to_string_lossy()); + } + + flags.into() + } + + /// The compiler to build the instrumented TPM with, or `None` to keep the + /// one the build is already configured to use. + /// + /// The flags above are clang-only: GCC has no `-fsanitize=fuzzer-no-link`, + /// and the `-fsanitize-coverage=trace-pc` scheme it does support was + /// removed from libFuzzer. + fn compiler() -> Result, Box> { + if let Some(compiler) = util::env("TCG_TPM_FUZZ_CC") { + return Ok(Some(PathBuf::from(compiler))); + } + + // Leave an already-clang compiler (and anything the caller pointed `CC` + // at) alone, so cross-compilation setups keep working. + if cc::Build::new().try_get_compiler()?.is_like_clang() { + return Ok(None); + } + + let clang = find_clang().ok_or( + "fuzzing needs clang to instrument the TPM's C code, but none was found on PATH. \ + Install clang, or point TCG_TPM_FUZZ_CC at one. To fuzz without instrumenting the \ + C code - which leaves the fuzzer blind to the code that parses TPM commands - set \ + TCG_TPM_FUZZ_CFLAGS to an empty value.", + )?; + + Ok(Some(clang)) + } + + /// Look for clang on `PATH`. + fn find_clang() -> Option { + // clang-cl is the driver that understands MSVC's command line. + let stem = if util::is_windows_msvc().ok()? { + "clang-cl" + } else { + "clang" + }; + + let path = std::env::var_os("PATH")?; + let mut newest: Option<(u32, PathBuf)> = None; + + for dir in std::env::split_paths(&path) { + let unversioned = dir.join(format!("{stem}{}", std::env::consts::EXE_SUFFIX)); + if unversioned.is_file() { + return Some(unversioned); + } + + // Debian and its derivatives only ship versioned binaries unless + // the `clang` metapackage is installed, so fall back to the newest + // version that is installed. + let Ok(entries) = fs_err::read_dir(&dir) else { + continue; + }; + + for entry in entries.flatten() { + let name = entry.file_name(); + let Some(name) = name.to_str() else { + continue; + }; + let name = name + .strip_suffix(std::env::consts::EXE_SUFFIX) + .unwrap_or(name); + + let Some(version) = name + .strip_prefix(stem) + .and_then(|version| version.strip_prefix('-')) + .and_then(|version| version.parse::().ok()) + else { + continue; + }; + + if newest.as_ref().is_none_or(|(newest, _)| version > *newest) { + newest = Some((version, entry.path())); + } + } + } + + newest.map(|(_, path)| path) + } +} + /// Environment and filesystem odds and ends shared by the rest of the script. mod util { use std::ffi::OsString; diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..c86f687 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,360 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitfield-struct" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca6739863c590881f038d033a146c51ddae239186a4327014839fd864f44ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "ms-tcg-tpm-sys" +version = "0.0.0" +dependencies = [ + "bitfield-struct", + "cc", + "cmake", + "fs-err", + "openssl-sys", + "postcard", + "serde", + "tracing", +] + +[[package]] +name = "ms-tcg-tpm-sys-fuzz" +version = "0.0.0" +dependencies = [ + "arbitrary", + "libfuzzer-sys", + "ms-tcg-tpm-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-src" +version = "300.6.1+3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..6dedadb --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,58 @@ +# Copyright (C) Microsoft Corporation. All rights reserved. + +[package] +name = "ms-tcg-tpm-sys-fuzz" +version = "0.0.0" +publish = false +edition = "2024" + +[package.metadata] +cargo-fuzz = true + +[features] +# The TPM library requires OpenSSL 3.5 or newer, which is newer than what most +# distros ship, so build it from source by default. +default = ["openssl", "vendored"] + +# `openssl` and `symcrypt` are mutually exclusive; see the root crate's manifest. +openssl = ["ms-tcg-tpm-sys/openssl"] +symcrypt = ["ms-tcg-tpm-sys/symcrypt"] +vendored = ["ms-tcg-tpm-sys/vendored"] + +[dependencies] +arbitrary = { version = "1", features = ["derive"] } +libfuzzer-sys = "0.4" +ms-tcg-tpm-sys = { path = "..", default-features = false } + +[[bin]] +name = "fuzz_tpm" +path = "fuzz_targets/fuzz_tpm.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_tpm_session" +path = "fuzz_targets/fuzz_tpm_session.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_nvmem" +path = "fuzz_targets/fuzz_nvmem.rs" +test = false +doc = false +bench = false + +[lints.rust] +missing_docs = "warn" +unused_qualifications = "warn" + +[lints.clippy] +undocumented_unsafe_blocks = "warn" + +# Keep the fuzz crate out of the root workspace, so that the instrumented build +# doesn't interfere with regular `cargo build` invocations. +[workspace] +members = ["."] diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..1113ea5 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,252 @@ +# Fuzzing `ms-tcg-tpm-sys` + +[`cargo-fuzz`](https://rust-fuzz.github.io/book/cargo-fuzz.html) targets for the +crate's untrusted-input boundaries: the TPM command stream, saved-state blobs, +and persisted nvmem blobs. + +## Running + +```sh +cargo install cargo-fuzz +rustup toolchain install nightly + +# Fuzz until interrupted. +cargo +nightly fuzz run fuzz_tpm + +# Anything after `--` goes to libFuzzer. +cargo +nightly fuzz run fuzz_tpm -- -max_total_time=600 -dict=fuzz/tpm.dict +cargo +nightly fuzz run fuzz_tpm_session -- -runs=100000 +``` + +The `fuzz_tpm` seed corpus is checked in, and `cargo fuzz` doesn't pick it up on +its own, so pass it as a second, read-only corpus to start from real commands +rather than random bytes. libFuzzer writes new inputs to the first directory it +is given and requires it to already exist: + +```sh +mkdir -p fuzz/corpus/fuzz_tpm +cargo +nightly fuzz run fuzz_tpm fuzz/corpus/fuzz_tpm fuzz/seed_corpus/fuzz_tpm \ + -- -dict=fuzz/tpm.dict +``` + +The other two targets take `arbitrary`-encoded structures rather than raw +bytes, so there's nothing meaningful to hand-write a seed for; they build their +corpus from scratch. + +Reproducing and minimizing a crash works as usual: + +```sh +cargo +nightly fuzz run fuzz_tpm fuzz/artifacts/fuzz_tpm/crash- +cargo +nightly fuzz tmin fuzz_tpm fuzz/artifacts/fuzz_tpm/crash- +``` + +libFuzzer's default `-timeout` is 1200 seconds, which is longer than any +campaign worth running, so an input that sends the TPM into an infinite loop is +not reported as a hang - it just silently pins a worker for the whole run, and +the only sign is a child still alive after `-max_total_time` has passed. Pass +something realistic: + +```sh +cargo +nightly fuzz run fuzz_nvmem -- -timeout=25 -max_total_time=600 +``` + +To collect every distinct failure in one run rather than stopping at the first, +fork mode keeps going and writes an artifact per failure: + +```sh +cargo +nightly fuzz run fuzz_nvmem -- -fork=4 -timeout=25 \ + -ignore_crashes=1 -ignore_timeouts=1 -ignore_ooms=1 +``` + +The `symcrypt` backend can be fuzzed with `--no-default-features --features +symcrypt` (after `./scripts/fetch-symcrypt.sh`). + +## Targets + +| Target | Input | What it covers | +| --- | --- | --- | +| `fuzz_tpm` | A raw TPM command stream | `execute_command`: header validation, command unmarshaling, and dispatch - the bytes a guest controls | +| `fuzz_tpm_session` | A sequence of platform operations | Commands interleaved with power cycles, live save / restore, locality changes, and cancellation | +| `fuzz_nvmem` | A persisted nvmem blob | Booting on a corrupted, truncated, or hostile nvmem blob | + +`fuzz_tpm` takes plain bytes: the input is split into commands along the +boundaries declared by each command's own `commandSize` field, so a corpus +entry can be a capture of a real command stream, and +[`seed_corpus/fuzz_tpm/`](seed_corpus/fuzz_tpm) holds hand-built commands to +start from. + +The other two take +[`arbitrary`](https://docs.rs/arbitrary)-derived structures. `fuzz_nvmem` mostly +works by splicing fuzzer controlled bytes into a blob the +TPM itself produced, since random bytes never survive a blob's framing and +header validation. + +## Oracles + +Beyond the crashes, hangs, and leaks that libFuzzer and the sanitizers find on +their own, the targets assert that: + +- a reported response length fits in the response buffer that was handed to the + TPM, +- a non-empty response is at least a header long, and its `responseSize` field + matches the number of bytes actually returned, +- a save / restore / save round-trip reproduces the blob it started from - + otherwise state is being dropped or invented in transit, and a migrated TPM + wouldn't match the one it was migrated from, +- restoring a blob the TPM just saved always succeeds, and +- an `initialize` that fails leaves the platform singleton free to be claimed + again. + +## Reaching the command handlers + +Almost every TPM command needs three things before its handler is entered: a +well formed header, handles that name something which exists, and a valid +authorization area. A mutator invents none of them, so a fuzzer left to itself +spends its whole budget being turned away at the front door. A first campaign +reached 14 of the 125 implemented commands, and the only two of those that +take an authorization were the two the seed corpus happened to spell out by +hand. + +Three things address that, all in [`src/lib.rs`](src/lib.rs): + +- `SETUP_COMMANDS` runs once per process, before the snapshot that every + iteration rolls back to, and leaves behind two loaded keys, a persistent copy + of one of them, five NV indices, and one HMAC and one policy session. Their + handles are what `KNOWN_HANDLES` lists, and the setup asserts the TPM handed + those exact handles back, so the table cannot silently drift out of date and + quietly cost coverage. +- `PASSWORD_SESSION` is a `TPM_RS_PW` authorization area with an empty + password, which is what the great majority of commands are asking for. + `fuzz_tpm_session` assembles it structurally; `tpm.dict` carries it so that + `fuzz_tpm` can splice one into a raw byte stream. +- The first authorization of a dictionary-attack protected object does not run + the command at all: it writes the `daUsed` state to NV and returns + `TPM_RC_RETRY` (see `SessionProcess.c`). The harness settles that before + snapshotting, so iterations don't spend their first authorized command on it. + +What gets seeded is shaped by the profile's limits rather than by what would be +convenient: + +- `MAX_LOADED_OBJECTS` is 3, so exactly two objects are seeded and the third + slot is deliberately left empty. Seeding a third would make every + `TPM2_Load`, `TPM2_Create` and `TPM2_CreateLoaded` fail with + `TPM_RC_OBJECT_MEMORY`, and the seeding would cost more coverage than it + bought. For the same reason two of the three session slots are used, not all + three. +- The two objects are an unrestricted ECC signing key and a restricted + decryption key. A signing key cannot be a parent, so without the second one + nothing that needs somewhere to put an object is reachable. +- NV indices are free - they don't consume object slots - so there is one of + each type that has commands specific to it: ordinary, counter, bit field, + extend, and one carrying `READ_STCLEAR`/`WRITE_STCLEAR` for the lock + commands. +- Persistent objects are free too, for the same reason, so the RSA and ML-DSA + keys are generated once, evicted to NV, and their transient slot handed + straight back. Without them neither algorithm runs at all: no RSA key means + key generation, the prime sieve and Miller-Rabin are never entered, and no + ML-DSA key means the streaming signature commands - which take a + `TPM2B_SIGNATURE_CTX`, an ML-DSA context - have nothing to name. RSA is + 1024-bit deliberately: it exercises the same generation path a larger modulus + would, and this runs on every process startup. + +[`seed_corpus/fuzz_tpm/`](seed_corpus/fuzz_tpm) then names those handles from +raw byte streams. Every seed there was checked to actually reach its command +handler rather than being rejected on the way in. + +Some commands can't be seeded at all, because what they take is data only the +TPM can produce: `TPM2_Load` wants a private blob wrapped by a particular +parent, and `TPM2_ContextLoad` wants a saved context. `canned_commands()` +covers those by assembling the commands at setup time out of real responses - +a `TPM2_Create` and a `TPM2_ContextSave` - which `fuzz_tpm_session` dispatches +through `Op::Canned`. Both are built before the snapshot, since a saved context +is only valid against the state it was saved from, which is the state every +iteration rolls back to. + +`fuzz_nvmem` has neither of those advantages: its +input is `arbitrary`-encoded, so there is no seed corpus to hand it and no +dictionary to splice from, and raw bytes almost never clear the command header. +That would waste the interesting half of what they test - not whether a +tampered blob is rejected, but what the TPM does while running on one that +wasn't. Both take commands as a `Known(u8)` index into `known_commands()` or +`Raw(Vec)` bytes, so the fuzzer can issue something real without giving up +the ability to send garbage. + +A handful of commands stay out of reach by construction, and are not worth +hand-holding: `TPM2_VerifySignature` and `TPM2_PolicyAuthorize` need a real +signature or ticket over TPM-generated data, `TPM2_NV_ChangeAuth` needs an +ADMIN-role policy session, `TPM2_PP_Commands` needs physical presence asserted, +and `TPM2_SignSequenceStart` needs an opaque `TPM2B_SIGNATURE_CTX`. + +Note that all three targets share this snapshot, so the effect is not limited to +the command targets: the blob `fuzz_nvmem` corrupts has real NV entries +rather than only what manufacturing wrote. + +## Determinism + +Replaying a crash has to reproduce it, so every input the TPM sees other than +the fuzzer's bytes is fixed: entropy comes from a fixed-seed PRNG, and the +monotonic timer advances by a fixed step per call. Both are rewound at the start +of every iteration. + +Manufacturing a TPM is far too slow to do per iteration, so every target +manufactures one per process and rolls it back to a pristine snapshot before +each iteration. State that a command leaves behind but that isn't part of a +saved-state blob will therefore bleed into subsequent iterations, which can make +a crash depend on the inputs that ran before it. That's worth chasing rather +than papering over: the same bleed-through would break a live migration. + +This matters in practice. `fuzz_nvmem` originally booted a fresh TPM per +iteration via `InitKind::ColdInitWithPersistentState` instead of rolling one +back, and 50 of the 61 artifacts a campaign produced could not be replayed on +their own. Installing the blob with `reset` on a rolled-back TPM fixed that. +Note that residual nondeterminism in a replay is itself a signal: the TPM +library reads uninitialized stack memory when an NV read fails, so what a +corrupt blob does can genuinely depend on what ran before it. + +## Instrumenting the C library + +Rust's `-Zsanitizer=address` and libFuzzer's coverage instrumentation only apply +to Rust code. Since essentially all of the interesting code here is the vendored +C TPM library, the C build has to be instrumented as well, which requires clang. + +[`build.rs`](../build.rs) handles this: `cargo fuzz` puts `--cfg fuzzing` in +`RUSTFLAGS`, which Cargo passes to build scripts as `CARGO_CFG_FUZZING`, so the +build script knows to build the TPM with clang, `-fsanitize=fuzzer-no-link`, and +whatever `-Zsanitizer` Cargo reported in `CARGO_CFG_SANITIZE`. Nothing needs to +be set by hand, and normal (non-fuzzing) builds are untouched. + +It's worth it: on a short run of `fuzz_tpm`, instrumenting the C code took +coverage from ~550 edges (the Rust wrapper alone) to ~2300, and it's what lets +ASan catch memory errors inside the TPM library - such as a read past the end of +a global - rather than only the ones its allocator interceptors see. + +Two env-vars adjust this, both accepting the target-prefixed forms the crate's +other env-vars do: + +- `TCG_TPM_FUZZ_CC` - the compiler to build the instrumented TPM with. Set this + when cross-compiling, or when the clang to use isn't the newest one on `PATH`. + If the build is already configured to use a clang (via `CC`, say), that one is + kept and this isn't needed. +- `TCG_TPM_FUZZ_CFLAGS` - replaces the instrumentation flags entirely. Setting + it to an empty value fuzzes without instrumenting the C code, which leaves the + fuzzer blind to the code that parses TPM commands. + +The build script fails rather than silently producing an uninstrumented fuzzer +if it can't find a clang. + +GCC can't be used for this - it has no `-fsanitize=fuzzer-no-link`, and the +`-fsanitize-coverage=trace-pc` scheme it does support was removed from libFuzzer. + +OpenSSL is deliberately left uninstrumented: it's a dependency of the code under +test rather than the target, and instrumenting it slows the build down and +spends the fuzzer's energy inside crypto primitives it can't steer. ASan's +allocator interceptors still cover heap errors there. + +## Corpus layout + +- `corpus//` - the working corpus libFuzzer grows as it runs + (git-ignored). +- `seed_corpus/fuzz_tpm/` - checked-in starting inputs. +- `artifacts//` - crashing inputs (git-ignored). +- `tpm.dict` - TPM wire-format constants (tags, command codes, handles, + algorithm ids) for libFuzzer's `-dict`. diff --git a/fuzz/fuzz_targets/fuzz_nvmem.rs b/fuzz/fuzz_targets/fuzz_nvmem.rs new file mode 100644 index 0000000..643a8fd --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_nvmem.rs @@ -0,0 +1,108 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Fuzzes booting the TPM on a persisted nvmem blob. +//! +//! A vTPM's nvmem blob lives outside the TPM, in whatever the host uses for +//! persistent storage, so the TPM has to survive being handed one that has been +//! rolled back or tampered with. Each iteration power-cycles onto a +//! corrupted blob and then drives commands against whatever comes up. +//! +//! The blob is installed with `reset`, on a TPM that [`with_tpm`] has just +//! rolled back, rather than with `InitKind::ColdInitWithPersistentState` on a +//! freshly built one. Both take the same path through the platform's nvmem +//! layer and into `_TPM_Init`, but the rollback means an iteration depends only +//! on its own input: leaving the previous iteration's globals in place made +//! most of this target's crashes impossible to replay on their own. +//! +//! The blob always stays the full `NV_MEMORY_SIZE`. The platform now rejects +//! any other size up front - a shorter region let the TPM library address NV +//! memory that wasn't there - so mutating the length here would just bounce off +//! that check and waste the iteration. `tests/nvmem_size.rs` covers it instead. + +#![no_main] + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use ms_tcg_tpm_sys_fuzz::Patch; +use ms_tcg_tpm_sys_fuzz::TPM2_SHUTDOWN_STATE; +use ms_tcg_tpm_sys_fuzz::TPM2_STARTUP_CLEAR; +use ms_tcg_tpm_sys_fuzz::TPM2_STARTUP_STATE; +use ms_tcg_tpm_sys_fuzz::baseline_nvmem; +use ms_tcg_tpm_sys_fuzz::known_commands; +use ms_tcg_tpm_sys_fuzz::split_commands; +use ms_tcg_tpm_sys_fuzz::with_tpm; + +/// Caps how much work a single input can ask for, keeping the fuzzer's +/// executions-per-second up. +const MAX_COMMANDS: usize = 8; + +/// A command to run against the TPM that came up on the corrupted blob. +#[derive(Arbitrary, Debug)] +enum Command { + /// One of the harness' well formed commands. This target has no seed + /// corpus and no dictionary, so without these it rarely gets a command + /// past the header and never sees what a tampered blob does to a TPM that + /// is actually running. + Known(u8), + /// Fuzzer supplied bytes, split on their declared command sizes. + Raw(Vec), +} + +#[derive(Arbitrary, Debug)] +struct Input { + /// Whether to shut down orderly before the power cycle, and come back up + /// with `TPM_SU_STATE`. Resuming reads far more out of the blob than a + /// clear start does, but only makes sense against a blob that claims an + /// orderly shutdown - which the patches below are free to lie about. + resume: bool, + /// Corruption to apply to the nvmem blob. + patches: Vec, + /// Commands to run against the TPM that comes up on the corrupted blob. + commands: Vec, +} + +fuzz_target!(|input: Input| { + let mut nvmem = baseline_nvmem().to_vec(); + Patch::apply_all(&mut nvmem, &input.patches); + + with_tpm(|tpm| { + if input.resume { + // Has to happen before the power cycle, on the pristine TPM, so + // that the state being resumed onto is one the TPM really wrote. + let _ = tpm.execute_command(&mut TPM2_SHUTDOWN_STATE.to_vec()); + } + + // Rejecting a blob outright is a perfectly good outcome. + if tpm.reset(Some(&nvmem)).is_err() { + return; + } + + // Start the TPM up before anything else; that's where the bulk of the + // nvmem is parsed. + let startup = if input.resume { + TPM2_STARTUP_STATE + } else { + TPM2_STARTUP_CLEAR + }; + + let mut commands = vec![startup.to_vec()]; + for command in &input.commands { + match command { + Command::Known(index) => { + let known = known_commands(); + commands.push(known[*index as usize % known.len()].to_vec()); + } + Command::Raw(bytes) => { + commands.append(&mut split_commands(bytes, MAX_COMMANDS)); + } + } + if commands.len() >= MAX_COMMANDS { + break; + } + } + + for command in commands.iter_mut().take(MAX_COMMANDS) { + let _ = tpm.execute_command(command); + } + }); +}); diff --git a/fuzz/fuzz_targets/fuzz_tpm.rs b/fuzz/fuzz_targets/fuzz_tpm.rs new file mode 100644 index 0000000..d11831e --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_tpm.rs @@ -0,0 +1,35 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Fuzzes TPM command dispatch. +//! +//! The input is a raw TPM command stream: it is split into individual commands +//! along the boundaries declared by each command's `commandSize` field, and +//! each one is dispatched through `MsTpm185Platform::execute_command`, which is +//! how a transport (say, a vTPM's MMIO/CRB interface) would hand guest +//! controlled bytes to this crate. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use ms_tcg_tpm_sys_fuzz::split_commands; +use ms_tcg_tpm_sys_fuzz::with_tpm; + +/// Caps how much work a single input can ask for, keeping the fuzzer's +/// executions-per-second up. +const MAX_COMMANDS: usize = 16; + +fuzz_target!(|data: &[u8]| { + if data.is_empty() { + return; + } + + let mut commands = split_commands(data, MAX_COMMANDS); + + with_tpm(|tpm| { + for command in &mut commands { + // Errors are the crate correctly rejecting a malformed request; + // it's crashes, hangs, and leaks that this target is looking for. + let _ = tpm.execute_command(command); + } + }); +}); diff --git a/fuzz/fuzz_targets/fuzz_tpm_session.rs b/fuzz/fuzz_targets/fuzz_tpm_session.rs new file mode 100644 index 0000000..c50dbe2 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_tpm_session.rs @@ -0,0 +1,203 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Fuzzes whole TPM sessions. +//! +//! Where `fuzz_tpm` hammers on a single entry point, this target drives the +//! rest of the crate's surface - power cycles, live save / restore, locality +//! changes and command cancellation - interleaved with commands, looking for +//! bugs that only show up in a particular ordering of platform events. +//! +//! Commands are (mostly) generated with a well formed header, so that the +//! fuzzer spends its time on per-command unmarshaling rather than on the header +//! parsing that `fuzz_tpm` already covers. + +#![no_main] + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use ms_tcg_tpm_sys::Locality; +use ms_tcg_tpm_sys_fuzz::KNOWN_HANDLES; +use ms_tcg_tpm_sys_fuzz::PASSWORD_SESSION; +use ms_tcg_tpm_sys_fuzz::TPM_CC_FIRST; +use ms_tcg_tpm_sys_fuzz::build_structured_command; +use ms_tcg_tpm_sys_fuzz::canned_commands; +use ms_tcg_tpm_sys_fuzz::with_tpm; + +/// Caps how much work a single input can ask for, keeping the fuzzer's +/// executions-per-second up. +const MAX_OPS: usize = 24; + +/// Caps the handle area. `MAX_HANDLE_NUM` is 3 in this profile, so anything +/// beyond that is parameter bytes rather than a handle. +const MAX_HANDLES: usize = 3; + +/// A handle to place in a command's handle area. +#[derive(Arbitrary, Debug)] +enum Handle { + /// One of the handles that actually names something, selected modulo + /// [`KNOWN_HANDLES`]. + Known(u8), + /// An arbitrary handle, for the handle validation itself. + Raw(u32), +} + +impl Handle { + fn resolve(&self) -> u32 { + match self { + Handle::Known(index) => KNOWN_HANDLES[*index as usize % KNOWN_HANDLES.len()], + Handle::Raw(handle) => *handle, + } + } +} + +/// The authorization area to attach to a command. +#[derive(Arbitrary, Debug)] +enum Auth { + /// No authorization area, tagging the command `TPM_ST_NO_SESSIONS`. + None, + /// One to three empty password sessions, which is what the great majority + /// of commands are asking for. + Password(u8), + /// A fuzzer supplied authorization area, to exercise the session parser + /// rather than the command behind it. + Raw(Vec), +} + +impl Auth { + fn build(&self) -> Option> { + match self { + Auth::None => None, + Auth::Password(count) => { + let count = 1 + *count as usize % 3; + Some(PASSWORD_SESSION.repeat(count)) + } + Auth::Raw(bytes) => Some(bytes.clone()), + } + } +} + +#[derive(Arbitrary, Debug)] +enum Op { + /// Dispatch a command with a well formed header, a handle area, and an + /// authorization area, leaving the fuzzer to drive the parameters. + Command { + /// Offset from `TPM_CC_FIRST`, which covers every implemented command + /// code, plus a margin of unimplemented ones. + code_offset: u8, + /// The command's handle area. + handles: Vec, + /// The command's authorization area. + auth: Auth, + /// Everything after the authorization area. + params: Vec, + }, + /// Dispatch raw bytes, header and all. + Raw(Vec), + /// Dispatch one of the commands the harness built out of data the TPM + /// itself produced - a `TPM2_Load` of a real private blob, or a + /// `TPM2_ContextLoad` of a real saved context. Neither is reachable + /// otherwise, and a loaded child object is what most of the remaining + /// object commands are waiting on. + Canned(u8), + /// Dispatch raw bytes through the unchecked entry point, skipping the + /// wrapper's request size validation. + RawUnchecked(Vec), + /// Round-trip the live state through a save / restore, the way a live + /// migration would. + SaveRestore, + /// Simulate a power cycle. + Reset, + /// Assign a locality to subsequent commands. Values that aren't valid + /// localities are skipped. + SetLocality(u8), + /// Set or clear the cancel flag. + SetCancelFlag(bool), + /// Jump the platform clock forward, in minutes. + AdvanceClock(u16), + /// Save, probe, restore, probe again. Restoring the state the first probe + /// ran against has to reproduce its answers, so state the blob fails to + /// carry shows up as a divergence rather than staying silent. + RollbackFidelity, +} + +fuzz_target!(|ops: Vec| { + if ops.is_empty() { + return; + } + + with_tpm(|tpm| { + for op in ops.iter().take(MAX_OPS) { + match op { + Op::Command { + code_offset, + handles, + auth, + params, + } => { + let handles: Vec = handles + .iter() + .take(MAX_HANDLES) + .map(Handle::resolve) + .collect(); + let code = TPM_CC_FIRST + *code_offset as u32; + let mut command = + build_structured_command(code, &handles, auth.build().as_deref(), params); + let _ = tpm.execute_command(&mut command); + } + Op::Raw(bytes) => { + let mut command = bytes.clone(); + let _ = tpm.execute_command(&mut command); + } + Op::Canned(index) => { + let canned = canned_commands(); + let mut command = canned[*index as usize % canned.len()].clone(); + let _ = tpm.execute_command(&mut command); + } + Op::RawUnchecked(bytes) => { + let mut command = bytes.clone(); + tpm.execute_command_unchecked(&mut command); + } + Op::SaveRestore => { + let saved = tpm.save_state(); + tpm.restore_state(saved.clone()) + .expect("state the TPM just saved should restore"); + + // Restoring a blob and saving it right back out has to + // produce that same blob, otherwise state is being dropped + // (or invented) on the way through, and a migrated TPM + // wouldn't match the one it was migrated from. + assert!( + tpm.save_state() == saved, + "save / restore / save round-trip changed the saved state" + ); + } + Op::Reset => { + tpm.reset(None).expect("power cycling should succeed"); + } + Op::SetLocality(locality) => { + if let Ok(locality) = Locality::try_from(*locality) { + tpm.set_locality(locality); + } + } + Op::SetCancelFlag(enabled) => { + tpm.set_cancel_flag(*enabled); + } + Op::AdvanceClock(minutes) => { + tpm.advance_clock(u64::from(*minutes) * 60_000); + } + Op::RollbackFidelity => { + let saved = tpm.save_state(); + let before = tpm.probe(); + tpm.restore_state(saved) + .expect("state the TPM just saved should restore"); + let after = tpm.probe(); + + assert!( + before == after, + "restore did not roll the TPM back: probe responses diverged" + ); + } + } + } + }); +}); diff --git a/fuzz/seed_corpus/fuzz_tpm/certify.bin b/fuzz/seed_corpus/fuzz_tpm/certify.bin new file mode 100644 index 0000000..9655c28 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/certify.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/change_eps.bin b/fuzz/seed_corpus/fuzz_tpm/change_eps.bin new file mode 100644 index 0000000..71410be Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/change_eps.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/change_pps.bin b/fuzz/seed_corpus/fuzz_tpm/change_pps.bin new file mode 100644 index 0000000..72a26c8 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/change_pps.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/clear.bin b/fuzz/seed_corpus/fuzz_tpm/clear.bin new file mode 100644 index 0000000..d6b0c77 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/clear.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/clear_control.bin b/fuzz/seed_corpus/fuzz_tpm/clear_control.bin new file mode 100644 index 0000000..f70f9d9 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/clear_control.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/clock_rate.bin b/fuzz/seed_corpus/fuzz_tpm/clock_rate.bin new file mode 100644 index 0000000..b3ac66f Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/clock_rate.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/clock_set.bin b/fuzz/seed_corpus/fuzz_tpm/clock_set.bin new file mode 100644 index 0000000..3bdac62 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/clock_set.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/commit.bin b/fuzz/seed_corpus/fuzz_tpm/commit.bin new file mode 100644 index 0000000..6f7a508 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/commit.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/commit2.bin b/fuzz/seed_corpus/fuzz_tpm/commit2.bin new file mode 100644 index 0000000..b192950 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/commit2.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/context_save.bin b/fuzz/seed_corpus/fuzz_tpm/context_save.bin new file mode 100644 index 0000000..62c1479 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/context_save.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/create.bin b/fuzz/seed_corpus/fuzz_tpm/create.bin new file mode 100644 index 0000000..876f9fd Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/create.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/create_loaded.bin b/fuzz/seed_corpus/fuzz_tpm/create_loaded.bin new file mode 100644 index 0000000..e44f590 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/create_loaded.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/create_primary.bin b/fuzz/seed_corpus/fuzz_tpm/create_primary.bin new file mode 100644 index 0000000..ee9bc26 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/create_primary.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/createprimary_sha384.bin b/fuzz/seed_corpus/fuzz_tpm/createprimary_sha384.bin new file mode 100644 index 0000000..4b962a3 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/createprimary_sha384.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/da_lock_reset.bin b/fuzz/seed_corpus/fuzz_tpm/da_lock_reset.bin new file mode 100644 index 0000000..8094f8a Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/da_lock_reset.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/da_parameters.bin b/fuzz/seed_corpus/fuzz_tpm/da_parameters.bin new file mode 100644 index 0000000..56e0dfe Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/da_parameters.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/decapsulate.bin b/fuzz/seed_corpus/fuzz_tpm/decapsulate.bin new file mode 100644 index 0000000..c3e4953 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/decapsulate.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/ec_ephemeral.bin b/fuzz/seed_corpus/fuzz_tpm/ec_ephemeral.bin new file mode 100644 index 0000000..c551337 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/ec_ephemeral.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/ecc_decrypt2.bin b/fuzz/seed_corpus/fuzz_tpm/ecc_decrypt2.bin new file mode 100644 index 0000000..0aed782 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/ecc_decrypt2.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/ecc_encrypt.bin b/fuzz/seed_corpus/fuzz_tpm/ecc_encrypt.bin new file mode 100644 index 0000000..995d98d Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/ecc_encrypt.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/ecc_parameters.bin b/fuzz/seed_corpus/fuzz_tpm/ecc_parameters.bin new file mode 100644 index 0000000..4bbf4c7 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/ecc_parameters.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/ecdh_keygen.bin b/fuzz/seed_corpus/fuzz_tpm/ecdh_keygen.bin new file mode 100644 index 0000000..53a2224 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/ecdh_keygen.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/ecdh_zgen.bin b/fuzz/seed_corpus/fuzz_tpm/ecdh_zgen.bin new file mode 100644 index 0000000..3f2b030 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/ecdh_zgen.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/ecdh_zgen2.bin b/fuzz/seed_corpus/fuzz_tpm/ecdh_zgen2.bin new file mode 100644 index 0000000..e98b8fb Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/ecdh_zgen2.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt.bin b/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt.bin new file mode 100644 index 0000000..e934914 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt2.bin b/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt2.bin new file mode 100644 index 0000000..a41e4ef Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/encryptdecrypt2.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/event_seq_complete.bin b/fuzz/seed_corpus/fuzz_tpm/event_seq_complete.bin new file mode 100644 index 0000000..10ea20a Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/event_seq_complete.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/flush_context.bin b/fuzz/seed_corpus/fuzz_tpm/flush_context.bin new file mode 100644 index 0000000..b4de2c4 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/flush_context.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/get_capability.bin b/fuzz/seed_corpus/fuzz_tpm/get_capability.bin new file mode 100644 index 0000000..61886a5 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/get_capability.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/get_cmd_audit.bin b/fuzz/seed_corpus/fuzz_tpm/get_cmd_audit.bin new file mode 100644 index 0000000..da2c238 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/get_cmd_audit.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/get_random.bin b/fuzz/seed_corpus/fuzz_tpm/get_random.bin new file mode 100644 index 0000000..5de8ad6 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/get_random.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/get_session_audit.bin b/fuzz/seed_corpus/fuzz_tpm/get_session_audit.bin new file mode 100644 index 0000000..ff29edf Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/get_session_audit.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/get_test_result.bin b/fuzz/seed_corpus/fuzz_tpm/get_test_result.bin new file mode 100644 index 0000000..c0dd727 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/get_test_result.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/get_time.bin b/fuzz/seed_corpus/fuzz_tpm/get_time.bin new file mode 100644 index 0000000..ff40c70 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/get_time.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/hash.bin b/fuzz/seed_corpus/fuzz_tpm/hash.bin new file mode 100644 index 0000000..c733557 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/hash.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/hash_sequence.bin b/fuzz/seed_corpus/fuzz_tpm/hash_sequence.bin new file mode 100644 index 0000000..661f8f4 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/hash_sequence.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/hash_sha1.bin b/fuzz/seed_corpus/fuzz_tpm/hash_sha1.bin new file mode 100644 index 0000000..b8740ed Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/hash_sha1.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/hash_sha384.bin b/fuzz/seed_corpus/fuzz_tpm/hash_sha384.bin new file mode 100644 index 0000000..7066f5c Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/hash_sha384.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/hash_sha512.bin b/fuzz/seed_corpus/fuzz_tpm/hash_sha512.bin new file mode 100644 index 0000000..3a667e7 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/hash_sha512.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/hierarchy_changeauth.bin b/fuzz/seed_corpus/fuzz_tpm/hierarchy_changeauth.bin new file mode 100644 index 0000000..9cd06e6 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/hierarchy_changeauth.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/hierarchy_control.bin b/fuzz/seed_corpus/fuzz_tpm/hierarchy_control.bin new file mode 100644 index 0000000..9680f90 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/hierarchy_control.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/incremental_self_test.bin b/fuzz/seed_corpus/fuzz_tpm/incremental_self_test.bin new file mode 100644 index 0000000..74965be Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/incremental_self_test.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/load_external.bin b/fuzz/seed_corpus/fuzz_tpm/load_external.bin new file mode 100644 index 0000000..d745d31 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/load_external.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/make_credential.bin b/fuzz/seed_corpus/fuzz_tpm/make_credential.bin new file mode 100644 index 0000000..589ef6a Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/make_credential.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_certify.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_certify.bin new file mode 100644 index 0000000..ac8c2a2 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/mldsa_certify.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_readpublic.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_readpublic.bin new file mode 100644 index 0000000..68be484 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/mldsa_readpublic.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_sign.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_sign.bin new file mode 100644 index 0000000..1ccdc23 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/mldsa_sign.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_sign_digest.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_sign_digest.bin new file mode 100644 index 0000000..a2e6ddb Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/mldsa_sign_digest.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_sign_seq.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_sign_seq.bin new file mode 100644 index 0000000..936bf66 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/mldsa_sign_seq.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_digest.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_digest.bin new file mode 100644 index 0000000..2a5e42e Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_digest.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_seq.bin b/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_seq.bin new file mode 100644 index 0000000..ea192a3 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/mldsa_verify_seq.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_certify.bin b/fuzz/seed_corpus/fuzz_tpm/nv_certify.bin new file mode 100644 index 0000000..86c2a89 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_certify.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_extend.bin b/fuzz/seed_corpus/fuzz_tpm/nv_extend.bin new file mode 100644 index 0000000..40b801d Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_extend.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_globalwritelock.bin b/fuzz/seed_corpus/fuzz_tpm/nv_globalwritelock.bin new file mode 100644 index 0000000..f583eee Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_globalwritelock.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_increment.bin b/fuzz/seed_corpus/fuzz_tpm/nv_increment.bin new file mode 100644 index 0000000..a0ce87b Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_increment.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_read.bin b/fuzz/seed_corpus/fuzz_tpm/nv_read.bin new file mode 100644 index 0000000..0baebbb Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_read.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_read_public.bin b/fuzz/seed_corpus/fuzz_tpm/nv_read_public.bin new file mode 100644 index 0000000..310a0bf Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_read_public.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_read_public2.bin b/fuzz/seed_corpus/fuzz_tpm/nv_read_public2.bin new file mode 100644 index 0000000..0c222e6 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_read_public2.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_readlock.bin b/fuzz/seed_corpus/fuzz_tpm/nv_readlock.bin new file mode 100644 index 0000000..43af978 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_readlock.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_setbits.bin b/fuzz/seed_corpus/fuzz_tpm/nv_setbits.bin new file mode 100644 index 0000000..a88688b Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_setbits.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_undefine.bin b/fuzz/seed_corpus/fuzz_tpm/nv_undefine.bin new file mode 100644 index 0000000..e8889cd Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_undefine.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_write.bin b/fuzz/seed_corpus/fuzz_tpm/nv_write.bin new file mode 100644 index 0000000..2040574 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_write.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/nv_writelock.bin b/fuzz/seed_corpus/fuzz_tpm/nv_writelock.bin new file mode 100644 index 0000000..660d852 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/nv_writelock.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/object_changeauth.bin b/fuzz/seed_corpus/fuzz_tpm/object_changeauth.bin new file mode 100644 index 0000000..f8b89e5 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/object_changeauth.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_allocate.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_allocate.bin new file mode 100644 index 0000000..b9d3061 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/pcr_allocate.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_event.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_event.bin new file mode 100644 index 0000000..8d82779 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/pcr_event.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_extend.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_extend.bin new file mode 100644 index 0000000..81de1f8 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/pcr_extend.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_read.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_read.bin new file mode 100644 index 0000000..90bea0e Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/pcr_read.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_reset.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_reset.bin new file mode 100644 index 0000000..6bcb7cf Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/pcr_reset.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_setauthpolicy.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_setauthpolicy.bin new file mode 100644 index 0000000..5bcc04d Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/pcr_setauthpolicy.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/pcr_setauthvalue.bin b/fuzz/seed_corpus/fuzz_tpm/pcr_setauthvalue.bin new file mode 100644 index 0000000..bae6fec Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/pcr_setauthvalue.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_auth_value.bin b/fuzz/seed_corpus/fuzz_tpm/policy_auth_value.bin new file mode 100644 index 0000000..b2527c7 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_auth_value.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_authorizenv.bin b/fuzz/seed_corpus/fuzz_tpm/policy_authorizenv.bin new file mode 100644 index 0000000..ce8ea37 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_authorizenv.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_capability.bin b/fuzz/seed_corpus/fuzz_tpm/policy_capability.bin new file mode 100644 index 0000000..36a5f50 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_capability.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_command_code.bin b/fuzz/seed_corpus/fuzz_tpm/policy_command_code.bin new file mode 100644 index 0000000..53d9b17 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_command_code.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_countertimer.bin b/fuzz/seed_corpus/fuzz_tpm/policy_countertimer.bin new file mode 100644 index 0000000..34bc631 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_countertimer.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_cphash.bin b/fuzz/seed_corpus/fuzz_tpm/policy_cphash.bin new file mode 100644 index 0000000..8b0225b Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_cphash.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_dupselect.bin b/fuzz/seed_corpus/fuzz_tpm/policy_dupselect.bin new file mode 100644 index 0000000..fec423f Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_dupselect.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_get_digest.bin b/fuzz/seed_corpus/fuzz_tpm/policy_get_digest.bin new file mode 100644 index 0000000..b7e8170 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_get_digest.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_locality.bin b/fuzz/seed_corpus/fuzz_tpm/policy_locality.bin new file mode 100644 index 0000000..2eab873 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_locality.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_namehash.bin b/fuzz/seed_corpus/fuzz_tpm/policy_namehash.bin new file mode 100644 index 0000000..644fd87 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_namehash.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_nv.bin b/fuzz/seed_corpus/fuzz_tpm/policy_nv.bin new file mode 100644 index 0000000..e4b23bb Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_nv.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_nvwritten.bin b/fuzz/seed_corpus/fuzz_tpm/policy_nvwritten.bin new file mode 100644 index 0000000..f8184db Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_nvwritten.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_or.bin b/fuzz/seed_corpus/fuzz_tpm/policy_or.bin new file mode 100644 index 0000000..a17fce1 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_or.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_parameters.bin b/fuzz/seed_corpus/fuzz_tpm/policy_parameters.bin new file mode 100644 index 0000000..63f10e3 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_parameters.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_password.bin b/fuzz/seed_corpus/fuzz_tpm/policy_password.bin new file mode 100644 index 0000000..ef03c7a Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_password.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_pcr.bin b/fuzz/seed_corpus/fuzz_tpm/policy_pcr.bin new file mode 100644 index 0000000..fb823f6 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_pcr.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_physical_presence.bin b/fuzz/seed_corpus/fuzz_tpm/policy_physical_presence.bin new file mode 100644 index 0000000..fe732f7 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_physical_presence.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_secret.bin b/fuzz/seed_corpus/fuzz_tpm/policy_secret.bin new file mode 100644 index 0000000..4ea97e4 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_secret.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_spdm.bin b/fuzz/seed_corpus/fuzz_tpm/policy_spdm.bin new file mode 100644 index 0000000..17012dc Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_spdm.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/policy_template.bin b/fuzz/seed_corpus/fuzz_tpm/policy_template.bin new file mode 100644 index 0000000..0617d1d Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/policy_template.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/quote.bin b/fuzz/seed_corpus/fuzz_tpm/quote.bin new file mode 100644 index 0000000..d76ef8c Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/quote.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/read_clock.bin b/fuzz/seed_corpus/fuzz_tpm/read_clock.bin new file mode 100644 index 0000000..5d3aa7e Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/read_clock.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/read_only_control.bin b/fuzz/seed_corpus/fuzz_tpm/read_only_control.bin new file mode 100644 index 0000000..ab9f8d0 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/read_only_control.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/read_public.bin b/fuzz/seed_corpus/fuzz_tpm/read_public.bin new file mode 100644 index 0000000..ea6c7b9 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/read_public.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/read_public_seeded.bin b/fuzz/seed_corpus/fuzz_tpm/read_public_seeded.bin new file mode 100644 index 0000000..f52de4f Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/read_public_seeded.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_certify.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_certify.bin new file mode 100644 index 0000000..7fae9ba Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_certify.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_decrypt.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_decrypt.bin new file mode 100644 index 0000000..c889df7 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_decrypt.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_decrypt_oaep.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_decrypt_oaep.bin new file mode 100644 index 0000000..531ac95 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_decrypt_oaep.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt.bin new file mode 100644 index 0000000..a20ea22 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_es.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_es.bin new file mode 100644 index 0000000..a31ff9a Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_es.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_oaep.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_oaep.bin new file mode 100644 index 0000000..0711cd2 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_encrypt_oaep.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_makecred.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_makecred.bin new file mode 100644 index 0000000..008819c Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_makecred.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_quote.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_quote.bin new file mode 100644 index 0000000..5a5c260 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_quote.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_readpublic.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_readpublic.bin new file mode 100644 index 0000000..ea6c7b9 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_readpublic.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_sign.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_sign.bin new file mode 100644 index 0000000..b693563 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_sign.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/rsa_sign_pss.bin b/fuzz/seed_corpus/fuzz_tpm/rsa_sign_pss.bin new file mode 100644 index 0000000..abf5857 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/rsa_sign_pss.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/self_test_full.bin b/fuzz/seed_corpus/fuzz_tpm/self_test_full.bin new file mode 100644 index 0000000..df17913 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/self_test_full.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/seq_sha384.bin b/fuzz/seed_corpus/fuzz_tpm/seq_sha384.bin new file mode 100644 index 0000000..941dda0 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/seq_sha384.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/set_algorithm_set.bin b/fuzz/seed_corpus/fuzz_tpm/set_algorithm_set.bin new file mode 100644 index 0000000..cb76d8e Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/set_algorithm_set.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/set_cc_audit.bin b/fuzz/seed_corpus/fuzz_tpm/set_cc_audit.bin new file mode 100644 index 0000000..7b5301d Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/set_cc_audit.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/set_primary_policy.bin b/fuzz/seed_corpus/fuzz_tpm/set_primary_policy.bin new file mode 100644 index 0000000..e7d8eda Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/set_primary_policy.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/shutdown_clear.bin b/fuzz/seed_corpus/fuzz_tpm/shutdown_clear.bin new file mode 100644 index 0000000..418aaa9 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/shutdown_clear.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/sign.bin b/fuzz/seed_corpus/fuzz_tpm/sign.bin new file mode 100644 index 0000000..5ae4c40 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/sign.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/startup_clear.bin b/fuzz/seed_corpus/fuzz_tpm/startup_clear.bin new file mode 100644 index 0000000..359215e Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/startup_clear.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/startup_state.bin b/fuzz/seed_corpus/fuzz_tpm/startup_state.bin new file mode 100644 index 0000000..2476631 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/startup_state.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/stream_startup_getrandom.bin b/fuzz/seed_corpus/fuzz_tpm/stream_startup_getrandom.bin new file mode 100644 index 0000000..67a39f7 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/stream_startup_getrandom.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/test_parms.bin b/fuzz/seed_corpus/fuzz_tpm/test_parms.bin new file mode 100644 index 0000000..5d24621 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/test_parms.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/truncated_command.bin b/fuzz/seed_corpus/fuzz_tpm/truncated_command.bin new file mode 100644 index 0000000..bd67332 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/truncated_command.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/unseal.bin b/fuzz/seed_corpus/fuzz_tpm/unseal.bin new file mode 100644 index 0000000..808e196 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/unseal.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/verify_signature.bin b/fuzz/seed_corpus/fuzz_tpm/verify_signature.bin new file mode 100644 index 0000000..2e6ad58 Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/verify_signature.bin differ diff --git a/fuzz/seed_corpus/fuzz_tpm/zgen_2phase2.bin b/fuzz/seed_corpus/fuzz_tpm/zgen_2phase2.bin new file mode 100644 index 0000000..83ff87d Binary files /dev/null and b/fuzz/seed_corpus/fuzz_tpm/zgen_2phase2.bin differ diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs new file mode 100644 index 0000000..e2d9a75 --- /dev/null +++ b/fuzz/src/lib.rs @@ -0,0 +1,1036 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +//! Shared plumbing for the `ms-tcg-tpm-sys` fuzz targets. +//! +//! # Determinism +//! +//! Fuzzing is only useful if a crashing input can be replayed, which means +//! every input the TPM sees other than the fuzzer's own bytes has to be +//! reproducible. [`FuzzPlatformCallbacks`] therefore backs the entropy source +//! with a fixed-seed PRNG and the monotonic timer with a call counter, both of +//! which are reset at the top of every iteration. +//! +//! # The global TPM instance +//! +//! The underlying C library keeps its state in globals, so only one +//! [`MsTpm185Platform`] can be live at a time, and manufacturing one is far too +//! slow to do per iteration. [`with_tpm`] instead manufactures a single TPM per +//! process and restores a pristine post-manufacture snapshot before each +//! iteration, which is both much faster and gives every iteration the same +//! starting state. +//! +//! Note that the snapshot only covers the state the library knows how to +//! save + restore. If a command leaves state behind that isn't part of a +//! saved-state blob, it will bleed into subsequent iterations - which is itself +//! a bug worth finding, since the same bleed-through would break a live +//! migration. + +#![warn(missing_docs)] + +use arbitrary::Arbitrary; +use ms_tcg_tpm_sys::DynResult; +use ms_tcg_tpm_sys::Error; +use ms_tcg_tpm_sys::InitKind; +use ms_tcg_tpm_sys::Locality; +use ms_tcg_tpm_sys::MsTpm185Platform; +use ms_tcg_tpm_sys::PlatformCallbacks; +use std::cell::RefCell; +use std::sync::Mutex; +use std::sync::OnceLock; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering::Relaxed; +use std::time::Duration; + +/// `MAX_COMMAND_SIZE` from `TpmProfile_Common.h`. +pub const MAX_COMMAND_SIZE: usize = 8192; + +/// `MAX_RESPONSE_SIZE` from `TpmProfile_Common.h`. +/// +/// `ExecuteCommand` marshals directly into the caller's response buffer without +/// bounds checking it against the response it is building, so anything smaller +/// than this is a heap overflow waiting to happen. Every response buffer handed +/// to the TPM by this harness is exactly this size. +pub const MAX_RESPONSE_SIZE: usize = 8192; + +/// Size of the `tag` + `commandSize` + `commandCode` command header, which is +/// also the size of the `tag` + `responseSize` + `responseCode` response +/// header. +pub const HEADER_SIZE: usize = 10; + +/// `TPM_ST_NO_SESSIONS` +pub const TPM_ST_NO_SESSIONS: u16 = 0x8001; +/// `TPM_ST_SESSIONS` +pub const TPM_ST_SESSIONS: u16 = 0x8002; + +/// `TPM_CC_FIRST` from `TpmTypes.h`. The last implemented command code is +/// `TPM_CC_LAST` (`0x1aa`). +pub const TPM_CC_FIRST: u32 = 0x0000011f; + +/// `TPM_RC_SUCCESS` +pub const TPM_RC_SUCCESS: u32 = 0; + +/// `TPM2_Startup(TPM_SU_CLEAR)` +pub const TPM2_STARTUP_CLEAR: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x01, 0x44, 0x00, 0x00, +]; + +/// `TPM2_Startup(TPM_SU_STATE)` +/// +/// A substantially different path through a saved nvmem blob than +/// `TPM_SU_CLEAR`: it restores PCRs, sessions and objects out of the state the +/// blob claims was saved, rather than reinitializing them. +pub const TPM2_STARTUP_STATE: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x01, 0x44, 0x00, 0x01, +]; + +/// `TPM2_Shutdown(TPM_SU_STATE)` +pub const TPM2_SHUTDOWN_STATE: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x01, 0x45, 0x00, 0x01, +]; + +/// `TPM2_SelfTest(fullTest = YES)` +const TPM2_SELF_TEST_FULL: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x01, 0x43, 0x01, +]; + +/// `TPM2_ReadPublic(SEEDED_TRANSIENT)`, used to confirm the seeded state +/// survived a rollback. +const TPM2_READ_PUBLIC_SEEDED: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x01, 0x73, 0x80, 0x00, 0x00, 0x00, +]; + +/// `TPM2_GetRandom(16)`. The DRBG state is carried by the saved blob, so a +/// restore has to rewind it and make this repeat itself. +const TPM2_GET_RANDOM: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x01, 0x7b, 0x00, 0x10, +]; + +/// A `TPM_RS_PW` authorization area holding an empty password. +/// +/// This is what the great majority of commands want, and it is the one thing +/// random bytes will essentially never produce: without it every command that +/// takes an authorization is rejected in `ParseSessionBuffer` before its +/// handler is ever entered. +pub const PASSWORD_SESSION: &[u8] = &[ + 0x40, 0x00, 0x00, 0x09, // sessionHandle = TPM_RS_PW + 0x00, 0x00, // nonce (empty) + 0x00, // sessionAttributes + 0x00, 0x00, // hmac (empty) +]; + +/// Transient handle of the primary key [`FuzzTpm::new`] seeds. +pub const SEEDED_TRANSIENT: u32 = 0x8000_0000; +/// Transient handle of the seeded storage key. +/// +/// The key at [`SEEDED_TRANSIENT`] is an unrestricted signing key, which +/// cannot be a parent. This one is restricted+decrypt, so `TPM2_Create`, +/// `TPM2_Load` and everything else that needs somewhere to put an object have +/// a parent to name. +/// +/// `MAX_LOADED_OBJECTS` is 3 in this profile, so these two are deliberately +/// the only objects seeded: the third slot is left free for the fuzzer to +/// load into, otherwise every `TPM2_Load` would fail with +/// `TPM_RC_OBJECT_MEMORY` and the seeding would cost more coverage than it +/// bought. +pub const SEEDED_STORAGE_PARENT: u32 = 0x8000_0001; +/// Persistent handle the seeded primary key is also evicted to. +pub const SEEDED_PERSISTENT: u32 = 0x8100_0000; +/// Persistent handle of the seeded RSA signing key. +/// +/// Persistent rather than transient so that it costs nothing against +/// `MAX_LOADED_OBJECTS`; without it no RSA code runs at all. +pub const SEEDED_RSA: u32 = 0x8100_0001; +/// Persistent handle of the seeded ML-DSA signing key. +pub const SEEDED_MLDSA: u32 = 0x8100_0002; +/// Ordinary NV index [`FuzzTpm::new`] defines and writes. +pub const SEEDED_NV_INDEX: u32 = 0x0100_0001; +/// NV counter index, for `TPM2_NV_Increment`. +pub const SEEDED_NV_COUNTER: u32 = 0x0100_0002; +/// NV bit field index, for `TPM2_NV_SetBits`. +pub const SEEDED_NV_BITS: u32 = 0x0100_0003; +/// NV extend index, for `TPM2_NV_Extend`. +pub const SEEDED_NV_EXTEND: u32 = 0x0100_0004; +/// NV index carrying the `READ_STCLEAR` / `WRITE_STCLEAR` attributes, for +/// `TPM2_NV_ReadLock` and `TPM2_NV_WriteLock`. +pub const SEEDED_NV_LOCKABLE: u32 = 0x0100_0005; +/// Handle of the seeded HMAC session. +pub const SEEDED_HMAC_SESSION: u32 = 0x0200_0000; +/// Handle of the seeded policy session. +/// +/// Session slots come from one pool regardless of type, so this is slot 1. +pub const SEEDED_POLICY_SESSION: u32 = 0x0300_0001; + +/// Handles a pristine [`FuzzTpm`] actually has live, plus the permanent ones +/// every TPM has. +/// +/// Commands are built around these so that the fuzzer spends its time inside +/// command handlers instead of bouncing off handle validation. Guessing a +/// four byte handle that names a live object is not something a mutator will +/// do on its own. +pub const KNOWN_HANDLES: &[u32] = &[ + 0x4000_0001, // TPM_RH_OWNER + 0x4000_0007, // TPM_RH_NULL + 0x4000_0009, // TPM_RS_PW + 0x4000_000a, // TPM_RH_LOCKOUT + 0x4000_000b, // TPM_RH_ENDORSEMENT + 0x4000_000c, // TPM_RH_PLATFORM + 0x4000_000d, // TPM_RH_PLATFORM_NV + SEEDED_TRANSIENT, + SEEDED_STORAGE_PARENT, + SEEDED_PERSISTENT, + SEEDED_RSA, + SEEDED_MLDSA, + SEEDED_NV_INDEX, + SEEDED_NV_COUNTER, + SEEDED_NV_BITS, + SEEDED_NV_EXTEND, + SEEDED_NV_LOCKABLE, + SEEDED_HMAC_SESSION, + SEEDED_POLICY_SESSION, + 0x0000_0000, // PCR 0 + 0x0000_0007, // PCR 7 +]; + +/// Builds a `TPM2_NV_DefineSpace` under owner authorization, with an empty +/// index authorization value. +#[rustfmt::skip] +const fn nv_define(index: u32, attributes: u32, data_size: u16) -> [u8; 45] { + let i = index.to_be_bytes(); + let a = attributes.to_be_bytes(); + let d = data_size.to_be_bytes(); + [ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x01, 0x2a, + 0x40, 0x00, 0x00, 0x01, // authHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // auth (empty) + 0x00, 0x0e, // publicInfo + i[0], i[1], i[2], i[3], // nvIndex + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + a[0], a[1], a[2], a[3], // attributes + 0x00, 0x00, // authPolicy (empty) + d[0], d[1], // dataSize + ] +} + +/// Commands run once per process, after startup, to give the pristine state +/// something for the fuzzer to work with: a loaded key, a persistent copy of +/// it, a written NV index, and one session of each type. +/// +/// Each entry is a command, and the handle its response is expected to report, +/// so that a drift between these blobs and [`KNOWN_HANDLES`] fails loudly +/// rather than quietly costing coverage. +#[rustfmt::skip] +const SETUP_COMMANDS: &[(&str, &[u8], Option)] = &[ + // TPM2_CreatePrimary(TPM_RH_OWNER, ECC NIST P-256 signing key). ECC rather + // than RSA because this runs on every fuzzing process' startup path. + ("TPM2_CreatePrimary", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x01, 0x31, + 0x40, 0x00, 0x00, 0x01, // primaryHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x16, // inPublic + 0x00, 0x23, // type = TPM_ALG_ECC + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x04, 0x00, 0x72, // fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|sign + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x10, // symmetric = TPM_ALG_NULL + 0x00, 0x10, // scheme = TPM_ALG_NULL + 0x00, 0x03, // curveID = TPM_ECC_NIST_P256 + 0x00, 0x10, // kdf = TPM_ALG_NULL + 0x00, 0x00, 0x00, 0x00, // unique (empty x, y) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) + ], Some(SEEDED_TRANSIENT)), + + // TPM2_EvictControl, to also reach the key through a persistent handle. + ("TPM2_EvictControl", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x01, 0x20, + 0x40, 0x00, 0x00, 0x01, // auth = TPM_RH_OWNER + 0x80, 0x00, 0x00, 0x00, // objectHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x00, // persistentHandle + ], None), + + // TPM2_NV_DefineSpace, an ordinary 32 byte owner/auth read-write index. + ("TPM2_NV_DefineSpace", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x01, 0x2a, + 0x40, 0x00, 0x00, 0x01, // authHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // auth (empty) + 0x00, 0x0e, // publicInfo + 0x01, 0x00, 0x00, 0x01, // nvIndex + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x06, 0x00, 0x06, // OWNERWRITE|AUTHWRITE|OWNERREAD|AUTHREAD + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x20, // dataSize + ], None), + + // TPM2_NV_Write, so the index is written and has contents to read back. + ("TPM2_NV_Write", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x01, 0x37, + 0x40, 0x00, 0x00, 0x01, // authHandle = TPM_RH_OWNER + 0x01, 0x00, 0x00, 0x01, // nvIndex + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, // data + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x00, 0x00, // offset + ], None), + + // TPM2_CreatePrimary again, this time a restricted decryption key, which + // is what an object needs as a parent. AES-128-CFB is mandatory for a + // restricted decrypt key; a null symmetric algorithm is rejected. + ("TPM2_CreatePrimary(storage)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x01, 0x31, + 0x40, 0x00, 0x00, 0x01, // primaryHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x1a, // inPublic + 0x00, 0x23, // type = TPM_ALG_ECC + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x03, 0x00, 0x72, // fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|restricted|decrypt + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x06, 0x00, 0x80, 0x00, 0x43, // symmetric = AES-128-CFB + 0x00, 0x10, // scheme = TPM_ALG_NULL + 0x00, 0x03, // curveID = TPM_ECC_NIST_P256 + 0x00, 0x10, // kdf = TPM_ALG_NULL + 0x00, 0x00, 0x00, 0x00, // unique (empty x, y) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) + ], Some(SEEDED_STORAGE_PARENT)), + + // TPM2_CreatePrimary a third time, RSA rather than ECC. Nothing else here + // reaches the RSA code at all - key generation, the prime sieve and + // Miller-Rabin included - and every RSA operation needs a key to name. + // 1024 bits because this runs on every process startup and exercises the + // same generation path a larger modulus would. + ("TPM2_CreatePrimary(RSA)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x01, 0x31, + 0x40, 0x00, 0x00, 0x01, // primaryHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x16, // inPublic + 0x00, 0x01, // type = TPM_ALG_RSA + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + // sign and decrypt both, so one key generation covers every RSA + // operation rather than only signing. + 0x00, 0x06, 0x00, 0x72, + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x10, // symmetric = TPM_ALG_NULL + 0x00, 0x10, // scheme = TPM_ALG_NULL + 0x04, 0x00, // keyBits = 1024 + 0x00, 0x00, 0x00, 0x00, // exponent = default + 0x00, 0x00, // unique (empty) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) + ], Some(0x8000_0002)), + + // Park the RSA key in NV and give the transient slot back. A persistent + // object costs nothing against `MAX_LOADED_OBJECTS`, so this buys RSA + // coverage without taking the slot the fuzzer needs for TPM2_Load. + ("TPM2_EvictControl(RSA)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x01, 0x20, + 0x40, 0x00, 0x00, 0x01, // auth = TPM_RH_OWNER + 0x80, 0x00, 0x00, 0x02, // objectHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x01, // persistentHandle + ], None), + + ("TPM2_FlushContext(RSA)", &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x01, 0x65, + 0x80, 0x00, 0x00, 0x02, // flushHandle, a parameter rather than a handle + ], None), + + // ML-DSA, same trick again. The streaming signature commands take a + // `TPM2B_SIGNATURE_CTX`, which is an ML-DSA context: with only ECC and RSA + // keys around, none of them are reachable and the whole ML-DSA provider is + // dead code as far as the fuzzer is concerned. + ("TPM2_CreatePrimary(ML-DSA)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x01, 0x31, + 0x40, 0x00, 0x00, 0x01, // primaryHandle = TPM_RH_OWNER + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x0f, // inPublic + 0x00, 0xa1, // type = TPM_ALG_MLDSA + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x04, 0x00, 0x72, // fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|sign + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x01, // parameterSet = ML-DSA-44 + 0x00, // allowExternalMu = NO + 0x00, 0x00, // unique (empty) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) + ], Some(0x8000_0002)), + + ("TPM2_EvictControl(ML-DSA)", &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x01, 0x20, + 0x40, 0x00, 0x00, 0x01, // auth = TPM_RH_OWNER + 0x80, 0x00, 0x00, 0x02, // objectHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x02, // persistentHandle + ], None), + + ("TPM2_FlushContext(ML-DSA)", &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x01, 0x65, + 0x80, 0x00, 0x00, 0x02, + ], None), + + // NV indices of each type, so the commands that only work against a + // particular one have something to name. The type lives in bits 4-7 of + // TPMA_NV, and fixes the size: counters and bit fields are 8 bytes, an + // extend index is one digest. + ("TPM2_NV_DefineSpace(counter)", &nv_define(SEEDED_NV_COUNTER, 0x0006_0016, 8), None), + ("TPM2_NV_DefineSpace(bits)", &nv_define(SEEDED_NV_BITS, 0x0006_0026, 8), None), + ("TPM2_NV_DefineSpace(extend)", &nv_define(SEEDED_NV_EXTEND, 0x0006_0046, 32), None), + // READ_STCLEAR (bit 31) and WRITE_STCLEAR (bit 14), for the lock commands. + ("TPM2_NV_DefineSpace(lockable)", &nv_define(SEEDED_NV_LOCKABLE, 0x8006_4006, 32), None), + + // TPM2_StartAuthSession, unbound and unsalted, one HMAC and one policy. + ("TPM2_StartAuthSession(HMAC)", &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x01, 0x76, + 0x40, 0x00, 0x00, 0x07, // tpmKey = TPM_RH_NULL + 0x40, 0x00, 0x00, 0x07, // bind = TPM_RH_NULL + 0x00, 0x20, // nonceCaller + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, // encryptedSalt (empty) + 0x00, // sessionType = TPM_SE_HMAC + 0x00, 0x10, // symmetric = TPM_ALG_NULL + 0x00, 0x0b, // authHash = TPM_ALG_SHA256 + ], Some(SEEDED_HMAC_SESSION)), + + ("TPM2_StartAuthSession(policy)", &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x01, 0x76, + 0x40, 0x00, 0x00, 0x07, // tpmKey = TPM_RH_NULL + 0x40, 0x00, 0x00, 0x07, // bind = TPM_RH_NULL + 0x00, 0x20, // nonceCaller + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x00, 0x00, // encryptedSalt (empty) + 0x01, // sessionType = TPM_SE_POLICY + 0x00, 0x10, // symmetric = TPM_ALG_NULL + 0x00, 0x0b, // authHash = TPM_ALG_SHA256 + ], Some(SEEDED_POLICY_SESSION)), +]; + +/// `TPM_CC_Load` +const TPM_CC_LOAD: u32 = 0x0000_0157; +/// `TPM_CC_ContextLoad` +const TPM_CC_CONTEXT_LOAD: u32 = 0x0000_0161; +/// `TPM_CC_VerifySignature` +const TPM_CC_VERIFY_SIGNATURE: u32 = 0x0000_0177; + +/// The `TPM2B_DIGEST` that [`TPM2_SIGN_SEEDED`] signs, which is also what the +/// signature it produces has to be verified against. +const SIGNED_DIGEST: &[u8] = &[ + 0x00, 0x20, // size + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, // + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, +]; + +/// `TPM2_Create` of an HMAC key under [`SEEDED_STORAGE_PARENT`]. +/// +/// Run for its output rather than its effect: the private blob it returns is +/// what makes a `TPM2_Load` possible. +#[rustfmt::skip] +const TPM2_CREATE_KEYEDHASH: &[u8] = &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x01, 0x53, + 0x80, 0x00, 0x00, 0x01, // parentHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, // inSensitive (empty) + 0x00, 0x10, // inPublic + 0x00, 0x08, // type = TPM_ALG_KEYEDHASH + 0x00, 0x0b, // nameAlg = TPM_ALG_SHA256 + 0x00, 0x04, 0x00, 0x72, // fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|sign + 0x00, 0x00, // authPolicy (empty) + 0x00, 0x05, 0x00, 0x0b, // scheme = HMAC-SHA256 + 0x00, 0x00, // unique (empty) + 0x00, 0x00, // outsideInfo (empty) + 0x00, 0x00, 0x00, 0x00, // creationPCR (empty) +]; + +/// `TPM2_ContextSave(SEEDED_TRANSIENT)`, run for the context blob it returns. +const TPM2_CONTEXT_SAVE_SEEDED: &[u8] = &[ + 0x80, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x01, 0x62, 0x80, 0x00, 0x00, 0x00, +]; + +/// Commands assembled at setup time out of data the TPM itself produced. +/// +/// `TPM2_Load` wants a private blob that only the TPM can have generated, and +/// `TPM2_ContextLoad` wants a saved context; a mutator will never invent +/// either, so without this those commands - and everything that needs a loaded +/// child object behind them - are unreachable. Built once per process, before +/// the snapshot, so the blobs stay valid for every iteration that rolls back +/// to it. +static CANNED_COMMANDS: OnceLock>> = OnceLock::new(); + +/// The commands described by [`CANNED_COMMANDS`], empty until a [`FuzzTpm`] +/// has been built. +pub fn canned_commands() -> &'static [Vec] { + CANNED_COMMANDS.get().map_or(&[], Vec::as_slice) +} + +/// Well formed commands that the targets which don't have a seed corpus can +/// reach for. +/// +/// `fuzz_nvmem` and `fuzz_restore_state` take `arbitrary`-encoded input, so +/// unlike `fuzz_tpm` there is no seed corpus and no dictionary to spell a real +/// command out with, and raw bytes almost never clear the header. That wastes +/// the interesting half of what those targets do: not whether a blob is +/// rejected, but what the TPM does while running on one that wasn't. +pub fn known_commands() -> &'static [&'static [u8]] { + static KNOWN: OnceLock> = OnceLock::new(); + KNOWN.get_or_init(|| { + let mut commands: Vec<&'static [u8]> = vec![ + TPM2_STARTUP_CLEAR, + TPM2_STARTUP_STATE, + TPM2_SHUTDOWN_STATE, + TPM2_SELF_TEST_FULL, + TPM2_READ_PUBLIC_SEEDED, + TPM2_SIGN_SEEDED, + TPM2_CREATE_KEYEDHASH, + TPM2_CONTEXT_SAVE_SEEDED, + ]; + commands.extend( + SETUP_COMMANDS + .iter() + // Key generation is not something to hand the fuzzer as a + // command it can call in a loop: the RSA primary alone costs + // more than everything else here put together, and the keys + // are seeded already, so replaying these buys nothing and + // costs most of the execution rate. + .filter(|(name, _, _)| !name.starts_with("TPM2_CreatePrimary")) + .map(|(_, command, _)| *command), + ); + commands + }) +} + +/// Splits a `TPM2_Create` response into its `outPrivate` and `outPublic` +/// fields, both keeping the two byte size prefix that `TPM2_Load` wants. +fn split_create_response(response: &[u8]) -> Option<(&[u8], &[u8])> { + // `TPM2_Create` is authorized, so its response is tagged TPM_ST_SESSIONS + // and the parameters start after the header's parameterSize. + let params = response.get(HEADER_SIZE + 4..)?; + + let size_at = |offset: usize| -> Option { + let size = params.get(offset..offset + 2)?; + Some(2 + u16::from_be_bytes(size.try_into().ok()?) as usize) + }; + + let private = size_at(0)?; + let public = size_at(private)?; + Some(( + params.get(..private)?, + params.get(private..private + public)?, + )) +} + +/// Extracts the `TPMT_SIGNATURE` from a `TPM2_Sign` response. +fn sign_response_signature(response: &[u8]) -> Option<&[u8]> { + // Authorized, so tagged TPM_ST_SESSIONS: a parameterSize precedes the + // parameters and bounds them, with the session area following. + let size = response.get(HEADER_SIZE..HEADER_SIZE + 4)?; + let size = u32::from_be_bytes(size.try_into().ok()?) as usize; + response.get(HEADER_SIZE + 4..HEADER_SIZE + 4 + size) +} + +/// `TPM2_Sign(SEEDED_TRANSIENT, ECDSA-SHA256)` over a fixed digest. +/// +/// Used to settle the dictionary attack `daUsed` state, and to confirm the +/// seeded key is actually usable. +#[rustfmt::skip] +const TPM2_SIGN_SEEDED: &[u8] = &[ + 0x80, 0x02, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x01, 0x5d, + 0x80, 0x00, 0x00, 0x00, // keyHandle + 0x00, 0x00, 0x00, 0x09, 0x40, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, // digest + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x00, 0x18, 0x00, 0x0b, // inScheme = ECDSA, SHA256 + 0x80, 0x24, 0x40, 0x00, 0x00, 0x07, 0x00, 0x00, // validation = null ticket +]; + +/// Seed for the entropy PRNG. Any fixed value will do; this one is arbitrary. +const PRNG_SEED: u64 = 0x0123_4567_89ab_cdef; + +static PRNG_STATE: AtomicU64 = AtomicU64::new(PRNG_SEED); +static CLOCK_TICKS: AtomicU64 = AtomicU64::new(0); +static COMMITTED_NVMEM: Mutex> = Mutex::new(Vec::new()); + +/// Rewinds the entropy source and the clock, so that a given sequence of TPM +/// operations always sees the same platform inputs. +/// +/// [`with_tpm`] does this for its callers; targets that build their own +/// [`MsTpm185Platform`] have to call it themselves. +pub fn reset_platform_inputs() { + PRNG_STATE.store(PRNG_SEED, Relaxed); + CLOCK_TICKS.store(0, Relaxed); +} + +/// SplitMix64. +fn next_random() -> u64 { + const GAMMA: u64 = 0x9e37_79b9_7f4a_7c15; + + // `fetch_add` hands back the previous state, so re-apply the step to get + // the state this draw corresponds to. + let mut z = PRNG_STATE.fetch_add(GAMMA, Relaxed).wrapping_add(GAMMA); + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) +} + +/// Deterministic [`PlatformCallbacks`] implementation. +pub struct FuzzPlatformCallbacks; + +impl PlatformCallbacks for FuzzPlatformCallbacks { + fn commit_nv_state(&mut self, state: &[u8]) -> DynResult<()> { + // Stashed (rather than dropped) so that `baseline_nvmem` can hand a + // real, TPM-written nvmem blob to the nvmem fuzz target. + let mut committed = COMMITTED_NVMEM.lock().unwrap(); + committed.clear(); + committed.extend_from_slice(state); + Ok(()) + } + + fn get_crypt_random(&mut self, buf: &mut [u8]) -> DynResult { + // The platform layer runs the FIPS continuous RNG test over + // consecutive 4 byte blocks, so this must not return a constant. + for chunk in buf.chunks_mut(size_of::()) { + let random = next_random().to_le_bytes(); + chunk.copy_from_slice(&random[..chunk.len()]); + } + Ok(buf.len()) + } + + fn monotonic_timer(&mut self) -> Duration { + // Advance by a fixed step per call, so that time-dependent code (clock + // updates, lockout self-heal, etc.) makes progress without making the + // TPM's behavior depend on how fast the fuzzer happens to be running. + Duration::from_millis(CLOCK_TICKS.fetch_add(1, Relaxed)) + } + + fn get_unique_value(&self) -> &'static [u8] { + b"ms-tcg-tpm-sys fuzzing platform unique value" + } +} + +thread_local! { + static TPM: RefCell> = const { RefCell::new(None) }; +} + +/// Hands `f` a TPM that has been rolled back to its pristine post-manufacture +/// state. +/// +/// The TPM is manufactured on first use and reused (via save / restore) by +/// every subsequent call. +pub fn with_tpm(f: impl FnOnce(&mut FuzzTpm) -> R) -> R { + TPM.with(|tpm| { + let mut tpm = tpm.borrow_mut(); + let tpm = match &mut *tpm { + Some(tpm) => { + tpm.rollback(); + tpm + } + slot => slot.insert(FuzzTpm::new()), + }; + f(tpm) + }) +} + +/// An nvmem blob written by a real, freshly manufactured TPM, for fuzz targets +/// that want to mutate a plausible blob rather than start from noise. +/// +/// Must not be called from inside a [`with_tpm`] closure, since it reaches the +/// per-process TPM through [`with_tpm`] itself. +pub fn baseline_nvmem() -> &'static [u8] { + static BASELINE: OnceLock> = OnceLock::new(); + BASELINE.get_or_init(|| { + // Manufacturing the per-process TPM is what writes the blob, so go + // through `with_tpm` rather than standing up a throwaway TPM here. Only + // one TPM can hold the platform singleton at a time, so a throwaway + // would panic for any caller that got here after the first `with_tpm`. + with_tpm(|_| ()); + + let committed = COMMITTED_NVMEM.lock().unwrap().clone(); + assert!( + !committed.is_empty(), + "manufacturing a TPM should have committed an nvmem blob" + ); + committed + }) +} + +/// A manufactured TPM, along with the buffers and pristine snapshot used to +/// drive it. +pub struct FuzzTpm { + platform: MsTpm185Platform, + response: Vec, + snapshot: Vec, +} + +impl FuzzTpm { + /// Manufactures a TPM, starts it up, and snapshots the result. + fn new() -> FuzzTpm { + reset_platform_inputs(); + + let platform = + MsTpm185Platform::initialize(Box::new(FuzzPlatformCallbacks), InitKind::ColdInit) + .expect("manufacturing a TPM should succeed"); + + let mut tpm = FuzzTpm { + platform, + response: vec![0; MAX_RESPONSE_SIZE], + snapshot: Vec::new(), + }; + + // Start the TPM up, and get the (slow) self tests out of the way once + // per process, so that iterations start from a state where the bulk of + // the command surface is reachable. + tpm.execute_expecting_success(TPM2_STARTUP_CLEAR, "TPM2_Startup"); + tpm.execute_expecting_success(TPM2_SELF_TEST_FULL, "TPM2_SelfTest"); + + for (name, command, expected_handle) in SETUP_COMMANDS { + let response = tpm.execute_expecting_success(command, name); + if let Some(expected) = expected_handle { + let handle = response + .get(HEADER_SIZE..HEADER_SIZE + 4) + .map(|h| u32::from_be_bytes(h.try_into().unwrap())); + assert_eq!( + handle, + Some(*expected), + "{name} returned {handle:#x?}, but KNOWN_HANDLES says {expected:#010x}" + ); + } + } + + // The first authorization of a dictionary-attack protected object does + // not run the command: it writes the `daUsed` state to NV and returns + // TPM_RC_RETRY (SessionProcess.c). Settle that here, or every iteration + // would spend its first authorized command on it and never reach the + // handler behind it. The second attempt has to succeed, which also + // confirms the seeded key works. + let _ = tpm.execute_command(&mut TPM2_SIGN_SEEDED.to_vec()); + let signed = tpm + .execute_expecting_success(TPM2_SIGN_SEEDED, "TPM2_Sign") + .to_vec(); + + // Has to happen before the snapshot: a saved context is only valid + // against the state it was saved from, which is the state every + // iteration rolls back to. + let create = tpm.execute_expecting_success(TPM2_CREATE_KEYEDHASH, "TPM2_Create(keyedhash)"); + let context = tpm.execute_expecting_success(TPM2_CONTEXT_SAVE_SEEDED, "TPM2_ContextSave"); + + let mut canned = Vec::new(); + if let Some((private, public)) = split_create_response(&create) { + let mut blobs = private.to_vec(); + blobs.extend_from_slice(public); + canned.push(build_structured_command( + TPM_CC_LOAD, + &[SEEDED_STORAGE_PARENT], + Some(PASSWORD_SESSION), + &blobs, + )); + } + if let Some(context) = context.get(HEADER_SIZE..) { + canned.push(build_structured_command( + TPM_CC_CONTEXT_LOAD, + &[], + None, + context, + )); + } + + // A signature the TPM itself produced. Nothing the fuzzer can invent + // verifies, so without this the whole signature-checking path - and + // the ticket-taking commands behind it - is only ever entered with + // garbage that fails at the first parse. + if let Some(signature) = sign_response_signature(&signed) { + let mut params = SIGNED_DIGEST.to_vec(); + params.extend_from_slice(signature); + canned.push(build_structured_command( + TPM_CC_VERIFY_SIGNATURE, + &[SEEDED_TRANSIENT], + None, + ¶ms, + )); + } + + assert_eq!( + canned.len(), + 3, + "all canned commands should have been built" + ); + let _ = CANNED_COMMANDS.set(canned); + + tpm.snapshot = tpm.platform.save_state(); + + // Everything above is only worth anything if it survives the rollback + // that starts every iteration. `s_objects` and `s_sessions` are part of + // the saved state, so it does - but silently losing the seeded handles + // would cost most of the reachable command surface, so check rather + // than assume. + tpm.rollback(); + tpm.execute_expecting_success(TPM2_READ_PUBLIC_SEEDED, "TPM2_ReadPublic"); + + tpm + } + + /// Rolls the TPM back to the state captured by [`FuzzTpm::new`]. + fn rollback(&mut self) { + reset_platform_inputs(); + self.platform + .restore_state(self.snapshot.clone()) + .expect("restoring the harness' own snapshot should succeed"); + } + + /// The pristine snapshot that every iteration starts from. + pub fn snapshot(&self) -> &[u8] { + &self.snapshot + } + + /// Executes a command through the size-checked entry point, returning the + /// response on success. + pub fn execute_command(&mut self, command: &mut [u8]) -> Result<&[u8], Error> { + let len = self.platform.execute_command(command, &mut self.response)?; + Ok(check_response(&self.response, len)) + } + + /// Executes a command through the unchecked entry point, returning the + /// response. + pub fn execute_command_unchecked(&mut self, command: &mut [u8]) -> &[u8] { + // SAFETY: `self.response` is `MAX_RESPONSE_SIZE` bytes, which is the + // largest response the TPM can produce. The request buffer needs no + // trimming: `ExecuteCommand` bounds every unmarshal by the + // `requestSize` it was handed, and rejects a command whose header + // declares a different size with `TPM_RC_COMMAND_SIZE` (see + // `commandSize != requestSize` in `ExecCommand.c`), so an oversized + // declared size can't walk off the end of `command`. + let len = unsafe { + self.platform + .execute_command_unchecked(command, &mut self.response) + }; + check_response(&self.response, len) + } + + /// Executes a command that is expected to succeed, returning its response + /// and panicking otherwise. + fn execute_expecting_success(&mut self, command: &[u8], name: &str) -> Vec { + let mut command = command.to_vec(); + let response = self + .execute_command(&mut command) + .unwrap_or_else(|e| panic!("{name} should be dispatchable: {e}")); + let code = response_code(response).expect("response should have a header"); + assert_eq!(code, TPM_RC_SUCCESS, "{name} returned {code:#010x}"); + response.to_vec() + } + + /// Simulates a power cycle, optionally swapping in a new nvmem blob. + pub fn reset(&mut self, nvmem: Option<&[u8]>) -> Result<(), Error> { + self.platform.reset(nvmem) + } + + /// Saves the live state into an opaque blob. + pub fn save_state(&self) -> Vec { + self.platform.save_state() + } + + /// Restores previously saved state. + pub fn restore_state(&mut self, state: Vec) -> Result<(), Error> { + self.platform.restore_state(state) + } + + /// Sets the locality subsequent commands run at. + pub fn set_locality(&mut self, locality: Locality) { + self.platform.set_locality(locality); + } + + /// Sets or clears the cancel flag. + pub fn set_cancel_flag(&mut self, enabled: bool) { + self.platform.set_cancel_flag(enabled); + } + + /// Jumps the platform clock forward. + /// + /// The clock otherwise advances a millisecond per read, so anything on a + /// realistic timeout - lockout self healing, ACT countdowns, the periodic + /// clock update that forces an NV write - is unreachable in a fuzz run. + pub fn advance_clock(&mut self, millis: u64) { + CLOCK_TICKS.fetch_add(millis, Relaxed); + } + + /// Runs read-only commands whose answers are fixed by the TPM's state. + /// + /// Comparing these either side of a save / restore says whether the blob + /// really carries everything the TPM's behavior depends on. + pub fn probe(&mut self) -> Vec { + let mut answers = Vec::new(); + + for probe in [TPM2_GET_RANDOM, TPM2_READ_PUBLIC_SEEDED] { + let mut command = probe.to_vec(); + if let Ok(response) = self.execute_command(&mut command) { + answers.extend_from_slice(response); + } + } + + answers + } +} + +/// Validates the invariants every TPM response is expected to uphold, and +/// returns the response. +/// +/// `len` is the response length the TPM reported, and `buffer` the response +/// buffer it was handed. +pub fn check_response(buffer: &[u8], len: usize) -> &[u8] { + assert!( + len <= buffer.len(), + "TPM reported a {len} byte response, which overruns the {} byte response buffer", + buffer.len() + ); + + let response = &buffer[..len]; + + // A response is either empty (the command was cancelled / dropped) or a + // well formed header, whose size field covers the whole response. + if !response.is_empty() { + assert!( + response.len() >= HEADER_SIZE, + "TPM returned a {} byte response, which is too short to hold a header", + response.len() + ); + + let declared = u32::from_be_bytes(response[2..6].try_into().unwrap()) as usize; + assert_eq!( + declared, + response.len(), + "response header declares {declared} bytes, but {} bytes were returned", + response.len() + ); + } + + response +} + +/// Extracts the response code from a response, if it has a header. +pub fn response_code(response: &[u8]) -> Option { + let code = response.get(6..HEADER_SIZE)?; + Some(u32::from_be_bytes(code.try_into().unwrap())) +} + +/// Builds a command with a well formed header wrapped around a fuzzer supplied +/// body (handles, authorization area, and parameters). +/// +/// Random bytes almost never form a valid header, which would leave the fuzzer +/// stuck at the TPM's front door. This gets it past the header parsing so that +/// it can spend its time on the far more interesting per-command unmarshaling +/// code. +pub fn build_command(tag: u16, command_code: u32, body: &[u8]) -> Vec { + let size = (HEADER_SIZE + body.len()) as u32; + + let mut command = Vec::with_capacity(HEADER_SIZE + body.len()); + command.extend_from_slice(&tag.to_be_bytes()); + command.extend_from_slice(&size.to_be_bytes()); + command.extend_from_slice(&command_code.to_be_bytes()); + command.extend_from_slice(body); + command +} + +/// Builds a command out of its structural pieces: handles, an authorization +/// area, and parameters. +/// +/// Getting past a command's front door needs three things a mutator will not +/// invent on its own - a well formed header, handles that name something that +/// exists, and a valid authorization area - and only the parameters are worth +/// spending fuzzing effort on. `auth` picks the tag: `Some` is +/// `TPM_ST_SESSIONS` with `auth` as the authorization area, `None` is +/// `TPM_ST_NO_SESSIONS`. +pub fn build_structured_command( + command_code: u32, + handles: &[u32], + auth: Option<&[u8]>, + params: &[u8], +) -> Vec { + let mut body = Vec::new(); + for handle in handles { + body.extend_from_slice(&handle.to_be_bytes()); + } + + let tag = match auth { + Some(auth) => { + body.extend_from_slice(&(auth.len() as u32).to_be_bytes()); + body.extend_from_slice(auth); + TPM_ST_SESSIONS + } + None => TPM_ST_NO_SESSIONS, + }; + + body.extend_from_slice(params); + build_command(tag, command_code, &body) +} + +/// Splits a byte stream into commands along the boundaries declared by each +/// command's own `commandSize` field. +/// +/// A stream of concatenated TPM commands splits exactly, so a corpus entry can +/// simply be a capture of a real command stream, while a stream with a bogus +/// size field is handed over as-is to exercise the size validation. +pub fn split_commands(data: &[u8], max_commands: usize) -> Vec> { + let mut commands = Vec::new(); + let mut rest = data; + + while !rest.is_empty() && commands.len() < max_commands { + let declared = rest + .get(2..6) + .map(|size| u32::from_be_bytes(size.try_into().unwrap()) as usize); + + let len = match declared { + Some(len) if (HEADER_SIZE..=rest.len()).contains(&len) => len, + _ => rest.len(), + }; + + let (command, tail) = rest.split_at(len); + commands.push(command.to_vec()); + rest = tail; + } + + commands +} + +/// A fuzzer directed splice into an existing blob. +/// +/// Used by the targets that fuzz blobs the TPM itself produced (saved state, +/// nvmem), where starting from random bytes would never get past the blob's +/// header validation. +#[derive(Arbitrary, Debug)] +pub struct Patch { + /// Offset to splice at, taken modulo the length of the blob. + pub offset: u32, + /// Bytes to splice in, truncated to fit. + pub bytes: Vec, +} + +impl Patch { + /// Applies a series of patches to `blob`. + pub fn apply_all(blob: &mut [u8], patches: &[Patch]) { + if blob.is_empty() { + return; + } + + for patch in patches { + let offset = patch.offset as usize % blob.len(); + let len = patch.bytes.len().min(blob.len() - offset); + blob[offset..offset + len].copy_from_slice(&patch.bytes[..len]); + } + } +} diff --git a/fuzz/tpm.dict b/fuzz/tpm.dict new file mode 100644 index 0000000..c3cfa8f --- /dev/null +++ b/fuzz/tpm.dict @@ -0,0 +1,175 @@ +# Copyright (C) Microsoft Corporation. All rights reserved. +# +# libFuzzer dictionary of TPM 2.0 wire-format constants, all big-endian, as +# they appear on the wire. +# +# cargo +nightly fuzz run fuzz_tpm -- -dict=fuzz/tpm.dict + +# Structure tags (TPM_ST) +tag_no_sessions="\x80\x01" +tag_sessions="\x80\x02" +tag_rsp_command="\x80\x04" +tag_attest_certify="\x80\x17" +tag_creation="\x80\x21" +tag_auth_secret="\x80\x23" +tag_hashcheck="\x80\x24" +tag_auth_signed="\x80\x25" + +# Startup / shutdown types (TPM_SU) +su_clear="\x00\x00" +su_state="\x00\x01" + +# Command codes (TPM_CC), the ones most likely to reach interesting state +cc_nv_undefine_space_special="\x00\x00\x01\x1f" +cc_evict_control="\x00\x00\x01\x20" +cc_hierarchy_control="\x00\x00\x01\x21" +cc_nv_undefine_space="\x00\x00\x01\x22" +cc_clear="\x00\x00\x01\x26" +cc_clear_control="\x00\x00\x01\x27" +cc_clock_set="\x00\x00\x01\x28" +cc_hierarchy_change_auth="\x00\x00\x01\x29" +cc_nv_define_space="\x00\x00\x01\x2a" +cc_pcr_allocate="\x00\x00\x01\x2b" +cc_set_primary_policy="\x00\x00\x01\x2e" +cc_change_pps="\x00\x00\x01\x2f" +cc_change_eps="\x00\x00\x01\x30" +cc_nv_write="\x00\x00\x01\x37" +cc_nv_increment="\x00\x00\x01\x34" +cc_nv_extend="\x00\x00\x01\x36" +cc_dictionary_attack_lock_reset="\x00\x00\x01\x39" +cc_dictionary_attack_parameters="\x00\x00\x01\x3a" +cc_nv_change_auth="\x00\x00\x01\x3b" +cc_pcr_event="\x00\x00\x01\x3c" +cc_pcr_reset="\x00\x00\x01\x3d" +cc_sequence_complete="\x00\x00\x01\x3e" +cc_incremental_self_test="\x00\x00\x01\x42" +cc_self_test="\x00\x00\x01\x43" +cc_startup="\x00\x00\x01\x44" +cc_shutdown="\x00\x00\x01\x45" +cc_stir_random="\x00\x00\x01\x46" +cc_activate_credential="\x00\x00\x01\x47" +cc_certify="\x00\x00\x01\x48" +cc_policy_nv="\x00\x00\x01\x49" +cc_create="\x00\x00\x01\x53" +cc_import="\x00\x00\x01\x56" +cc_load="\x00\x00\x01\x57" +cc_quote="\x00\x00\x01\x58" +cc_rsa_decrypt="\x00\x00\x01\x59" +cc_sequence_update="\x00\x00\x01\x5c" +cc_sign="\x00\x00\x01\x5d" +cc_unseal="\x00\x00\x01\x5e" +cc_context_load="\x00\x00\x01\x61" +cc_context_save="\x00\x00\x01\x62" +cc_flush_context="\x00\x00\x01\x65" +cc_load_external="\x00\x00\x01\x67" +cc_make_credential="\x00\x00\x01\x68" +cc_nv_read_public="\x00\x00\x01\x69" +cc_policy_authorize="\x00\x00\x01\x6a" +cc_policy_secret="\x00\x00\x01\x51" +cc_create_primary="\x00\x00\x01\x31" +cc_nv_read="\x00\x00\x01\x4e" +cc_read_public="\x00\x00\x01\x73" +cc_start_auth_session="\x00\x00\x01\x76" +cc_verify_signature="\x00\x00\x01\x77" +cc_ecc_parameters="\x00\x00\x01\x78" +cc_get_capability="\x00\x00\x01\x7a" +cc_get_random="\x00\x00\x01\x7b" +cc_get_test_result="\x00\x00\x01\x7c" +cc_hash="\x00\x00\x01\x7d" +cc_pcr_read="\x00\x00\x01\x7e" +cc_policy_pcr="\x00\x00\x01\x7f" +cc_policy_restart="\x00\x00\x01\x80" +cc_read_clock="\x00\x00\x01\x81" +cc_pcr_extend="\x00\x00\x01\x82" +cc_event_sequence_complete="\x00\x00\x01\x85" +cc_hash_sequence_start="\x00\x00\x01\x86" +cc_policy_get_digest="\x00\x00\x01\x89" +cc_test_parms="\x00\x00\x01\x8a" +cc_policy_password="\x00\x00\x01\x8c" +cc_create_loaded="\x00\x00\x01\x91" +cc_policy_authorize_nv="\x00\x00\x01\x92" +cc_ac_send="\x00\x00\x01\x94" +cc_certify_x509="\x00\x00\x01\x97" +cc_last="\x00\x00\x01\xaa" + +# Permanent handles (TPM_RH) and session handles +rh_owner="\x40\x00\x00\x01" +rh_null="\x40\x00\x00\x07" +rh_password="\x40\x00\x00\x09" +rh_lockout="\x40\x00\x00\x0a" +rh_endorsement="\x40\x00\x00\x0b" +rh_platform="\x40\x00\x00\x0c" +rh_platform_nv="\x40\x00\x00\x0d" +handle_pcr0="\x00\x00\x00\x00" +handle_hmac_session="\x02\x00\x00\x00" +handle_policy_session="\x03\x00\x00\x00" +handle_transient="\x80\x00\x00\x00" +handle_persistent="\x81\x00\x00\x00" +handle_nv_index="\x01\x00\x00\x01" + +# Handles the harness seeds into the pristine state, so these name something +# that actually exists. See SETUP_COMMANDS in src/lib.rs. +seeded_transient="\x80\x00\x00\x00" +seeded_persistent="\x81\x00\x00\x00" +seeded_nv_index="\x01\x00\x00\x01" +seeded_hmac_session="\x02\x00\x00\x00" +seeded_policy_session="\x03\x00\x00\x01" + +# Authorization areas. A command tagged TPM_ST_SESSIONS is rejected before its +# handler runs unless it carries a well formed one of these, and a mutator will +# not produce one on its own. +auth_pw_session="\x40\x00\x00\x09\x00\x00\x00\x00\x00" +auth_pw_empty="\x00\x00\x00\x09\x40\x00\x00\x09\x00\x00\x00\x00\x00" +auth_pw_empty_x2="\x00\x00\x00\x12\x40\x00\x00\x09\x00\x00\x00\x00\x00\x40\x00\x00\x09\x00\x00\x00\x00\x00" +auth_pw_continue="\x00\x00\x00\x09\x40\x00\x00\x09\x00\x00\x01\x00\x00" + +# Algorithm identifiers (TPM_ALG) +alg_error="\x00\x00" +alg_rsa="\x00\x01" +alg_sha1="\x00\x04" +alg_hmac="\x00\x05" +alg_aes="\x00\x06" +alg_keyedhash="\x00\x08" +alg_xor="\x00\x0a" +alg_sha256="\x00\x0b" +alg_sha384="\x00\x0c" +alg_sha512="\x00\x0d" +alg_null="\x00\x10" +alg_sm3_256="\x00\x12" +alg_sm4="\x00\x13" +alg_rsassa="\x00\x14" +alg_rsaes="\x00\x15" +alg_rsapss="\x00\x16" +alg_oaep="\x00\x17" +alg_ecdsa="\x00\x18" +alg_ecdh="\x00\x19" +alg_symcipher="\x00\x25" +alg_camellia="\x00\x26" +alg_sha3_256="\x00\x27" +alg_ctr="\x00\x40" +alg_ofb="\x00\x41" +alg_cbc="\x00\x42" +alg_cfb="\x00\x43" +alg_ecb="\x00\x44" + +# ECC curves (TPM_ECC_CURVE) +curve_nist_p256="\x00\x03" +curve_nist_p384="\x00\x04" +curve_bn_p256="\x00\x10" + +# Capabilities (TPM_CAP) +cap_algs="\x00\x00\x00\x00" +cap_handles="\x00\x00\x00\x01" +cap_commands="\x00\x00\x00\x02" +cap_pcrs="\x00\x00\x00\x05" +cap_tpm_properties="\x00\x00\x00\x06" +cap_pcr_properties="\x00\x00\x00\x07" +cap_ecc_curves="\x00\x00\x00\x08" +cap_vendor_property="\x00\x00\x01\x00" + +# Sizes that tend to sit at the edge of a buffer +size_zero="\x00\x00" +size_max_digest="\x00\x40" +size_max_buffer="\x04\x00" +size_max_command="\x00\x00\x20\x00" +size_overflow="\xff\xff\xff\xff"