diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..5a0d5e4
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto eol=lf
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..5b90688
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,779 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bitfield"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "cc"
+version = "1.2.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "windows-link",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flexi_logger"
+version = "0.31.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aea7feddba9b4e83022270d49a58d4a1b3fdad04b34f78cf1ce471f698e42672"
+dependencies = [
+ "chrono",
+ "log",
+ "nu-ansi-term",
+ "regex",
+ "thiserror",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hostname-validator"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "js-sys"
+version = "0.3.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.182"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "mbox"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d142aeadbc4e8c679fc6d93fbe7efe1c021fa7d80629e615915b519e3bc6de"
+dependencies = [
+ "libc",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "oid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "picky-asn1"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "295eea0f33c16be21e2a98b908fdd4d73c04dd48c8480991b76dbcf0cb58b212"
+dependencies = [
+ "oid",
+ "serde",
+ "serde_bytes",
+]
+
+[[package]]
+name = "picky-asn1-der"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5df7873a9e36d42dadb393bea5e211fe83d793c172afad5fb4ec846ec582793f"
+dependencies = [
+ "picky-asn1",
+ "serde",
+ "serde_bytes",
+]
+
+[[package]]
+name = "picky-asn1-x509"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c5f20f71a68499ff32310f418a6fad8816eac1a2859ed3f0c5c741389dd6208"
+dependencies = [
+ "base64",
+ "oid",
+ "picky-asn1",
+ "picky-asn1-der",
+ "serde",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tpm2-cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "flexi_logger",
+ "hex",
+ "log",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tss-esapi",
+]
+
+[[package]]
+name = "tss-esapi"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ea9ccde878b029392ac97b5be1f470173d06ea41d18ad0bb3c92794c16a0f2"
+dependencies = [
+ "bitfield",
+ "enumflags2",
+ "getrandom",
+ "hostname-validator",
+ "log",
+ "mbox",
+ "num-derive",
+ "num-traits",
+ "oid",
+ "picky-asn1",
+ "picky-asn1-x509",
+ "regex",
+ "serde",
+ "tss-esapi-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "tss-esapi-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "535cd192581c2ec4d5f82e670b1d3fbba6a23ccce8c85de387642051d7cad5b5"
+dependencies = [
+ "pkg-config",
+ "target-lexicon",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..8af900b
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "tpm2-cli"
+version = "0.1.0"
+edition = "2024"
+rust-version = "1.90.0"
+license = "Apache-2.0"
+readme = "README.md"
+exclude = [".gitignore", ".gitattributes", ".github/*"]
+
+[[bin]]
+name = "tpm2"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1.0.102"
+clap = { version = "4.5.60", features = ["derive", "env"] }
+flexi_logger = "0.31.8"
+hex = "0.4.3"
+log = "0.4.29"
+serde = { version = "1.0.228", features = ["derive"] }
+serde_json = "1.0.149"
+thiserror = "2.0.18"
+tss-esapi = "7.6.0"
diff --git a/README.md b/README.md
index 645424a..6559cc0 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,84 @@
-# rust-tpm2-cli
\ No newline at end of file
+# rust-tpm2-cli
+
+
+[](https://doc.rust-lang.org/stable/releases.html#version-1900-2025-09-18)
+[](https://opensource.org/licenses/Apache-2.0)
+
+
+
+
+
+**rust-tpm2-cli** is a suite of Rust-based command-line tools for interacting with Trusted Platform Module (TPM) 2.0 devices.
+
+> [!NOTE]
+> This project is heavily inspired by [tpm2-tools](https://github.com/tpm2-software/tpm2-tools) and gratefully acknowledges the work of its contributors.
+> The (sub)command names and CLI argument names are designed to be largely compatible with those of `tpm2-tools`. See the [Comparison with tpm2-tools](#comparison-with-tpm2-tools) section for details.
+
+## Quick start
+
+### Dependencies
+
+```bash
+# Native dependencies
+sudo apt update
+sudo apt install -y build-essential clang libtss2-dev pkg-config
+
+# Rust toolchain
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+source "$HOME/.cargo/env"
+```
+
+### Build
+
+```bash
+git clone https://github.com/hyperfinitism/rust-tpm2-cli
+cd rust-tpm2-cli
+cargo build -r
+# => ./target/release/tpm2
+```
+
+### TPM device settings
+
+```bash
+# Add the current user to the tss group to grant TPM access permission
+sudo usermod "$USER" -aG tss
+newgrp tss
+
+# Check TPM device path, e.g., /dev/tpm0
+ls -l /dev/tpm*
+
+# Set TPM device path
+export TPM2TOOLS_TCTI="device:/dev/tpm0"
+```
+
+## Usage
+
+Under construction.
+
+## Comparison with tpm2-tools
+
+While broadly following the `tpm2-tools` APIs, `rust-tpm2-cli` is a from-scratch implementation. The key differences are:
+
+| | `tpm2-tools` | `rust-tpm2-cli` |
+|---|---|---|
+| **Language** | C | Rust |
+| **TPM Software Stack (TSS)** | [tpm2-tss](https://github.com/tpm2-software/tpm2-tss) | [rust-tss-esapi](https://github.com/parallaxsecond/rust-tss-esapi) |
+| **Binary size order**\* | sub MB | several MB |
+
+> \* The size of the binary depends on both the version and the build environment. This comparison uses `tpm2-tools` v5.7 and `rust-tpm2-cli` latest (at the time of writing this document).
+
+`tpm2-tools` has a significantly smaller binary footprint, making it a better fit for resource-constrained environments such as IoT devices with limited storage or memory. It also benefits from a long track record and broad backward compatibility.
+
+`rust-tpm2-cli` trades binary size for Rust's memory safety guarantees, rich type system, and expressive language features, which reduce entire classes of bugs at compile time.
+
+### API refinements
+
+`rust-tpm2-cli` introduces a number of deliberate improvements (breaking changes) for clarity and consistency:
+
+- **Explicit handle vs. context arguments**: Where `tpm2-tools` accepts either a TPM handle (hex string) or a context file path through a single argument, `rust-tpm2-cli` provides dedicated arguments for each, making the type of the input unambiguous.
+
+- **Extended context file support**: Some arguments in `tpm2-tools` accept only a TPM handle in hex string form without an apparent reason. `rust-tpm2-cli` removes this restriction and allows a context file to be specified wherever it is semantically appropriate.
+
+- **Subcommand splitting**: Subcommands that conflate distinct operations have been separated. For example, the `encryptdecrypt` subcommand of `tpm2-tools` is split into two dedicated subcommands `encrypt` and `decrypt`. (At the moment, `encryptdecrypt` is kept for compatibility.)
+
+- **Flexible logging**: rust-tpm2-cli uses [flexi_logger](https://github.com/emabee/flexi_logger) for flexible logging control via CLI flags. Logs can also be written to a file.
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..2a11804
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,254 @@
+use clap::{Parser, Subcommand};
+use flexi_logger::LevelFilter;
+use std::path::PathBuf;
+
+use crate::cmd;
+
+#[derive(Parser)]
+#[command(name = "tpm2", version, about = "Rust-based CLI tools for TPM 2.0")]
+pub struct Cli {
+ #[command(flatten)]
+ pub global: GlobalOpts,
+
+ #[command(subcommand)]
+ pub command: Commands,
+}
+
+#[derive(Parser)]
+pub struct GlobalOpts {
+ /// TCTI configuration (e.g. device:/dev/tpm0, mssim:host=localhost,port=2321)
+ #[arg(short = 'T', long = "tcti", env = "TPM2TOOLS_TCTI")]
+ pub tcti: Option,
+
+ /// Enable errata fixups
+ #[arg(short = 'Z', long = "enable-errata")]
+ pub enable_errata: bool,
+
+ /// Verbosity level (Trace, Debug, Info, Warn, Error, Off)
+ #[arg(short = 'v', long, default_value = "Info")]
+ pub verbosity: LevelFilter,
+
+ /// Log file path (default: None)
+ #[arg(short = 'l', long = "log-file")]
+ pub log_file: Option,
+}
+
+macro_rules! tpm2_commands {
+ ( $( $(#[$meta:meta])* $variant:ident($path:path) ),* $(,)? ) => {
+ #[derive(Subcommand)]
+ pub enum Commands {
+ $( $(#[$meta])* $variant($path), )*
+ }
+
+ impl Commands {
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ match self {
+ $( Self::$variant(c) => c.execute(global), )*
+ }
+ }
+ }
+ };
+}
+
+tpm2_commands! {
+ /// Activate a credential and recover the secret
+ Activatecredential(cmd::activatecredential::ActivateCredentialCmd),
+ /// Certify that an object is loaded in the TPM
+ Certify(cmd::certify::CertifyCmd),
+ /// Certify creation data for an object
+ Certifycreation(cmd::certifycreation::CertifyCreationCmd),
+ /// Change auth value for an object or hierarchy
+ Changeauth(cmd::changeauth::ChangeAuthCmd),
+ /// Change the endorsement primary seed
+ Changeeps(cmd::changeeps::ChangeEpsCmd),
+ /// Change the platform primary seed
+ Changepps(cmd::changepps::ChangePpsCmd),
+ /// Verify a TPM quote
+ Checkquote(cmd::checkquote::CheckQuoteCmd),
+ /// Clear the TPM
+ Clear(cmd::clear::ClearCmd),
+ /// Enable or disable TPM2_Clear
+ Clearcontrol(cmd::clearcontrol::ClearControlCmd),
+ /// Adjust the clock rate
+ Clockrateadjust(cmd::clockrateadjust::ClockRateAdjustCmd),
+ /// Perform the first part of an ECC anonymous signing operation
+ Commit(cmd::commit::CommitCmd),
+ /// Create a child key
+ Create(cmd::create::CreateCmd),
+ /// Create an attestation key (AK) under an EK
+ Createak(cmd::createak::CreateAkCmd),
+ /// Create a TCG-compliant endorsement key (EK)
+ Createek(cmd::createek::CreateEkCmd),
+ /// Create a policy from a trial session
+ Createpolicy(cmd::createpolicy::CreatePolicyCmd),
+ /// Create a primary key
+ Createprimary(cmd::createprimary::CreatePrimaryCmd),
+ /// Decrypt data with a symmetric TPM key
+ Decrypt(cmd::decrypt::DecryptCmd),
+ /// Reset or configure dictionary attack lockout
+ Dictionarylockout(cmd::dictionarylockout::DictionaryLockoutCmd),
+ /// Duplicate an object for use on another TPM
+ Duplicate(cmd::duplicate::DuplicateCmd),
+ /// Generate an ephemeral ECDH key pair and compute a shared secret
+ Ecdhkeygen(cmd::ecdhkeygen::EcdhKeygenCmd),
+ /// Perform ECDH key exchange Z-point generation
+ Ecdhzgen(cmd::ecdhzgen::EcdhZgenCmd),
+ /// Create an ephemeral key for two-phase key exchange
+ Ecephemeral(cmd::ecephemeral::EcEphemeralCmd),
+ /// Encrypt data with a symmetric TPM key
+ Encrypt(cmd::encrypt::EncryptCmd),
+ /// Encrypt or decrypt data with a symmetric key
+ Encryptdecrypt(cmd::encryptdecrypt::EncryptDecryptCmd),
+ /// Parse and display a binary TPM2 event log
+ Eventlog(cmd::eventlog::EventLogCmd),
+ /// Make a transient object persistent (or evict a persistent object)
+ Evictcontrol(cmd::evictcontrol::EvictControlCmd),
+ /// Flush a context (handle) from the TPM
+ Flushcontext(cmd::flushcontext::FlushContextCmd),
+ /// Query TPM capabilities and properties
+ Getcap(cmd::getcap::GetCapCmd),
+ /// Get the command audit digest
+ Getcommandauditdigest(cmd::getcommandauditdigest::GetCommandAuditDigestCmd),
+ /// Get ECC curve parameters
+ Geteccparameters(cmd::geteccparameters::GetEccParametersCmd),
+ /// Retrieve the EK certificate from TPM NV storage
+ Getekcertificate(cmd::getekcertificate::GetEkCertificateCmd),
+ /// Get random bytes from the TPM
+ Getrandom(cmd::getrandom::GetRandomCmd),
+ /// Get the session audit digest
+ Getsessionauditdigest(cmd::getsessionauditdigest::GetSessionAuditDigestCmd),
+ /// Get the TPM self-test result
+ Gettestresult(cmd::gettestresult::GetTestResultCmd),
+ /// Get a signed timestamp from the TPM
+ Gettime(cmd::gettime::GetTimeCmd),
+ /// Compute a hash using the TPM
+ Hash(cmd::hash::HashCmd),
+ /// Enable or disable TPM hierarchies
+ Hierarchycontrol(cmd::hierarchycontrol::HierarchyControlCmd),
+ /// Compute HMAC using a TPM key
+ Hmac(cmd::tpmhmac::HmacCmd),
+ /// Import a wrapped key into the TPM
+ Import(cmd::import::ImportCmd),
+ /// Run incremental self-test on specified algorithms
+ Incrementalselftest(cmd::incrementalselftest::IncrementalSelfTestCmd),
+ /// Load a key into the TPM
+ Load(cmd::load::LoadCmd),
+ /// Load an external key into the TPM
+ Loadexternal(cmd::loadexternal::LoadExternalCmd),
+ /// Create a credential blob for a TPM key
+ Makecredential(cmd::makecredential::MakeCredentialCmd),
+ /// Certify the contents of an NV index
+ Nvcertify(cmd::nvcertify::NvCertifyCmd),
+ /// Define an NV index
+ Nvdefine(cmd::nvdefine::NvDefineCmd),
+ /// Extend data into an NV index
+ Nvextend(cmd::nvextend::NvExtendCmd),
+ /// Increment an NV counter
+ Nvincrement(cmd::nvincrement::NvIncrementCmd),
+ /// Read data from an NV index
+ Nvread(cmd::nvread::NvReadCmd),
+ /// Lock an NV index for reading
+ Nvreadlock(cmd::nvreadlock::NvReadLockCmd),
+ /// Read the public area of an NV index
+ Nvreadpublic(cmd::nvreadpublic::NvReadPublicCmd),
+ /// Set bits in an NV bit field
+ Nvsetbits(cmd::nvsetbits::NvSetBitsCmd),
+ /// Remove an NV index
+ Nvundefine(cmd::nvundefine::NvUndefineCmd),
+ /// Write data to an NV index
+ Nvwrite(cmd::nvwrite::NvWriteCmd),
+ /// Lock an NV index for writing
+ Nvwritelock(cmd::nvwritelock::NvWriteLockCmd),
+ /// Allocate PCR banks
+ Pcrallocate(cmd::pcrallocate::PcrAllocateCmd),
+ /// Extend a PCR with event data
+ Pcrevent(cmd::pcrevent::PcrEventCmd),
+ /// Extend a PCR with a digest
+ Pcrextend(cmd::pcrextend::PcrExtendCmd),
+ /// Read PCR values
+ Pcrread(cmd::pcrread::PcrReadCmd),
+ /// Reset a PCR register
+ Pcrreset(cmd::pcrreset::PcrResetCmd),
+ /// Extend a policy with PolicyAuthorize
+ Policyauthorize(cmd::policyauthorize::PolicyAuthorizeCmd),
+ /// Extend a policy using NV-stored policy
+ Policyauthorizenv(cmd::policyauthorizenv::PolicyAuthorizeNvCmd),
+ /// Extend a policy with PolicyAuthValue
+ Policyauthvalue(cmd::policyauthvalue::PolicyAuthValueCmd),
+ /// Extend a policy with PolicyCommandCode
+ Policycommandcode(cmd::policycommandcode::PolicyCommandCodeCmd),
+ /// Extend a policy with PolicyCounterTimer
+ Policycountertimer(cmd::policycountertimer::PolicyCounterTimerCmd),
+ /// Extend a policy with PolicyCpHash
+ Policycphash(cmd::policycphash::PolicyCpHashCmd),
+ /// Extend a policy with PolicyDuplicationSelect
+ Policyduplicationselect(cmd::policyduplicationselect::PolicyDuplicationSelectCmd),
+ /// Extend a policy with PolicyLocality
+ Policylocality(cmd::policylocality::PolicyLocalityCmd),
+ /// Extend a policy with PolicyNameHash
+ Policynamehash(cmd::policynamehash::PolicyNameHashCmd),
+ /// Extend a policy bound to NV index contents
+ Policynv(cmd::policynv::PolicyNvCmd),
+ /// Extend a policy with PolicyNvWritten
+ Policynvwritten(cmd::policynvwritten::PolicyNvWrittenCmd),
+ /// Extend a policy with PolicyOR
+ Policyor(cmd::policyor::PolicyOrCmd),
+ /// Extend a policy with PolicyPassword
+ Policypassword(cmd::policypassword::PolicyPasswordCmd),
+ /// Extend a policy with PolicyPCR
+ Policypcr(cmd::policypcr::PolicyPcrCmd),
+ /// Reset a policy session
+ Policyrestart(cmd::policyrestart::PolicyRestartCmd),
+ /// Extend a policy session with PolicySecret
+ Policysecret(cmd::policysecret::PolicySecretCmd),
+ /// Extend a policy with PolicySigned
+ Policysigned(cmd::policysigned::PolicySignedCmd),
+ /// Extend a policy with PolicyTemplate
+ Policytemplate(cmd::policytemplate::PolicyTemplateCmd),
+ /// Extend a policy with a ticket
+ Policyticket(cmd::policyticket::PolicyTicketCmd),
+ /// Decode and display a TPM data structure
+ Print(cmd::print::PrintCmd),
+ /// Generate a TPM quote
+ Quote(cmd::quote::QuoteCmd),
+ /// Decode a TPM response code
+ Rcdecode(cmd::rcdecode::RcDecodeCmd),
+ /// Read the TPM clock
+ Readclock(cmd::readclock::ReadClockCmd),
+ /// Read the public area of a loaded object
+ Readpublic(cmd::readpublic::ReadPublicCmd),
+ /// RSA decrypt data
+ Rsadecrypt(cmd::rsadecrypt::RsaDecryptCmd),
+ /// RSA encrypt data
+ Rsaencrypt(cmd::rsaencrypt::RsaEncryptCmd),
+ /// Run the TPM self-test
+ Selftest(cmd::selftest::SelfTestCmd),
+ /// Send a raw TPM command
+ Send(cmd::send::SendCmd),
+ /// Configure session attributes
+ Sessionconfig(cmd::sessionconfig::SessionConfigCmd),
+ /// Set the TPM clock
+ Setclock(cmd::setclock::SetClockCmd),
+ /// Set or clear command audit status
+ Setcommandauditstatus(cmd::setcommandauditstatus::SetCommandAuditStatusCmd),
+ /// Set the primary policy for a hierarchy
+ Setprimarypolicy(cmd::setprimarypolicy::SetPrimaryPolicyCmd),
+ /// Send TPM2_Shutdown
+ Shutdown(cmd::shutdown::ShutdownCmd),
+ /// Sign data with a TPM key
+ Sign(cmd::sign::SignCmd),
+ /// Start a TPM authorization session
+ Startauthsession(cmd::startauthsession::StartAuthSessionCmd),
+ /// Send TPM2_Startup
+ Startup(cmd::startup::StartupCmd),
+ /// Stir random data into the TPM RNG
+ Stirrandom(cmd::stirrandom::StirRandomCmd),
+ /// Test if algorithm parameters are supported
+ Testparms(cmd::testparms::TestParmsCmd),
+ /// Unseal data from a sealed object
+ Unseal(cmd::unseal::UnsealCmd),
+ /// Verify a signature using a TPM key
+ Verifysignature(cmd::verifysignature::VerifySignatureCmd),
+ /// Perform two-phase ECDH key exchange
+ Zgen2phase(cmd::zgen2phase::Zgen2PhaseCmd),
+}
diff --git a/src/cmd/activatecredential.rs b/src/cmd/activatecredential.rs
new file mode 100644
index 0000000..fae2605
--- /dev/null
+++ b/src/cmd/activatecredential.rs
@@ -0,0 +1,194 @@
+use std::path::PathBuf;
+
+use anyhow::Context;
+use clap::Parser;
+use log::info;
+use tss_esapi::constants::SessionType;
+use tss_esapi::handles::ObjectHandle;
+use tss_esapi::interface_types::resource_handles::HierarchyAuth;
+use tss_esapi::interface_types::session_handles::AuthSession;
+use tss_esapi::structures::{EncryptedSecret, IdObject};
+
+use crate::cli::GlobalOpts;
+use crate::context::create_context;
+use crate::handle::{ContextSource, load_key_from_source};
+use crate::parse;
+use crate::parse::parse_hex_u32;
+use crate::session::{flush_policy_session, load_session_from_file, start_ek_policy_session};
+
+/// Activate a credential associated with a TPM object.
+///
+/// Wraps `TPM2_ActivateCredential`: given a credential blob produced by
+/// `tpm2 makecredential`, decrypts it using the credential key (typically
+/// an EK) and verifies the binding to the credentialed key (typically an AK).
+#[derive(Parser)]
+pub struct ActivateCredentialCmd {
+ /// Credentialed key context file — the object the credential is bound to (AK)
+ #[arg(
+ short = 'c',
+ long = "credentialedkey-context",
+ conflicts_with = "credentialed_context_handle"
+ )]
+ pub credentialed_context: Option,
+
+ /// Credentialed key handle (hex, e.g. 0x81000001)
+ #[arg(long = "credentialedkey-context-handle", value_parser = parse_hex_u32, conflicts_with = "credentialed_context")]
+ pub credentialed_context_handle: Option,
+
+ /// Credential key context file — the key used to decrypt the seed (EK)
+ #[arg(
+ short = 'C',
+ long = "credentialkey-context",
+ conflicts_with = "credential_key_context_handle"
+ )]
+ pub credential_key_context: Option,
+
+ /// Credential key handle (hex, e.g. 0x81000001)
+ #[arg(long = "credentialkey-context-handle", value_parser = parse_hex_u32, conflicts_with = "credential_key_context")]
+ pub credential_key_context_handle: Option,
+
+ /// Auth value for the credentialed key
+ #[arg(short = 'p', long = "credentialedkey-auth")]
+ pub credentialed_auth: Option,
+
+ /// Auth for the credential key (EK).
+ ///
+ /// Use `session:` to supply an already-satisfied policy session,
+ /// or a plain password / `hex:` / `file:` value for the endorsement
+ /// hierarchy auth used when starting an internal EK policy session.
+ #[arg(short = 'P', long = "credentialkey-auth")]
+ pub credential_key_auth: Option,
+
+ /// Input credential blob file (from tpm2 makecredential)
+ #[arg(short = 'i', long = "credential-blob")]
+ pub credential_blob: PathBuf,
+
+ /// Output file for the decrypted credential secret
+ #[arg(short = 'o', long = "certinfo-data")]
+ pub certinfo_data: PathBuf,
+}
+
+impl ActivateCredentialCmd {
+ fn credentialed_context_source(&self) -> anyhow::Result {
+ match (&self.credentialed_context, self.credentialed_context_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!(
+ "exactly one of --credentialedkey-context or --credentialedkey-context-handle must be provided"
+ ),
+ }
+ }
+
+ fn credential_key_context_source(&self) -> anyhow::Result {
+ match (
+ &self.credential_key_context,
+ self.credential_key_context_handle,
+ ) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!(
+ "exactly one of --credentialkey-context or --credentialkey-context-handle must be provided"
+ ),
+ }
+ }
+
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut ctx = create_context(global.tcti.as_deref())?;
+
+ let activate_handle = load_key_from_source(&mut ctx, &self.credentialed_context_source()?)?;
+ let key_handle = load_key_from_source(&mut ctx, &self.credential_key_context_source()?)?;
+
+ // Set auth on the credentialed key (AK) if provided.
+ if let Some(ref a) = self.credentialed_auth {
+ let auth = parse::parse_auth(a)?;
+ ctx.tr_set_auth(activate_handle.into(), auth)
+ .context("failed to set credentialed key auth")?;
+ }
+
+ // Read and parse the credential blob.
+ let blob = std::fs::read(&self.credential_blob).with_context(|| {
+ format!(
+ "reading credential blob: {}",
+ self.credential_blob.display()
+ )
+ })?;
+ let (id_object, encrypted_secret) = parse_credential_blob(&blob)?;
+
+ // Determine the EK authorization session.
+ //
+ // If `-P session:` is given, load the external (already
+ // satisfied) policy session from the file. Otherwise start an
+ // internal EK policy session with PolicySecret(endorsement).
+ let external_session = self.is_external_session();
+ let ek_session = if let Some(path) = external_session {
+ load_session_from_file(&mut ctx, path.as_ref(), SessionType::Policy)?
+ } else {
+ // Set endorsement hierarchy auth if a password was given.
+ if let Some(ref a) = self.credential_key_auth {
+ let auth = parse::parse_auth(a)?;
+ let eh_obj: ObjectHandle = HierarchyAuth::Endorsement.into();
+ ctx.tr_set_auth(eh_obj, auth)
+ .context("failed to set endorsement hierarchy auth")?;
+ }
+ let ps = start_ek_policy_session(&mut ctx)?;
+ AuthSession::PolicySession(ps)
+ };
+
+ // ActivateCredential needs two auth sessions:
+ // session 1 → credentialed key (AK): password
+ // session 2 → credential key (EK): policy
+ ctx.set_sessions((Some(AuthSession::Password), Some(ek_session), None));
+ let cert_info = ctx
+ .activate_credential(activate_handle, key_handle, id_object, encrypted_secret)
+ .context("TPM2_ActivateCredential failed")?;
+ ctx.clear_sessions();
+
+ if external_session.is_none() {
+ // Flush the internally-created policy session.
+ if let AuthSession::PolicySession(ps) = ek_session {
+ flush_policy_session(&mut ctx, ps)?;
+ }
+ }
+
+ // Write decrypted secret.
+ std::fs::write(&self.certinfo_data, cert_info.value())
+ .with_context(|| format!("writing certinfo to {}", self.certinfo_data.display()))?;
+ info!("certinfo saved to {}", self.certinfo_data.display());
+
+ Ok(())
+ }
+
+ /// If `-P` starts with `session:`, return the file path portion.
+ fn is_external_session(&self) -> Option<&str> {
+ self.credential_key_auth
+ .as_deref()
+ .and_then(|v| v.strip_prefix("session:"))
+ }
+}
+
+/// Parse a credential blob file into `(IdObject, EncryptedSecret)`.
+///
+/// Format: `[u16 BE id_len][id_data][u16 BE secret_len][secret_data]`.
+fn parse_credential_blob(blob: &[u8]) -> anyhow::Result<(IdObject, EncryptedSecret)> {
+ if blob.len() < 4 {
+ anyhow::bail!("credential blob too short");
+ }
+ let id_size = u16::from_be_bytes([blob[0], blob[1]]) as usize;
+ let id_end = 2 + id_size;
+ if blob.len() < id_end + 2 {
+ anyhow::bail!("credential blob truncated");
+ }
+ let id_object = IdObject::try_from(blob[2..id_end].to_vec())
+ .map_err(|e| anyhow::anyhow!("invalid IdObject: {e}"))?;
+
+ let secret_start = id_end;
+ let secret_size = u16::from_be_bytes([blob[secret_start], blob[secret_start + 1]]) as usize;
+ let secret_end = secret_start + 2 + secret_size;
+ if blob.len() < secret_end {
+ anyhow::bail!("credential blob truncated (encrypted secret)");
+ }
+ let encrypted_secret = EncryptedSecret::try_from(blob[secret_start + 2..secret_end].to_vec())
+ .map_err(|e| anyhow::anyhow!("invalid EncryptedSecret: {e}"))?;
+
+ Ok((id_object, encrypted_secret))
+}
diff --git a/src/cmd/certify.rs b/src/cmd/certify.rs
new file mode 100644
index 0000000..6312322
--- /dev/null
+++ b/src/cmd/certify.rs
@@ -0,0 +1,170 @@
+use std::path::PathBuf;
+
+use anyhow::Context;
+use clap::Parser;
+use log::info;
+use tss_esapi::structures::Data;
+use tss_esapi::traits::Marshall;
+
+use crate::cli::GlobalOpts;
+use crate::context::create_context;
+use crate::handle::{ContextSource, load_key_from_source, load_object_from_source};
+use crate::parse;
+use crate::parse::parse_hex_u32;
+use crate::session::execute_with_optional_session;
+
+/// Certify that an object is loaded in the TPM.
+///
+/// Wraps TPM2_Certify: the signing key produces a signed attestation
+/// structure proving that the certified object is loaded and
+/// self-consistent.
+#[derive(Parser)]
+pub struct CertifyCmd {
+ /// Object to certify (context file path)
+ #[arg(
+ short = 'c',
+ long = "certifiedkey-context",
+ conflicts_with = "certified_context_handle"
+ )]
+ pub certified_context: Option,
+
+ /// Object to certify (hex handle, e.g. 0x81000001)
+ #[arg(long = "certifiedkey-context-handle", value_parser = parse_hex_u32, conflicts_with = "certified_context")]
+ pub certified_context_handle: Option,
+
+ /// Signing key context file path
+ #[arg(
+ short = 'C',
+ long = "signingkey-context",
+ conflicts_with = "signing_context_handle"
+ )]
+ pub signing_context: Option,
+
+ /// Signing key handle (hex, e.g. 0x81000001)
+ #[arg(long = "signingkey-context-handle", value_parser = parse_hex_u32, conflicts_with = "signing_context")]
+ pub signing_context_handle: Option,
+
+ /// Auth value for the certified object
+ #[arg(short = 'P', long = "certifiedkey-auth")]
+ pub certified_auth: Option,
+
+ /// Auth value for the signing key
+ #[arg(short = 'p', long = "signingkey-auth")]
+ pub signing_auth: Option,
+
+ /// Hash algorithm for signing
+ #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")]
+ pub hash_algorithm: String,
+
+ /// Signature scheme (rsassa, rsapss, ecdsa, null)
+ #[arg(long = "scheme", default_value = "null")]
+ pub scheme: String,
+
+ /// Qualifying data (hex string)
+ #[arg(
+ short = 'q',
+ long = "qualification",
+ conflicts_with = "qualification_file"
+ )]
+ pub qualification: Option,
+
+ /// Qualifying data file path
+ #[arg(long = "qualification-file", conflicts_with = "qualification")]
+ pub qualification_file: Option,
+
+ /// Output file for the attestation data (marshaled TPMS_ATTEST)
+ #[arg(short = 'o', long = "attestation")]
+ pub attestation: Option,
+
+ /// Output file for the signature (marshaled TPMT_SIGNATURE)
+ #[arg(short = 's', long = "signature")]
+ pub signature: Option,
+
+ /// Session context file for authorization
+ #[arg(short = 'S', long = "session")]
+ pub session: Option,
+}
+
+impl CertifyCmd {
+ fn certified_context_source(&self) -> anyhow::Result {
+ match (&self.certified_context, self.certified_context_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!(
+ "exactly one of --certifiedkey-context or --certifiedkey-context-handle must be provided"
+ ),
+ }
+ }
+
+ fn signing_context_source(&self) -> anyhow::Result {
+ match (&self.signing_context, self.signing_context_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!(
+ "exactly one of --signingkey-context or --signingkey-context-handle must be provided"
+ ),
+ }
+ }
+
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut ctx = create_context(global.tcti.as_deref())?;
+
+ let object_handle = load_object_from_source(&mut ctx, &self.certified_context_source()?)?;
+ let signing_key = load_key_from_source(&mut ctx, &self.signing_context_source()?)?;
+ let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?;
+ let scheme = parse::parse_signature_scheme(&self.scheme, hash_alg)?;
+
+ if let Some(ref auth_str) = self.certified_auth {
+ let auth = parse::parse_auth(auth_str)?;
+ ctx.tr_set_auth(object_handle, auth)
+ .context("failed to set certified key auth")?;
+ }
+ if let Some(ref auth_str) = self.signing_auth {
+ let auth = parse::parse_auth(auth_str)?;
+ ctx.tr_set_auth(signing_key.into(), auth)
+ .context("failed to set signing key auth")?;
+ }
+
+ let qualifying = match (&self.qualification, &self.qualification_file) {
+ (Some(q), None) => {
+ let bytes =
+ parse::parse_qualification_hex(q).context("failed to parse qualifying data")?;
+ Data::try_from(bytes).map_err(|e| anyhow::anyhow!("qualifying data: {e}"))?
+ }
+ (None, Some(path)) => {
+ let bytes = parse::parse_qualification_file(path)
+ .context("failed to read qualifying data file")?;
+ Data::try_from(bytes).map_err(|e| anyhow::anyhow!("qualifying data: {e}"))?
+ }
+ (None, None) => Data::default(),
+ _ => {
+ anyhow::bail!("only one of --qualification or --qualification-file may be provided")
+ }
+ };
+
+ let session_path = self.session.as_deref();
+ let (attest, signature) = execute_with_optional_session(&mut ctx, session_path, |ctx| {
+ ctx.certify(object_handle, signing_key, qualifying.clone(), scheme)
+ })
+ .context("TPM2_Certify failed")?;
+
+ if let Some(ref path) = self.attestation {
+ let bytes = attest.marshall().context("failed to marshal TPMS_ATTEST")?;
+ std::fs::write(path, &bytes)
+ .with_context(|| format!("writing attestation to {}", path.display()))?;
+ info!("attestation saved to {}", path.display());
+ }
+
+ if let Some(ref path) = self.signature {
+ let bytes = signature
+ .marshall()
+ .context("failed to marshal TPMT_SIGNATURE")?;
+ std::fs::write(path, &bytes)
+ .with_context(|| format!("writing signature to {}", path.display()))?;
+ info!("signature saved to {}", path.display());
+ }
+
+ info!("certify succeeded");
+ Ok(())
+ }
+}
diff --git a/src/cmd/certifycreation.rs b/src/cmd/certifycreation.rs
new file mode 100644
index 0000000..a1be5ff
--- /dev/null
+++ b/src/cmd/certifycreation.rs
@@ -0,0 +1,211 @@
+use std::path::PathBuf;
+
+use anyhow::Context;
+use clap::Parser;
+use log::info;
+use tss_esapi::constants::tss::*;
+use tss_esapi::tss2_esys::*;
+
+use crate::cli::GlobalOpts;
+use crate::handle::ContextSource;
+use crate::parse::{self, parse_hex_u32};
+use crate::raw_esys::{self, RawEsysContext};
+
+/// Certify the creation data associated with an object.
+///
+/// Wraps TPM2_CertifyCreation (raw FFI).
+#[derive(Parser)]
+pub struct CertifyCreationCmd {
+ /// Signing key context file path
+ #[arg(
+ short = 'C',
+ long = "signingkey-context",
+ conflicts_with = "signing_context_handle"
+ )]
+ pub signing_context: Option,
+
+ /// Signing key handle (hex, e.g. 0x81000001)
+ #[arg(long = "signingkey-context-handle", value_parser = parse_hex_u32, conflicts_with = "signing_context")]
+ pub signing_context_handle: Option,
+
+ /// Object context file path
+ #[arg(
+ short = 'c',
+ long = "certifiedkey-context",
+ conflicts_with = "certified_context_handle"
+ )]
+ pub certified_context: Option,
+
+ /// Object handle (hex, e.g. 0x81000001)
+ #[arg(long = "certifiedkey-context-handle", value_parser = parse_hex_u32, conflicts_with = "certified_context")]
+ pub certified_context_handle: Option,
+
+ /// Auth value for the signing key
+ #[arg(short = 'P', long = "signingkey-auth")]
+ pub signing_auth: Option,
+
+ /// Creation hash file
+ #[arg(short = 'd', long = "creation-hash")]
+ pub creation_hash: PathBuf,
+
+ /// Creation ticket file
+ #[arg(short = 't', long = "ticket")]
+ pub ticket: PathBuf,
+
+ /// Qualifying data (hex string)
+ #[arg(
+ short = 'q',
+ long = "qualification",
+ conflicts_with = "qualification_file"
+ )]
+ pub qualification: Option,
+
+ /// Qualifying data file path
+ #[arg(long = "qualification-file", conflicts_with = "qualification")]
+ pub qualification_file: Option,
+
+ /// Signature scheme (null)
+ #[arg(short = 'g', long = "scheme", default_value = "null")]
+ pub scheme: String,
+
+ /// Output file for attestation
+ #[arg(short = 'o', long = "attestation")]
+ pub attestation: Option,
+
+ /// Output file for signature
+ #[arg(short = 's', long = "signature")]
+ pub signature: Option,
+}
+
+impl CertifyCreationCmd {
+ fn signing_context_source(&self) -> anyhow::Result {
+ match (&self.signing_context, self.signing_context_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!(
+ "exactly one of --signingkey-context or --signingkey-context-handle must be provided"
+ ),
+ }
+ }
+
+ fn certified_context_source(&self) -> anyhow::Result {
+ match (&self.certified_context, self.certified_context_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!(
+ "exactly one of --certifiedkey-context or --certifiedkey-context-handle must be provided"
+ ),
+ }
+ }
+
+ #[allow(clippy::field_reassign_with_default)]
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut raw = RawEsysContext::new(global.tcti.as_deref())?;
+ let sign_handle = raw.resolve_handle_from_source(&self.signing_context_source()?)?;
+ let obj_handle = raw.resolve_handle_from_source(&self.certified_context_source()?)?;
+
+ if let Some(ref auth_str) = self.signing_auth {
+ let auth = parse::parse_auth(auth_str)?;
+ raw.set_auth(sign_handle, auth.value())?;
+ }
+
+ let creation_hash_data = std::fs::read(&self.creation_hash).with_context(|| {
+ format!(
+ "reading creation hash from {}",
+ self.creation_hash.display()
+ )
+ })?;
+ let mut creation_hash = TPM2B_DIGEST::default();
+ let len = creation_hash_data.len().min(creation_hash.buffer.len());
+ creation_hash.size = len as u16;
+ creation_hash.buffer[..len].copy_from_slice(&creation_hash_data[..len]);
+
+ let ticket_data = std::fs::read(&self.ticket)
+ .with_context(|| format!("reading ticket from {}", self.ticket.display()))?;
+ // The ticket is a TPMT_TK_CREATION structure. For simplicity, we'll treat
+ // it as raw bytes and construct the struct manually if it's the right size.
+ let creation_ticket = if ticket_data.len() >= 6 {
+ let mut tk = TPMT_TK_CREATION::default();
+ // First 2 bytes: tag, next 4 bytes: hierarchy, rest: digest
+ tk.tag = u16::from_le_bytes([ticket_data[0], ticket_data[1]]);
+ tk.hierarchy = u32::from_le_bytes([
+ ticket_data[2],
+ ticket_data[3],
+ ticket_data[4],
+ ticket_data[5],
+ ]);
+ if ticket_data.len() > 6 {
+ let digest_data = &ticket_data[6..];
+ let dlen = digest_data.len().min(tk.digest.buffer.len());
+ tk.digest.size = dlen as u16;
+ tk.digest.buffer[..dlen].copy_from_slice(&digest_data[..dlen]);
+ }
+ tk
+ } else {
+ TPMT_TK_CREATION::default()
+ };
+
+ let qualifying = match (&self.qualification, &self.qualification_file) {
+ (Some(q), None) => {
+ let bytes = parse::parse_qualification_hex(q)?;
+ let mut qd = TPM2B_DATA::default();
+ let len = bytes.len().min(qd.buffer.len());
+ qd.size = len as u16;
+ qd.buffer[..len].copy_from_slice(&bytes[..len]);
+ qd
+ }
+ (None, Some(path)) => {
+ let bytes = parse::parse_qualification_file(path)?;
+ let mut qd = TPM2B_DATA::default();
+ let len = bytes.len().min(qd.buffer.len());
+ qd.size = len as u16;
+ qd.buffer[..len].copy_from_slice(&bytes[..len]);
+ qd
+ }
+ _ => TPM2B_DATA::default(),
+ };
+
+ let in_scheme = TPMT_SIG_SCHEME {
+ scheme: TPM2_ALG_NULL,
+ ..Default::default()
+ };
+
+ unsafe {
+ let mut certify_info: *mut TPM2B_ATTEST = std::ptr::null_mut();
+ let mut sig: *mut TPMT_SIGNATURE = std::ptr::null_mut();
+
+ let rc = Esys_CertifyCreation(
+ raw.ptr(),
+ sign_handle,
+ obj_handle,
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &qualifying,
+ &creation_hash,
+ &in_scheme,
+ &creation_ticket,
+ &mut certify_info,
+ &mut sig,
+ );
+ if rc != 0 {
+ anyhow::bail!("Esys_CertifyCreation failed: 0x{rc:08x}");
+ }
+
+ if let Some(ref path) = self.attestation {
+ raw_esys::write_raw_attestation(certify_info, path)?;
+ info!("attestation saved to {}", path.display());
+ }
+ if let Some(ref path) = self.signature {
+ raw_esys::write_raw_signature(sig, path)?;
+ info!("signature saved to {}", path.display());
+ }
+
+ Esys_Free(certify_info as *mut _);
+ Esys_Free(sig as *mut _);
+ }
+
+ info!("certify creation succeeded");
+ Ok(())
+ }
+}
diff --git a/src/cmd/changeauth.rs b/src/cmd/changeauth.rs
new file mode 100644
index 0000000..1066790
--- /dev/null
+++ b/src/cmd/changeauth.rs
@@ -0,0 +1,145 @@
+use std::path::PathBuf;
+
+use anyhow::Context;
+use clap::Parser;
+use log::info;
+
+use crate::cli::GlobalOpts;
+use crate::context::create_context;
+use crate::handle::{ContextSource, load_object_from_source};
+use crate::parse;
+use crate::parse::parse_hex_u32;
+use crate::session::execute_with_optional_session;
+
+/// Change the authorization value of a TPM object or hierarchy.
+///
+/// For hierarchies: wraps TPM2_HierarchyChangeAuth.
+/// For loaded objects: wraps TPM2_ObjectChangeAuth.
+#[derive(Parser)]
+pub struct ChangeAuthCmd {
+ /// Object context file path
+ #[arg(short = 'c', long = "object-context", conflicts_with_all = ["object_context_handle", "object_context_hierarchy"])]
+ pub object_context: Option,
+
+ /// Object handle (hex, e.g. 0x81000001)
+ #[arg(long = "object-context-handle", value_parser = parse_hex_u32, conflicts_with_all = ["object_context", "object_context_hierarchy"])]
+ pub object_context_handle: Option,
+
+ /// Hierarchy shorthand (o/owner, p/platform, e/endorsement, l/lockout)
+ #[arg(long = "object-hierarchy", conflicts_with_all = ["object_context", "object_context_handle"])]
+ pub object_context_hierarchy: Option,
+
+ /// Parent object context file path (required for loaded objects, not for hierarchies)
+ #[arg(
+ short = 'C',
+ long = "parent-context",
+ conflicts_with = "parent_context_handle"
+ )]
+ pub parent_context: Option,
+
+ /// Parent object handle (hex, e.g. 0x81000001)
+ #[arg(long = "parent-context-handle", value_parser = parse_hex_u32, conflicts_with = "parent_context")]
+ pub parent_context_handle: Option,
+
+ /// Current auth value for the object/hierarchy
+ #[arg(short = 'p', long = "auth")]
+ pub auth: Option,
+
+ /// New auth value
+ #[arg(short = 'r', long = "new-auth")]
+ pub new_auth: String,
+
+ /// Output file for the new private portion (for loaded objects)
+ #[arg(short = 'o', long = "output")]
+ pub output: Option,
+
+ /// Session context file for authorization
+ #[arg(short = 'S', long = "session")]
+ pub session: Option,
+}
+
+impl ChangeAuthCmd {
+ fn object_context_source(&self) -> anyhow::Result {
+ match (&self.object_context, self.object_context_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!(
+ "exactly one of --object-context or --object-context-handle must be provided"
+ ),
+ }
+ }
+
+ fn parent_context_source(&self) -> anyhow::Result {
+ match (&self.parent_context, self.parent_context_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!(
+ "exactly one of --parent-context or --parent-context-handle must be provided"
+ ),
+ }
+ }
+
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut ctx = create_context(global.tcti.as_deref())?;
+
+ let new_auth = parse::parse_auth(&self.new_auth)?;
+
+ // Check if this is a hierarchy handle.
+ let hierarchy = match &self.object_context_hierarchy {
+ Some(h) => match h.to_lowercase().as_str() {
+ "o" | "owner" => Some(tss_esapi::handles::AuthHandle::Owner),
+ "p" | "platform" => Some(tss_esapi::handles::AuthHandle::Platform),
+ "e" | "endorsement" => Some(tss_esapi::handles::AuthHandle::Endorsement),
+ "l" | "lockout" => Some(tss_esapi::handles::AuthHandle::Lockout),
+ _ => anyhow::bail!("unknown hierarchy shorthand: {h}"),
+ },
+ None => None,
+ };
+
+ if let Some(auth_handle) = hierarchy {
+ if let Some(ref auth_str) = self.auth {
+ let auth = parse::parse_auth(auth_str)?;
+ ctx.tr_set_auth(auth_handle.into(), auth)
+ .context("tr_set_auth failed")?;
+ }
+
+ let session_path = self.session.as_deref();
+ execute_with_optional_session(&mut ctx, session_path, |ctx| {
+ ctx.hierarchy_change_auth(auth_handle, new_auth.clone())
+ })
+ .context("TPM2_HierarchyChangeAuth failed")?;
+
+ info!("hierarchy auth changed");
+ } else {
+ let object_handle = load_object_from_source(&mut ctx, &self.object_context_source()?)?;
+ let parent_source = self.parent_context_source().map_err(|_| {
+ anyhow::anyhow!(
+ "-C/--parent-context or --parent-context-handle is required for loaded objects"
+ )
+ })?;
+ let parent_handle = load_object_from_source(&mut ctx, &parent_source)?;
+
+ if let Some(ref auth_str) = self.auth {
+ let auth = parse::parse_auth(auth_str)?;
+ ctx.tr_set_auth(object_handle, auth)
+ .context("tr_set_auth failed")?;
+ }
+
+ let session_path = self.session.as_deref();
+ let new_private = execute_with_optional_session(&mut ctx, session_path, |ctx| {
+ ctx.object_change_auth(object_handle, parent_handle, new_auth.clone())
+ })
+ .context("TPM2_ObjectChangeAuth failed")?;
+
+ if let Some(ref path) = self.output {
+ std::fs::write(path, new_private.value())
+ .with_context(|| format!("writing output to {}", path.display()))?;
+ info!("new private saved to {}", path.display());
+ }
+
+ info!("object auth changed");
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/cmd/changeeps.rs b/src/cmd/changeeps.rs
new file mode 100644
index 0000000..d2ab691
--- /dev/null
+++ b/src/cmd/changeeps.rs
@@ -0,0 +1,44 @@
+use clap::Parser;
+use log::info;
+use tss_esapi::tss2_esys::*;
+
+use crate::cli::GlobalOpts;
+use crate::parse;
+use crate::raw_esys::RawEsysContext;
+
+/// Replace the endorsement primary seed and flush resident objects.
+///
+/// Wraps TPM2_ChangeEPS (raw FFI).
+#[derive(Parser)]
+pub struct ChangeEpsCmd {
+ /// Auth value for the platform hierarchy
+ #[arg(short = 'p', long = "auth")]
+ pub auth: Option,
+}
+
+impl ChangeEpsCmd {
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut raw = RawEsysContext::new(global.tcti.as_deref())?;
+
+ if let Some(ref auth_str) = self.auth {
+ let auth = parse::parse_auth(auth_str)?;
+ raw.set_auth(ESYS_TR_RH_PLATFORM, auth.value())?;
+ }
+
+ unsafe {
+ let rc = Esys_ChangeEPS(
+ raw.ptr(),
+ ESYS_TR_RH_PLATFORM,
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ );
+ if rc != 0 {
+ anyhow::bail!("Esys_ChangeEPS failed: 0x{rc:08x}");
+ }
+ }
+
+ info!("endorsement primary seed changed");
+ Ok(())
+ }
+}
diff --git a/src/cmd/changepps.rs b/src/cmd/changepps.rs
new file mode 100644
index 0000000..ae7c8b9
--- /dev/null
+++ b/src/cmd/changepps.rs
@@ -0,0 +1,44 @@
+use clap::Parser;
+use log::info;
+use tss_esapi::tss2_esys::*;
+
+use crate::cli::GlobalOpts;
+use crate::parse;
+use crate::raw_esys::RawEsysContext;
+
+/// Replace the platform primary seed and flush resident objects.
+///
+/// Wraps TPM2_ChangePPS (raw FFI).
+#[derive(Parser)]
+pub struct ChangePpsCmd {
+ /// Auth value for the platform hierarchy
+ #[arg(short = 'p', long = "auth")]
+ pub auth: Option,
+}
+
+impl ChangePpsCmd {
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut raw = RawEsysContext::new(global.tcti.as_deref())?;
+
+ if let Some(ref auth_str) = self.auth {
+ let auth = parse::parse_auth(auth_str)?;
+ raw.set_auth(ESYS_TR_RH_PLATFORM, auth.value())?;
+ }
+
+ unsafe {
+ let rc = Esys_ChangePPS(
+ raw.ptr(),
+ ESYS_TR_RH_PLATFORM,
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ );
+ if rc != 0 {
+ anyhow::bail!("Esys_ChangePPS failed: 0x{rc:08x}");
+ }
+ }
+
+ info!("platform primary seed changed");
+ Ok(())
+ }
+}
diff --git a/src/cmd/checkquote.rs b/src/cmd/checkquote.rs
new file mode 100644
index 0000000..89f32ff
--- /dev/null
+++ b/src/cmd/checkquote.rs
@@ -0,0 +1,203 @@
+use std::path::PathBuf;
+
+use anyhow::{Context, bail};
+use clap::Parser;
+use log::info;
+use tss_esapi::structures::{Attest, AttestInfo, MaxBuffer, Signature};
+use tss_esapi::traits::UnMarshall;
+
+use crate::cli::GlobalOpts;
+use crate::context::create_context;
+use crate::handle::{ContextSource, load_key_from_source};
+use crate::parse;
+use crate::parse::parse_hex_u32;
+
+/// Verify a TPM quote.
+///
+/// Hashes the quote message, verifies the signature with the public key,
+/// and optionally checks qualification data and PCR values embedded in the
+/// attestation structure.
+#[derive(Parser)]
+pub struct CheckQuoteCmd {
+ /// Public key context file path
+ #[arg(short = 'u', long = "public", conflicts_with = "public_handle")]
+ pub public: Option,
+
+ /// Public key handle (hex, e.g. 0x81000001)
+ #[arg(short = 'H', long = "public-handle", value_parser = parse_hex_u32, conflicts_with = "public")]
+ pub public_handle: Option,
+
+ /// Quote message file (marshaled TPMS_ATTEST)
+ #[arg(short = 'm', long = "message")]
+ pub message: PathBuf,
+
+ /// Signature file (marshaled TPMT_SIGNATURE)
+ #[arg(short = 's', long = "signature")]
+ pub signature: PathBuf,
+
+ /// Hash algorithm used to digest the message
+ #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")]
+ pub hash_algorithm: String,
+
+ /// PCR values file for additional verification
+ #[arg(short = 'f', long = "pcr", conflicts_with = "pcr-list")]
+ pub pcr_file: Option,
+
+ /// PCR selection list (e.g. sha256:0,1,2)
+ #[arg(short = 'l', long = "pcr-list", conflicts_with = "pcr")]
+ pub pcr_list: Option,
+
+ /// Qualification data (hex string) for replay-protection check
+ #[arg(
+ short = 'q',
+ long = "qualification",
+ conflicts_with = "qualification_file"
+ )]
+ pub qualification: Option,
+
+ /// Qualifying data file path
+ #[arg(long = "qualification-file", conflicts_with = "qualification")]
+ pub qualification_file: Option,
+}
+
+impl CheckQuoteCmd {
+ fn public_source(&self) -> anyhow::Result {
+ match (&self.public, self.public_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!("exactly one of --public or --public-handle must be provided"),
+ }
+ }
+
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut ctx = create_context(global.tcti.as_deref())?;
+
+ let key_handle = load_key_from_source(&mut ctx, &self.public_source()?)?;
+ let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?;
+
+ // ---------------------------------------------------------------
+ // 1. Read and hash the quote message
+ // ---------------------------------------------------------------
+ let msg_bytes = std::fs::read(&self.message)
+ .with_context(|| format!("reading message: {}", self.message.display()))?;
+
+ let buffer = MaxBuffer::try_from(msg_bytes.clone())
+ .map_err(|e| anyhow::anyhow!("message too large: {e}"))?;
+
+ let (digest, _ticket) = ctx
+ .execute_without_session(|ctx| {
+ ctx.hash(
+ buffer,
+ hash_alg,
+ tss_esapi::interface_types::resource_handles::Hierarchy::Owner,
+ )
+ })
+ .context("TPM2_Hash failed")?;
+
+ // ---------------------------------------------------------------
+ // 2. Read the signature and verify against the computed digest
+ // ---------------------------------------------------------------
+ let sig_bytes = std::fs::read(&self.signature)
+ .with_context(|| format!("reading signature: {}", self.signature.display()))?;
+ let signature = Signature::unmarshall(&sig_bytes)
+ .map_err(|e| anyhow::anyhow!("failed to parse signature: {e}"))?;
+
+ ctx.execute_without_session(|ctx| {
+ ctx.verify_signature(key_handle, digest.clone(), signature)
+ })
+ .context("TPM2_VerifySignature failed — quote signature is invalid")?;
+
+ info!("signature verification: OK");
+
+ // ---------------------------------------------------------------
+ // 3. Unmarshal the attestation structure for further checks
+ // ---------------------------------------------------------------
+ let attest = Attest::unmarshall(&msg_bytes)
+ .map_err(|e| anyhow::anyhow!("failed to unmarshal TPMS_ATTEST: {e}"))?;
+
+ let quote_info = match attest.attested() {
+ AttestInfo::Quote { info } => info,
+ other => bail!(
+ "expected a Quote attestation, got {:?}",
+ std::mem::discriminant(other)
+ ),
+ };
+
+ // ---------------------------------------------------------------
+ // 4. Check qualification (extraData / nonce)
+ // ---------------------------------------------------------------
+ let qualification_data = match (&self.qualification, &self.qualification_file) {
+ (Some(q), None) => Some(
+ parse::parse_qualification_hex(q).context("failed to parse qualification data")?,
+ ),
+ (None, Some(path)) => Some(
+ parse::parse_qualification_file(path)
+ .context("failed to parse qualification data")?,
+ ),
+ _ => None,
+ };
+ if let Some(ref expected) = qualification_data {
+ if attest.extra_data().value() != expected.as_slice() {
+ bail!(
+ "qualification mismatch: quote contains {:?}, expected {:?}",
+ hex::encode(attest.extra_data().value()),
+ hex::encode(expected)
+ );
+ }
+ info!("qualification check: OK");
+ }
+
+ // ---------------------------------------------------------------
+ // 5. Check PCR digest
+ // ---------------------------------------------------------------
+ if let Some(ref pcr_path) = self.pcr_file {
+ let pcr_bytes = std::fs::read(pcr_path)
+ .with_context(|| format!("reading PCR file: {}", pcr_path.display()))?;
+
+ // Hash the raw PCR values to compare with the digest in the
+ // quote. Use the same algorithm that was used for the quote.
+ let pcr_buf = MaxBuffer::try_from(pcr_bytes)
+ .map_err(|e| anyhow::anyhow!("PCR data too large: {e}"))?;
+
+ let (pcr_digest, _) = ctx
+ .execute_without_session(|ctx| {
+ ctx.hash(
+ pcr_buf,
+ hash_alg,
+ tss_esapi::interface_types::resource_handles::Hierarchy::Owner,
+ )
+ })
+ .context("TPM2_Hash of PCR values failed")?;
+
+ let expected_pcr_digest = quote_info.pcr_digest();
+ if pcr_digest.value() != expected_pcr_digest.value() {
+ bail!(
+ "PCR digest mismatch: computed {}, quote contains {}",
+ hex::encode(pcr_digest.value()),
+ hex::encode(expected_pcr_digest.value())
+ );
+ }
+ info!("PCR digest check: OK");
+ }
+
+ // ---------------------------------------------------------------
+ // 6. If a PCR selection list was supplied, compare with the quote
+ // ---------------------------------------------------------------
+ if let Some(ref pcr_list_str) = self.pcr_list {
+ let expected_selection = parse::parse_pcr_selection(pcr_list_str)?;
+ let quote_selection = quote_info.pcr_selection();
+
+ let expected_dbg = format!("{expected_selection:?}");
+ let quote_dbg = format!("{quote_selection:?}");
+ if expected_dbg != quote_dbg {
+ bail!(
+ "PCR selection mismatch: expected {expected_dbg}, quote contains {quote_dbg}"
+ );
+ }
+ info!("PCR selection check: OK");
+ }
+
+ info!("quote verification: OK");
+ Ok(())
+ }
+}
diff --git a/src/cmd/clear.rs b/src/cmd/clear.rs
new file mode 100644
index 0000000..03e0215
--- /dev/null
+++ b/src/cmd/clear.rs
@@ -0,0 +1,43 @@
+use std::path::PathBuf;
+
+use anyhow::Context;
+use clap::Parser;
+use log::info;
+
+use crate::cli::GlobalOpts;
+use crate::context::create_context;
+use crate::parse;
+use crate::session::execute_with_optional_session;
+
+/// Clear the TPM -- removes all loaded objects, sessions, and saved contexts.
+///
+/// By default uses the lockout hierarchy. Use `-c` to specify a different
+/// authorization handle (owner, platform, lockout).
+#[derive(Parser)]
+pub struct ClearCmd {
+ /// Authorization handle (o/owner, p/platform, l/lockout)
+ #[arg(short = 'c', long = "auth", default_value = "l")]
+ pub auth_handle: String,
+
+ /// Session context file for authorization
+ #[arg(short = 'S', long = "session")]
+ pub session: Option,
+}
+
+impl ClearCmd {
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut ctx = create_context(global.tcti.as_deref())?;
+
+ let auth = parse::parse_auth_handle(&self.auth_handle)?;
+
+ let session_path = self.session.as_deref();
+ execute_with_optional_session(&mut ctx, session_path, |ctx| {
+ ctx.clear(auth)?;
+ Ok(())
+ })
+ .context("TPM2_Clear failed")?;
+
+ info!("TPM cleared");
+ Ok(())
+ }
+}
diff --git a/src/cmd/clearcontrol.rs b/src/cmd/clearcontrol.rs
new file mode 100644
index 0000000..84f3fd3
--- /dev/null
+++ b/src/cmd/clearcontrol.rs
@@ -0,0 +1,56 @@
+use anyhow::Context;
+use clap::Parser;
+use log::info;
+
+use crate::cli::GlobalOpts;
+use crate::context::create_context;
+use crate::parse;
+use crate::session::execute_with_optional_session;
+
+/// Enable or disable the TPM2_Clear command.
+///
+/// Wraps TPM2_ClearControl.
+#[derive(Parser)]
+pub struct ClearControlCmd {
+ /// Auth handle (p/platform or l/lockout)
+ #[arg(short = 'C', long = "hierarchy")]
+ pub hierarchy: String,
+
+ /// Auth value for the hierarchy
+ #[arg(short = 'P', long = "auth")]
+ pub auth: Option,
+
+ /// Set to disable clear (true) or enable clear (false)
+ #[arg(short = 's', long = "disable-clear", default_value = "true")]
+ pub disable: bool,
+
+ /// Session context file
+ #[arg(short = 'S', long = "session")]
+ pub session: Option,
+}
+
+impl ClearControlCmd {
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut ctx = create_context(global.tcti.as_deref())?;
+
+ let auth_handle = parse::parse_auth_handle(&self.hierarchy)?;
+
+ if let Some(ref auth_str) = self.auth {
+ let auth = parse::parse_auth(auth_str)?;
+ ctx.tr_set_auth(auth_handle.into(), auth)
+ .context("tr_set_auth failed")?;
+ }
+
+ let session_path = self.session.as_deref();
+ execute_with_optional_session(&mut ctx, session_path, |ctx| {
+ ctx.clear_control(auth_handle, self.disable)
+ })
+ .context("TPM2_ClearControl failed")?;
+
+ info!(
+ "clear control set: clear is {}",
+ if self.disable { "DISABLED" } else { "ENABLED" }
+ );
+ Ok(())
+ }
+}
diff --git a/src/cmd/clockrateadjust.rs b/src/cmd/clockrateadjust.rs
new file mode 100644
index 0000000..4a17527
--- /dev/null
+++ b/src/cmd/clockrateadjust.rs
@@ -0,0 +1,67 @@
+use clap::Parser;
+use log::info;
+use tss_esapi::constants::tss::*;
+use tss_esapi::tss2_esys::*;
+
+use crate::cli::GlobalOpts;
+use crate::parse;
+use crate::raw_esys::RawEsysContext;
+
+/// Adjust the rate of advance of the TPM clock.
+///
+/// Wraps TPM2_ClockRateAdjust (raw FFI).
+#[derive(Parser)]
+pub struct ClockRateAdjustCmd {
+ /// Auth hierarchy (o/owner or p/platform)
+ #[arg(short = 'c', long = "hierarchy", default_value = "o")]
+ pub hierarchy: String,
+
+ /// Auth value
+ #[arg(short = 'p', long = "auth")]
+ pub auth: Option,
+
+ /// Rate adjustment (slower, slow, medium, fast, faster)
+ #[arg()]
+ pub rate: String,
+}
+
+impl ClockRateAdjustCmd {
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let mut raw = RawEsysContext::new(global.tcti.as_deref())?;
+ let auth_handle = RawEsysContext::resolve_hierarchy(&self.hierarchy)?;
+
+ if let Some(ref auth_str) = self.auth {
+ let auth = parse::parse_auth(auth_str)?;
+ raw.set_auth(auth_handle, auth.value())?;
+ }
+
+ let rate_adjust: TPM2_CLOCK_ADJUST = match self.rate.to_lowercase().as_str() {
+ "slower" => TPM2_CLOCK_COARSE_SLOWER,
+ "slow" => TPM2_CLOCK_FINE_SLOWER,
+ "medium" | "none" => TPM2_CLOCK_NO_CHANGE,
+ "fast" => TPM2_CLOCK_FINE_FASTER,
+ "faster" => TPM2_CLOCK_COARSE_FASTER,
+ _ => anyhow::bail!(
+ "invalid rate: {}; use slower/slow/medium/fast/faster",
+ self.rate
+ ),
+ };
+
+ unsafe {
+ let rc = Esys_ClockRateAdjust(
+ raw.ptr(),
+ auth_handle,
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ rate_adjust,
+ );
+ if rc != 0 {
+ anyhow::bail!("Esys_ClockRateAdjust failed: 0x{rc:08x}");
+ }
+ }
+
+ info!("clock rate adjusted to {}", self.rate);
+ Ok(())
+ }
+}
diff --git a/src/cmd/commit.rs b/src/cmd/commit.rs
new file mode 100644
index 0000000..70017c8
--- /dev/null
+++ b/src/cmd/commit.rs
@@ -0,0 +1,121 @@
+use std::path::PathBuf;
+
+use anyhow::Context;
+use clap::Parser;
+use log::info;
+
+use crate::cli::GlobalOpts;
+use crate::handle::ContextSource;
+use crate::parse::parse_hex_u32;
+use crate::raw_esys;
+
+/// Perform the first part of an ECC anonymous signing operation.
+///
+/// Wraps TPM2_Commit: performs point multiplications on the provided
+/// points and returns intermediate signing values. The signing key
+/// must use the ECDAA scheme.
+#[derive(Parser)]
+pub struct CommitCmd {
+ /// Signing key context file path
+ #[arg(short = 'c', long = "context", conflicts_with = "context_handle")]
+ pub context: Option,
+
+ /// Signing key handle (hex, e.g. 0x81000001)
+ #[arg(short = 'H', long = "context-handle", value_parser = parse_hex_u32, conflicts_with = "context")]
+ pub context_handle: Option,
+
+ /// Auth value for the signing key
+ #[arg(short = 'p', long = "auth")]
+ pub auth: Option,
+
+ /// ECC point P1 input file (optional)
+ #[arg(long = "eccpoint-P")]
+ pub eccpoint_p: Option,
+
+ /// Basepoint x-coordinate data file (optional, s2 parameter)
+ #[arg()]
+ pub basepoint_x: Option,
+
+ /// Basepoint y-coordinate file (optional, y2 parameter)
+ #[arg(long = "basepoint-y")]
+ pub basepoint_y: Option,
+
+ /// Output ECC point K file
+ #[arg(long = "eccpoint-K")]
+ pub eccpoint_k: Option,
+
+ /// Output ECC point L file
+ #[arg(long = "eccpoint-L")]
+ pub eccpoint_l: Option,
+
+ /// Output ECC point E file
+ #[arg(short = 'u', long = "public")]
+ pub eccpoint_e: Option,
+
+ /// Output counter file
+ #[arg(short = 't', long = "counter")]
+ pub counter: Option,
+}
+
+impl CommitCmd {
+ fn context_source(&self) -> anyhow::Result {
+ match (&self.context, self.context_handle) {
+ (Some(path), None) => Ok(ContextSource::File(path.clone())),
+ (None, Some(handle)) => Ok(ContextSource::Handle(handle)),
+ _ => anyhow::bail!("exactly one of --context or --context-handle must be provided"),
+ }
+ }
+
+ pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> {
+ let p1 = read_opt_file(&self.eccpoint_p)?;
+ let s2 = read_opt_file(&self.basepoint_x)?;
+ let y2 = read_opt_file(&self.basepoint_y)?;
+
+ let result = raw_esys::commit(
+ global.tcti.as_deref(),
+ &self.context_source()?,
+ self.auth.as_deref(),
+ p1.as_deref(),
+ s2.as_deref(),
+ y2.as_deref(),
+ )
+ .context("TPM2_Commit failed")?;
+
+ if let Some(ref path) = self.eccpoint_k {
+ std::fs::write(path, &result.k)
+ .with_context(|| format!("writing K to {}", path.display()))?;
+ info!("ECC point K saved to {}", path.display());
+ }
+
+ if let Some(ref path) = self.eccpoint_l {
+ std::fs::write(path, &result.l)
+ .with_context(|| format!("writing L to {}", path.display()))?;
+ info!("ECC point L saved to {}", path.display());
+ }
+
+ if let Some(ref path) = self.eccpoint_e {
+ std::fs::write(path, &result.e)
+ .with_context(|| format!("writing E to {}", path.display()))?;
+ info!("ECC point E saved to {}", path.display());
+ }
+
+ if let Some(ref path) = self.counter {
+ std::fs::write(path, result.counter.to_le_bytes())
+ .with_context(|| format!("writing counter to {}", path.display()))?;
+ info!("counter saved to {}", path.display());
+ }
+
+ info!("commit succeeded (counter={})", result.counter);
+ Ok(())
+ }
+}
+
+fn read_opt_file(path: &Option) -> anyhow::Result