From 5078aa5d3e16eb0bfee8f3edc3ce6209c574ab84 Mon Sep 17 00:00:00 2001 From: Takuma IMAMURA <209989118+hyperfinitism@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:11:41 +0900 Subject: [PATCH] feat: implement core functionality Implements 100 subcommands covering all core functionality of TPM 2.0, including cryptography, key management, PCR bank, NV storage, attestation, session and policy management, and utilities. Signed-off-by: Takuma IMAMURA <209989118+hyperfinitism@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 Co-Authored-By: Claude Sonnet 4.6 --- .gitattributes | 2 + Cargo.lock | 779 +++++++++++++++++++++++++++++ Cargo.toml | 23 + README.md | 85 +++- src/cli.rs | 254 ++++++++++ src/cmd/activatecredential.rs | 194 +++++++ src/cmd/certify.rs | 170 +++++++ src/cmd/certifycreation.rs | 211 ++++++++ src/cmd/changeauth.rs | 145 ++++++ src/cmd/changeeps.rs | 44 ++ src/cmd/changepps.rs | 44 ++ src/cmd/checkquote.rs | 203 ++++++++ src/cmd/clear.rs | 43 ++ src/cmd/clearcontrol.rs | 56 +++ src/cmd/clockrateadjust.rs | 67 +++ src/cmd/commit.rs | 121 +++++ src/cmd/create.rs | 179 +++++++ src/cmd/createak.rs | 236 +++++++++ src/cmd/createek.rs | 217 ++++++++ src/cmd/createpolicy.rs | 91 ++++ src/cmd/createprimary.rs | 152 ++++++ src/cmd/decrypt.rs | 135 +++++ src/cmd/dictionarylockout.rs | 91 ++++ src/cmd/duplicate.rs | 167 +++++++ src/cmd/ecdhkeygen.rs | 73 +++ src/cmd/ecdhzgen.rs | 99 ++++ src/cmd/ecephemeral.rs | 68 +++ src/cmd/encrypt.rs | 135 +++++ src/cmd/encryptdecrypt.rs | 131 +++++ src/cmd/eventlog.rs | 288 +++++++++++ src/cmd/evictcontrol.rs | 94 ++++ src/cmd/flushcontext.rs | 141 ++++++ src/cmd/getcap.rs | 231 +++++++++ src/cmd/getcommandauditdigest.rs | 129 +++++ src/cmd/geteccparameters.rs | 75 +++ src/cmd/getekcertificate.rs | 117 +++++ src/cmd/getrandom.rs | 66 +++ src/cmd/getsessionauditdigest.rs | 141 ++++++ src/cmd/gettestresult.rs | 34 ++ src/cmd/gettime.rs | 126 +++++ src/cmd/hash.rs | 98 ++++ src/cmd/hierarchycontrol.rs | 74 +++ src/cmd/import.rs | 151 ++++++ src/cmd/incrementalselftest.rs | 77 +++ src/cmd/load.rs | 97 ++++ src/cmd/loadexternal.rs | 113 +++++ src/cmd/makecredential.rs | 95 ++++ src/cmd/mod.rs | 100 ++++ src/cmd/nvcertify.rs | 155 ++++++ src/cmd/nvdefine.rs | 83 +++ src/cmd/nvextend.rs | 83 +++ src/cmd/nvincrement.rs | 75 +++ src/cmd/nvread.rs | 89 ++++ src/cmd/nvreadlock.rs | 62 +++ src/cmd/nvreadpublic.rs | 48 ++ src/cmd/nvsetbits.rs | 75 +++ src/cmd/nvundefine.rs | 65 +++ src/cmd/nvwrite.rs | 77 +++ src/cmd/nvwritelock.rs | 62 +++ src/cmd/pcrallocate.rs | 121 +++++ src/cmd/pcrevent.rs | 75 +++ src/cmd/pcrextend.rs | 105 ++++ src/cmd/pcrread.rs | 71 +++ src/cmd/pcrreset.rs | 62 +++ src/cmd/policyauthorize.rs | 108 ++++ src/cmd/policyauthorizenv.rs | 74 +++ src/cmd/policyauthvalue.rs | 55 ++ src/cmd/policycommandcode.rs | 89 ++++ src/cmd/policycountertimer.rs | 71 +++ src/cmd/policycphash.rs | 63 +++ src/cmd/policyduplicationselect.rs | 82 +++ src/cmd/policylocality.rs | 75 +++ src/cmd/policynamehash.rs | 64 +++ src/cmd/policynv.rs | 98 ++++ src/cmd/policynvwritten.rs | 58 +++ src/cmd/policyor.rs | 75 +++ src/cmd/policypassword.rs | 55 ++ src/cmd/policypcr.rs | 76 +++ src/cmd/policyrestart.rs | 42 ++ src/cmd/policysecret.rs | 136 +++++ src/cmd/policysigned.rs | 162 ++++++ src/cmd/policytemplate.rs | 68 +++ src/cmd/policyticket.rs | 108 ++++ src/cmd/print.rs | 201 ++++++++ src/cmd/quote.rs | 139 +++++ src/cmd/rcdecode.rs | 142 ++++++ src/cmd/readclock.rs | 50 ++ src/cmd/readpublic.rs | 58 +++ src/cmd/rsadecrypt.rs | 123 +++++ src/cmd/rsaencrypt.rs | 105 ++++ src/cmd/selftest.rs | 28 ++ src/cmd/send.rs | 77 +++ src/cmd/sessionconfig.rs | 105 ++++ src/cmd/setclock.rs | 54 ++ src/cmd/setcommandauditstatus.rs | 93 ++++ src/cmd/setprimarypolicy.rs | 79 +++ src/cmd/shutdown.rs | 31 ++ src/cmd/sign.rs | 114 +++++ src/cmd/startauthsession.rs | 81 +++ src/cmd/startup.rs | 30 ++ src/cmd/stirrandom.rs | 36 ++ src/cmd/testparms.rs | 82 +++ src/cmd/tpmhmac.rs | 97 ++++ src/cmd/unseal.rs | 64 +++ src/cmd/verifysignature.rs | 160 ++++++ src/cmd/zgen2phase.rs | 150 ++++++ src/context.rs | 13 + src/error.rs | 19 + src/handle.rs | 114 +++++ src/logger.rs | 19 + src/main.rs | 29 ++ src/output.rs | 19 + src/parse.rs | 301 +++++++++++ src/pcr.rs | 43 ++ src/raw_esys.rs | 375 ++++++++++++++ src/session.rs | 135 +++++ src/tcti.rs | 38 ++ 117 files changed, 12532 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/cli.rs create mode 100644 src/cmd/activatecredential.rs create mode 100644 src/cmd/certify.rs create mode 100644 src/cmd/certifycreation.rs create mode 100644 src/cmd/changeauth.rs create mode 100644 src/cmd/changeeps.rs create mode 100644 src/cmd/changepps.rs create mode 100644 src/cmd/checkquote.rs create mode 100644 src/cmd/clear.rs create mode 100644 src/cmd/clearcontrol.rs create mode 100644 src/cmd/clockrateadjust.rs create mode 100644 src/cmd/commit.rs create mode 100644 src/cmd/create.rs create mode 100644 src/cmd/createak.rs create mode 100644 src/cmd/createek.rs create mode 100644 src/cmd/createpolicy.rs create mode 100644 src/cmd/createprimary.rs create mode 100644 src/cmd/decrypt.rs create mode 100644 src/cmd/dictionarylockout.rs create mode 100644 src/cmd/duplicate.rs create mode 100644 src/cmd/ecdhkeygen.rs create mode 100644 src/cmd/ecdhzgen.rs create mode 100644 src/cmd/ecephemeral.rs create mode 100644 src/cmd/encrypt.rs create mode 100644 src/cmd/encryptdecrypt.rs create mode 100644 src/cmd/eventlog.rs create mode 100644 src/cmd/evictcontrol.rs create mode 100644 src/cmd/flushcontext.rs create mode 100644 src/cmd/getcap.rs create mode 100644 src/cmd/getcommandauditdigest.rs create mode 100644 src/cmd/geteccparameters.rs create mode 100644 src/cmd/getekcertificate.rs create mode 100644 src/cmd/getrandom.rs create mode 100644 src/cmd/getsessionauditdigest.rs create mode 100644 src/cmd/gettestresult.rs create mode 100644 src/cmd/gettime.rs create mode 100644 src/cmd/hash.rs create mode 100644 src/cmd/hierarchycontrol.rs create mode 100644 src/cmd/import.rs create mode 100644 src/cmd/incrementalselftest.rs create mode 100644 src/cmd/load.rs create mode 100644 src/cmd/loadexternal.rs create mode 100644 src/cmd/makecredential.rs create mode 100644 src/cmd/mod.rs create mode 100644 src/cmd/nvcertify.rs create mode 100644 src/cmd/nvdefine.rs create mode 100644 src/cmd/nvextend.rs create mode 100644 src/cmd/nvincrement.rs create mode 100644 src/cmd/nvread.rs create mode 100644 src/cmd/nvreadlock.rs create mode 100644 src/cmd/nvreadpublic.rs create mode 100644 src/cmd/nvsetbits.rs create mode 100644 src/cmd/nvundefine.rs create mode 100644 src/cmd/nvwrite.rs create mode 100644 src/cmd/nvwritelock.rs create mode 100644 src/cmd/pcrallocate.rs create mode 100644 src/cmd/pcrevent.rs create mode 100644 src/cmd/pcrextend.rs create mode 100644 src/cmd/pcrread.rs create mode 100644 src/cmd/pcrreset.rs create mode 100644 src/cmd/policyauthorize.rs create mode 100644 src/cmd/policyauthorizenv.rs create mode 100644 src/cmd/policyauthvalue.rs create mode 100644 src/cmd/policycommandcode.rs create mode 100644 src/cmd/policycountertimer.rs create mode 100644 src/cmd/policycphash.rs create mode 100644 src/cmd/policyduplicationselect.rs create mode 100644 src/cmd/policylocality.rs create mode 100644 src/cmd/policynamehash.rs create mode 100644 src/cmd/policynv.rs create mode 100644 src/cmd/policynvwritten.rs create mode 100644 src/cmd/policyor.rs create mode 100644 src/cmd/policypassword.rs create mode 100644 src/cmd/policypcr.rs create mode 100644 src/cmd/policyrestart.rs create mode 100644 src/cmd/policysecret.rs create mode 100644 src/cmd/policysigned.rs create mode 100644 src/cmd/policytemplate.rs create mode 100644 src/cmd/policyticket.rs create mode 100644 src/cmd/print.rs create mode 100644 src/cmd/quote.rs create mode 100644 src/cmd/rcdecode.rs create mode 100644 src/cmd/readclock.rs create mode 100644 src/cmd/readpublic.rs create mode 100644 src/cmd/rsadecrypt.rs create mode 100644 src/cmd/rsaencrypt.rs create mode 100644 src/cmd/selftest.rs create mode 100644 src/cmd/send.rs create mode 100644 src/cmd/sessionconfig.rs create mode 100644 src/cmd/setclock.rs create mode 100644 src/cmd/setcommandauditstatus.rs create mode 100644 src/cmd/setprimarypolicy.rs create mode 100644 src/cmd/shutdown.rs create mode 100644 src/cmd/sign.rs create mode 100644 src/cmd/startauthsession.rs create mode 100644 src/cmd/startup.rs create mode 100644 src/cmd/stirrandom.rs create mode 100644 src/cmd/testparms.rs create mode 100644 src/cmd/tpmhmac.rs create mode 100644 src/cmd/unseal.rs create mode 100644 src/cmd/verifysignature.rs create mode 100644 src/cmd/zgen2phase.rs create mode 100644 src/context.rs create mode 100644 src/error.rs create mode 100644 src/handle.rs create mode 100644 src/logger.rs create mode 100644 src/main.rs create mode 100644 src/output.rs create mode 100644 src/parse.rs create mode 100644 src/pcr.rs create mode 100644 src/raw_esys.rs create mode 100644 src/session.rs create mode 100644 src/tcti.rs 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 + +![SemVer](https://img.shields.io/badge/tpm2--cli-pre--release-39c5bb) +[![MSRV](https://img.shields.io/badge/MSRV-1.90.0-pink.svg)](https://doc.rust-lang.org/stable/releases.html#version-1900-2025-09-18) +[![License](https://img.shields.io/badge/License-Apache--2.0-red.svg)](https://opensource.org/licenses/Apache-2.0) + +

+ Logo of rust-tpm2-cli +

+ +**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>> { + match path { + Some(p) => { + let data = std::fs::read(p).with_context(|| format!("reading {}", p.display()))?; + Ok(Some(data)) + } + None => Ok(None), + } +} diff --git a/src/cmd/create.rs b/src/cmd/create.rs new file mode 100644 index 0000000..7cc7484 --- /dev/null +++ b/src/cmd/create.rs @@ -0,0 +1,179 @@ +use log::info; +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use tss_esapi::attributes::ObjectAttributesBuilder; +use tss_esapi::interface_types::algorithm::PublicAlgorithm; +use tss_esapi::interface_types::ecc::EccCurve; +use tss_esapi::interface_types::key_bits::RsaKeyBits; +use tss_esapi::structures::{ + EccScheme, HashScheme, KeyDerivationFunctionScheme, Public, PublicBuilder, + PublicEccParametersBuilder, PublicRsaParametersBuilder, RsaExponent, RsaScheme, +}; + +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::execute_with_optional_session; + +/// Create a child key under a parent key. +#[derive(Parser)] +pub struct CreateCmd { + /// Parent key context file path + #[arg( + short = 'C', + long = "parent-context", + conflicts_with = "parent_context_handle" + )] + pub parent_context: Option, + + /// Parent key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "parent-context-handle", value_parser = parse_hex_u32, conflicts_with = "parent_context")] + pub parent_context_handle: Option, + + /// Key algorithm (rsa, ecc) + #[arg(short = 'G', long = "key-algorithm", default_value = "rsa")] + pub algorithm: String, + + /// Hash algorithm + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Authorization value for the new key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Output file for the private portion + #[arg(short = 'r', long = "private")] + pub private_out: Option, + + /// Output file for the public portion + #[arg(short = 'u', long = "public")] + pub public_out: Option, + + /// RSA key size in bits + #[arg(long = "key-size", default_value = "2048")] + pub key_size: u16, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl CreateCmd { + 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 parent_handle = load_key_from_source(&mut ctx, &self.parent_context_source()?)?; + let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?; + let public = build_signing_public(&self.algorithm, hash_alg, self.key_size)?; + + let auth = match &self.auth { + Some(a) => Some(parse::parse_auth(a)?), + None => None, + }; + + let session_path = self.session.as_deref(); + let result = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.create( + parent_handle, + public.clone(), + auth.clone(), + None, + None, + None, + ) + }) + .context("TPM2_Create failed")?; + + info!("key created successfully"); + + if let Some(ref path) = self.private_out { + let bytes = result.out_private.value(); + std::fs::write(path, bytes)?; + info!("private portion saved to {}", path.display()); + } + + if let Some(ref path) = self.public_out { + std::fs::write(path, format!("{:?}", result.out_public))?; + info!("public portion saved to {}", path.display()); + } + + Ok(()) + } +} + +fn build_signing_public( + alg: &str, + hash_alg: tss_esapi::interface_types::algorithm::HashingAlgorithm, + key_size: u16, +) -> anyhow::Result { + let attributes = ObjectAttributesBuilder::new() + .with_fixed_tpm(true) + .with_fixed_parent(true) + .with_sensitive_data_origin(true) + .with_user_with_auth(true) + .with_sign_encrypt(true) + .build() + .context("failed to build object attributes")?; + + let builder = PublicBuilder::new() + .with_name_hashing_algorithm(hash_alg) + .with_object_attributes(attributes); + + match alg.to_lowercase().as_str() { + "rsa" => { + let bits = match key_size { + 1024 => RsaKeyBits::Rsa1024, + 2048 => RsaKeyBits::Rsa2048, + 3072 => RsaKeyBits::Rsa3072, + 4096 => RsaKeyBits::Rsa4096, + _ => bail!("unsupported RSA key size: {key_size}"), + }; + let params = PublicRsaParametersBuilder::new() + .with_scheme(RsaScheme::RsaSsa(HashScheme::new(hash_alg))) + .with_key_bits(bits) + .with_exponent(RsaExponent::default()) + .with_is_signing_key(true) + .build() + .context("failed to build RSA parameters")?; + + builder + .with_public_algorithm(PublicAlgorithm::Rsa) + .with_rsa_parameters(params) + .with_rsa_unique_identifier(Default::default()) + .build() + .context("failed to build RSA public") + } + "ecc" => { + let params = PublicEccParametersBuilder::new() + .with_ecc_scheme(EccScheme::EcDsa(HashScheme::new(hash_alg))) + .with_curve(EccCurve::NistP256) + .with_is_signing_key(true) + .with_key_derivation_function_scheme(KeyDerivationFunctionScheme::Null) + .build() + .context("failed to build ECC parameters")?; + + builder + .with_public_algorithm(PublicAlgorithm::Ecc) + .with_ecc_parameters(params) + .with_ecc_unique_identifier(Default::default()) + .build() + .context("failed to build ECC public") + } + _ => bail!("unsupported key algorithm: {alg}"), + } +} diff --git a/src/cmd/createak.rs b/src/cmd/createak.rs new file mode 100644 index 0000000..6b5158e --- /dev/null +++ b/src/cmd/createak.rs @@ -0,0 +1,236 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::attributes::ObjectAttributesBuilder; +use tss_esapi::handles::ObjectHandle; +use tss_esapi::interface_types::algorithm::{HashingAlgorithm, PublicAlgorithm}; +use tss_esapi::interface_types::ecc::EccCurve; +use tss_esapi::interface_types::key_bits::RsaKeyBits; +use tss_esapi::interface_types::resource_handles::HierarchyAuth; +use tss_esapi::interface_types::session_handles::AuthSession; +use tss_esapi::structures::{ + EccScheme, HashScheme, KeyDerivationFunctionScheme, Public, PublicBuilder, + PublicEccParametersBuilder, PublicRsaParametersBuilder, RsaExponent, RsaScheme, + SymmetricDefinitionObject, +}; +use tss_esapi::traits::Marshall; + +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, start_ek_policy_session}; + +/// Create an attestation key (AK) under an endorsement key. +/// +/// The AK is a restricted signing key created as a child of the specified +/// EK. Its name can be used with `tpm2 makecredential` / `activatecredential` +/// for remote attestation flows. +#[derive(Parser)] +pub struct CreateAkCmd { + /// EK context file path + #[arg(short = 'C', long = "ek-context", conflicts_with = "ek_context_handle")] + pub ek_context: Option, + + /// EK handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "ek-context-handle", value_parser = parse_hex_u32, conflicts_with = "ek_context")] + pub ek_context_handle: Option, + + /// Output context file for the attestation key + #[arg(short = 'c', long = "ak-context")] + pub ak_context: PathBuf, + + /// Key algorithm (ecc, rsa, keyedhash) + #[arg(short = 'G', long = "key-algorithm", default_value = "rsa")] + pub algorithm: String, + + /// Hash algorithm (sha1, sha256, sha384, sha512) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Endorsement hierarchy auth value + #[arg(short = 'P', long = "eh-auth")] + pub eh_auth: Option, + + /// Auth value for the attestation key + #[arg(short = 'p', long = "ak-auth")] + pub ak_auth: Option, + + /// Output file for AK public portion (TPM2B_PUBLIC, marshaled binary) + #[arg(short = 'u', long = "public")] + pub public: Option, + + /// Output file for AK private portion (TPM2B_PRIVATE, marshaled binary) + #[arg(short = 'r', long = "private")] + pub private: Option, + + /// Output file for AK name (binary) + #[arg(short = 'n', long = "ak-name")] + pub ak_name: Option, +} + +impl CreateAkCmd { + fn ek_context_source(&self) -> anyhow::Result { + match (&self.ek_context, self.ek_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => { + anyhow::bail!("exactly one of --ek-context or --ek-context-handle must be provided") + } + } + } + + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?; + let ak_template = build_ak_public(&self.algorithm, hash_alg)?; + + let ek_handle = load_key_from_source(&mut ctx, &self.ek_context_source()?)?; + + let ak_auth = match &self.ak_auth { + Some(a) => Some(parse::parse_auth(a)?), + None => None, + }; + + // Set endorsement hierarchy auth if provided. + if let Some(ref a) = self.eh_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")?; + } + + // --- Create AK under EK (requires EK policy session) --- + let policy_session = start_ek_policy_session(&mut ctx)?; + ctx.set_sessions((Some(AuthSession::PolicySession(policy_session)), None, None)); + let result = ctx + .create( + ek_handle, + ak_template.clone(), + ak_auth.clone(), + None, + None, + None, + ) + .context("TPM2_Create failed")?; + ctx.clear_sessions(); + + flush_policy_session(&mut ctx, policy_session)?; + + // --- Load AK under EK (requires a fresh policy session) --- + let policy_session = start_ek_policy_session(&mut ctx)?; + ctx.set_sessions((Some(AuthSession::PolicySession(policy_session)), None, None)); + let ak_handle = ctx + .load( + ek_handle, + result.out_private.clone(), + result.out_public.clone(), + ) + .context("TPM2_Load failed")?; + ctx.clear_sessions(); + + flush_policy_session(&mut ctx, policy_session)?; + + info!("AK handle: 0x{:08x}", u32::from(ak_handle)); + + // Read the AK name from the TPM. + let (_, ak_name_obj, _) = ctx + .execute_without_session(|ctx| ctx.read_public(ak_handle)) + .context("TPM2_ReadPublic failed")?; + info!("AK name: 0x{}", hex::encode(ak_name_obj.value())); + + // Save outputs. + if let Some(ref path) = self.public { + let pub_bytes = result + .out_public + .marshall() + .context("failed to marshal public")?; + std::fs::write(path, &pub_bytes) + .with_context(|| format!("writing public to {}", path.display()))?; + info!("public saved to {}", path.display()); + } + + if let Some(ref path) = self.private { + std::fs::write(path, result.out_private.value()) + .with_context(|| format!("writing private to {}", path.display()))?; + info!("private saved to {}", path.display()); + } + + if let Some(ref path) = self.ak_name { + std::fs::write(path, ak_name_obj.value()) + .with_context(|| format!("writing AK name to {}", path.display()))?; + info!("AK name saved to {}", path.display()); + } + + // Save AK context. + let saved = ctx + .context_save(ak_handle.into()) + .context("context_save failed")?; + let json = serde_json::to_string(&saved)?; + std::fs::write(&self.ak_context, json) + .with_context(|| format!("writing AK context to {}", self.ak_context.display()))?; + info!("AK context saved to {}", self.ak_context.display()); + + Ok(()) + } +} + +fn build_ak_public(alg: &str, hash_alg: HashingAlgorithm) -> anyhow::Result { + let attributes = ObjectAttributesBuilder::new() + .with_fixed_tpm(true) + .with_fixed_parent(true) + .with_sensitive_data_origin(true) + .with_user_with_auth(true) + .with_sign_encrypt(true) + .with_restricted(true) + .build() + .context("failed to build object attributes")?; + + let builder = PublicBuilder::new() + .with_name_hashing_algorithm(HashingAlgorithm::Sha256) + .with_object_attributes(attributes); + + match alg.to_lowercase().as_str() { + "rsa" => { + let params = PublicRsaParametersBuilder::new() + .with_scheme(RsaScheme::RsaSsa(HashScheme::new(hash_alg))) + .with_key_bits(RsaKeyBits::Rsa2048) + .with_exponent(RsaExponent::default()) + .with_is_signing_key(true) + .with_restricted(true) + .with_symmetric(SymmetricDefinitionObject::Null) + .build() + .context("failed to build RSA parameters")?; + + builder + .with_public_algorithm(PublicAlgorithm::Rsa) + .with_rsa_parameters(params) + .with_rsa_unique_identifier(Default::default()) + .build() + .context("failed to build RSA AK public") + } + "ecc" => { + let params = PublicEccParametersBuilder::new() + .with_ecc_scheme(EccScheme::EcDsa(HashScheme::new(hash_alg))) + .with_curve(EccCurve::NistP256) + .with_is_signing_key(true) + .with_restricted(true) + .with_symmetric(SymmetricDefinitionObject::Null) + .with_key_derivation_function_scheme(KeyDerivationFunctionScheme::Null) + .build() + .context("failed to build ECC parameters")?; + + builder + .with_public_algorithm(PublicAlgorithm::Ecc) + .with_ecc_parameters(params) + .with_ecc_unique_identifier(Default::default()) + .build() + .context("failed to build ECC AK public") + } + _ => bail!("unsupported AK algorithm: {alg}; supported: rsa, ecc"), + } +} diff --git a/src/cmd/createek.rs b/src/cmd/createek.rs new file mode 100644 index 0000000..ab964da --- /dev/null +++ b/src/cmd/createek.rs @@ -0,0 +1,217 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::attributes::ObjectAttributesBuilder; +use tss_esapi::handles::{ObjectHandle, PersistentTpmHandle}; +use tss_esapi::interface_types::algorithm::{HashingAlgorithm, PublicAlgorithm}; +use tss_esapi::interface_types::dynamic_handles::Persistent; +use tss_esapi::interface_types::ecc::EccCurve; +use tss_esapi::interface_types::key_bits::RsaKeyBits; +use tss_esapi::interface_types::resource_handles::{Hierarchy, HierarchyAuth, Provision}; +use tss_esapi::structures::{ + Digest, EccScheme, KeyDerivationFunctionScheme, Public, PublicBuilder, + PublicEccParametersBuilder, PublicRsaParametersBuilder, RsaExponent, RsaScheme, + SymmetricDefinitionObject, +}; +use tss_esapi::traits::Marshall; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse; +use crate::session::execute_with_optional_session; + +/// TCG profile-compliant EK auth policy digest (SHA-256). +/// +/// This is the well-known policy digest for `PolicySecret(TPM_RH_ENDORSEMENT)`, +/// required by the default EK templates in the TCG EK Credential Profile. +const EK_AUTH_POLICY_SHA256: [u8; 32] = [ + 0x83, 0x71, 0x97, 0x67, 0x44, 0x84, 0xb3, 0xf8, 0x1a, 0x90, 0xcc, 0x8d, 0x46, 0xa5, 0xd7, 0x24, + 0xfd, 0x52, 0xd7, 0x6e, 0x06, 0x52, 0x0b, 0x64, 0xf2, 0xa1, 0xda, 0x1b, 0x33, 0x14, 0x69, 0xaa, +]; + +/// Create a TCG-compliant endorsement key (EK). +/// +/// Generates an EK as the primary object of the endorsement hierarchy. The +/// key can be saved as a transient context file or persisted directly to a +/// permanent handle. +#[derive(Parser)] +pub struct CreateEkCmd { + /// Key algorithm (rsa, ecc) + #[arg(short = 'G', long = "key-algorithm", default_value = "rsa")] + pub algorithm: String, + + /// Endorsement hierarchy auth value + #[arg(short = 'P', long = "eh-auth")] + pub eh_auth: Option, + + /// Owner hierarchy auth value (required when persisting) + #[arg(short = 'w', long = "owner-auth")] + pub owner_auth: Option, + + /// Output context file path, or persistent handle (e.g. 0x81010001) + #[arg(short = 'c', long = "ek-context")] + pub ek_context: Option, + + /// Output file for the public portion (TPM2B_PUBLIC, marshaled binary) + #[arg(short = 'u', long = "public")] + pub public: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl CreateEkCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let public_template = build_ek_public(&self.algorithm)?; + + // Set endorsement hierarchy auth if provided. + if let Some(ref a) = self.eh_auth { + let auth = parse::parse_auth(a)?; + let hier_obj: ObjectHandle = HierarchyAuth::Endorsement.into(); + ctx.tr_set_auth(hier_obj, auth) + .context("failed to set endorsement hierarchy auth")?; + } + + let session_path = self.session.as_deref(); + let result = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.create_primary( + Hierarchy::Endorsement, + public_template.clone(), + None, // EK uses policy auth, not password + None, + None, + None, + ) + }) + .context("TPM2_CreatePrimary failed")?; + + info!("handle: 0x{:08x}", u32::from(result.key_handle)); + + // Write public portion if requested. + if let Some(ref path) = self.public { + let pub_bytes = result + .out_public + .marshall() + .context("failed to marshal public key")?; + std::fs::write(path, &pub_bytes) + .with_context(|| format!("writing public key to {}", path.display()))?; + info!("public key saved to {}", path.display()); + } + + // Save context or persist to a permanent handle. + if let Some(ref ek_ctx) = self.ek_context { + if let Some(raw) = try_parse_persistent_handle(ek_ctx) { + self.persist_ek(&mut ctx, result.key_handle.into(), raw)?; + } else { + let saved = ctx + .context_save(result.key_handle.into()) + .context("context_save failed")?; + let json = serde_json::to_string(&saved)?; + std::fs::write(ek_ctx, json) + .with_context(|| format!("writing context to {ek_ctx}"))?; + info!("context saved to {ek_ctx}"); + } + } + + Ok(()) + } + + fn persist_ek( + &self, + ctx: &mut tss_esapi::Context, + obj_handle: ObjectHandle, + raw_handle: u32, + ) -> anyhow::Result<()> { + let persistent_tpm_handle = PersistentTpmHandle::new(raw_handle) + .map_err(|e| anyhow::anyhow!("invalid persistent handle: {e}"))?; + let persistent: Persistent = persistent_tpm_handle.into(); + + // Set owner hierarchy auth if provided (evict_control uses owner auth). + if let Some(ref a) = self.owner_auth { + let auth = parse::parse_auth(a)?; + let hier_obj: ObjectHandle = HierarchyAuth::Owner.into(); + ctx.tr_set_auth(hier_obj, auth) + .context("failed to set owner hierarchy auth")?; + } + + ctx.execute_with_nullauth_session(|ctx| -> tss_esapi::Result<_> { + ctx.evict_control(Provision::Owner, obj_handle, persistent) + }) + .context("TPM2_EvictControl failed")?; + + info!("EK persisted at 0x{:08x}", raw_handle); + Ok(()) + } +} + +/// Try to parse a string as a hex persistent handle (0x-prefixed). +/// Returns `None` if the string does not look like a hex handle. +fn try_parse_persistent_handle(s: &str) -> Option { + let stripped = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))?; + u32::from_str_radix(stripped, 16).ok() +} + +fn build_ek_public(alg: &str) -> anyhow::Result { + let auth_policy = Digest::try_from(EK_AUTH_POLICY_SHA256.as_slice()) + .map_err(|e| anyhow::anyhow!("invalid auth policy: {e}"))?; + + let attributes = ObjectAttributesBuilder::new() + .with_fixed_tpm(true) + .with_fixed_parent(true) + .with_sensitive_data_origin(true) + .with_admin_with_policy(true) + .with_restricted(true) + .with_decrypt(true) + .build() + .context("failed to build object attributes")?; + + let builder = PublicBuilder::new() + .with_name_hashing_algorithm(HashingAlgorithm::Sha256) + .with_object_attributes(attributes) + .with_auth_policy(auth_policy); + + match alg.to_lowercase().as_str() { + "rsa" => { + let params = PublicRsaParametersBuilder::new() + .with_scheme(RsaScheme::Null) + .with_key_bits(RsaKeyBits::Rsa2048) + .with_exponent(RsaExponent::default()) + .with_is_decryption_key(true) + .with_restricted(true) + .with_symmetric(SymmetricDefinitionObject::AES_128_CFB) + .build() + .context("failed to build RSA parameters")?; + + builder + .with_public_algorithm(PublicAlgorithm::Rsa) + .with_rsa_parameters(params) + .with_rsa_unique_identifier(Default::default()) + .build() + .context("failed to build RSA EK public template") + } + "ecc" => { + let params = PublicEccParametersBuilder::new() + .with_ecc_scheme(EccScheme::Null) + .with_curve(EccCurve::NistP256) + .with_is_decryption_key(true) + .with_restricted(true) + .with_symmetric(SymmetricDefinitionObject::AES_128_CFB) + .with_key_derivation_function_scheme(KeyDerivationFunctionScheme::Null) + .build() + .context("failed to build ECC parameters")?; + + builder + .with_public_algorithm(PublicAlgorithm::Ecc) + .with_ecc_parameters(params) + .with_ecc_unique_identifier(Default::default()) + .build() + .context("failed to build ECC EK public template") + } + _ => bail!("unsupported EK algorithm: {alg}; supported: rsa, ecc"), + } +} diff --git a/src/cmd/createpolicy.rs b/src/cmd/createpolicy.rs new file mode 100644 index 0000000..9e2c6c4 --- /dev/null +++ b/src/cmd/createpolicy.rs @@ -0,0 +1,91 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::SymmetricDefinition; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Create a policy from a policy script (trial session). +/// +/// Starts a trial policy session, prints its digest, and saves it. +/// Complex multi-step policies should be built by chaining individual +/// policy commands (policypcr, policycommandcode, etc.) against a +/// trial session created with `startauthsession --policy-session`. +/// +/// This command provides a simple shortcut for common single-step policies. +#[derive(Parser)] +pub struct CreatePolicyCmd { + /// Hash algorithm for the policy (default: sha256) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: PathBuf, + + /// Policy type: pcr + #[arg(long = "policy-pcr")] + pub policy_pcr: bool, + + /// PCR selection for --policy-pcr (e.g. sha256:0,1,2) + #[arg(short = 'l', long = "pcr-list")] + pub pcr_list: Option, +} + +impl CreatePolicyCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + let hash_alg = crate::parse::parse_hashing_algorithm(&self.hash_algorithm)?; + + // Start a trial session. + let session = ctx + .start_auth_session( + None, + None, + None, + SessionType::Trial, + SymmetricDefinition::AES_128_CFB, + hash_alg, + ) + .context("failed to start trial session")? + .ok_or_else(|| anyhow::anyhow!("no session returned"))?; + + let policy_session: tss_esapi::interface_types::session_handles::PolicySession = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected policy session"))?; + + if self.policy_pcr { + let pcr_spec = self + .pcr_list + .as_deref() + .ok_or_else(|| anyhow::anyhow!("--pcr-list required with --policy-pcr"))?; + let pcr_selection = crate::parse::parse_pcr_selection(pcr_spec)?; + ctx.policy_pcr(policy_session, Default::default(), pcr_selection) + .context("TPM2_PolicyPCR failed")?; + } + + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + + std::fs::write(&self.policy, digest.value()) + .with_context(|| format!("writing policy to {}", self.policy.display()))?; + info!( + "policy digest saved to {} ({} bytes)", + self.policy.display(), + digest.value().len() + ); + + // Flush the trial session. + let obj_handle: ObjectHandle = SessionHandle::from(policy_session).into(); + ctx.flush_context(obj_handle) + .context("failed to flush trial session")?; + + Ok(()) + } +} diff --git a/src/cmd/createprimary.rs b/src/cmd/createprimary.rs new file mode 100644 index 0000000..94c2f27 --- /dev/null +++ b/src/cmd/createprimary.rs @@ -0,0 +1,152 @@ +use log::info; +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use tss_esapi::attributes::ObjectAttributesBuilder; +use tss_esapi::interface_types::algorithm::PublicAlgorithm; +use tss_esapi::interface_types::ecc::EccCurve; +use tss_esapi::interface_types::key_bits::RsaKeyBits; +use tss_esapi::structures::{ + EccScheme, KeyDerivationFunctionScheme, Public, PublicBuilder, PublicEccParametersBuilder, + PublicRsaParametersBuilder, RsaExponent, RsaScheme, SymmetricDefinitionObject, +}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse; +use crate::session::execute_with_optional_session; + +/// Create a primary key under a hierarchy. +#[derive(Parser)] +pub struct CreatePrimaryCmd { + /// Hierarchy (o/owner, p/platform, e/endorsement, n/null) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Key algorithm (rsa, ecc) + #[arg(short = 'G', long = "key-algorithm", default_value = "rsa")] + pub algorithm: String, + + /// Hash algorithm (sha1, sha256, sha384, sha512) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Authorization value for the key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Output context file for the created primary key + #[arg(short = 'c', long = "context")] + pub context: Option, + + /// RSA key size in bits (default: 2048) + #[arg(long = "key-size", default_value = "2048")] + pub key_size: u16, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl CreatePrimaryCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let hierarchy = parse::parse_hierarchy(&self.hierarchy)?; + let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?; + let public = build_public(&self.algorithm, hash_alg, self.key_size)?; + + let auth = match &self.auth { + Some(a) => Some(parse::parse_auth(a)?), + None => None, + }; + + let session_path = self.session.as_deref(); + let result = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.create_primary(hierarchy, public.clone(), auth.clone(), None, None, None) + }) + .context("TPM2_CreatePrimary failed")?; + + info!("handle: 0x{:08x}", u32::from(result.key_handle)); + + // Save context if requested + if let Some(ref path) = self.context { + let saved = ctx + .context_save(result.key_handle.into()) + .context("context_save failed")?; + let json = serde_json::to_string(&saved)?; + std::fs::write(path, json)?; + info!("context saved to {}", path.display()); + } + + Ok(()) + } +} + +fn build_public( + alg: &str, + hash_alg: tss_esapi::interface_types::algorithm::HashingAlgorithm, + key_size: u16, +) -> anyhow::Result { + let attributes = ObjectAttributesBuilder::new() + .with_fixed_tpm(true) + .with_fixed_parent(true) + .with_sensitive_data_origin(true) + .with_user_with_auth(true) + .with_decrypt(true) + .with_restricted(true) + .build() + .context("failed to build object attributes")?; + + let builder = PublicBuilder::new() + .with_name_hashing_algorithm(hash_alg) + .with_object_attributes(attributes); + + match alg.to_lowercase().as_str() { + "rsa" => { + let bits = match key_size { + 1024 => RsaKeyBits::Rsa1024, + 2048 => RsaKeyBits::Rsa2048, + 3072 => RsaKeyBits::Rsa3072, + 4096 => RsaKeyBits::Rsa4096, + _ => bail!("unsupported RSA key size: {key_size}"), + }; + let params = PublicRsaParametersBuilder::new() + .with_scheme(RsaScheme::Null) + .with_key_bits(bits) + .with_exponent(RsaExponent::default()) + .with_is_decryption_key(true) + .with_restricted(true) + .with_symmetric(SymmetricDefinitionObject::AES_128_CFB) + .build() + .context("failed to build RSA parameters")?; + + builder + .with_public_algorithm(PublicAlgorithm::Rsa) + .with_rsa_parameters(params) + .with_rsa_unique_identifier(Default::default()) + .build() + .context("failed to build RSA public") + } + "ecc" => { + let params = PublicEccParametersBuilder::new() + .with_ecc_scheme(EccScheme::Null) + .with_curve(EccCurve::NistP256) + .with_is_decryption_key(true) + .with_restricted(true) + .with_symmetric(SymmetricDefinitionObject::AES_128_CFB) + .with_key_derivation_function_scheme(KeyDerivationFunctionScheme::Null) + .build() + .context("failed to build ECC parameters")?; + + builder + .with_public_algorithm(PublicAlgorithm::Ecc) + .with_ecc_parameters(params) + .with_ecc_unique_identifier(Default::default()) + .build() + .context("failed to build ECC public") + } + _ => bail!("unsupported key algorithm: {alg}"), + } +} diff --git a/src/cmd/decrypt.rs b/src/cmd/decrypt.rs new file mode 100644 index 0000000..c1c45fa --- /dev/null +++ b/src/cmd/decrypt.rs @@ -0,0 +1,135 @@ +use std::io::Read; +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{InitialValue, MaxBuffer}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_key_from_source}; +use crate::output; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Decrypt data with a symmetric key held by the TPM. +/// +/// Reads ciphertext from a file (or stdin) and writes plaintext to the +/// output file (or stdout). +#[derive(Parser)] +pub struct DecryptCmd { + /// Symmetric key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// Symmetric key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Authorization value for the key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Cipher mode (cfb, cbc, ecb, ofb, ctr) + #[arg(short = 'G', long = "mode", default_value = "cfb")] + pub mode: String, + + /// Initialization vector input file (default: all zeros) + #[arg(short = 'i', long = "iv")] + pub iv_input: Option, + + /// Output file for the IV produced by the TPM (for chaining) + #[arg(long = "iv-out")] + pub iv_output: Option, + + /// Output file for the decrypted data (default: stdout) + #[arg(short = 'o', long = "output")] + pub output: Option, + + /// Input file to decrypt (default: stdin) + #[arg()] + pub input: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl DecryptCmd { + fn context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-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.context_source()?)?; + let mode = parse::parse_symmetric_mode(&self.mode)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(key_handle.into(), auth) + .context("tr_set_auth failed")?; + } + + let ciphertext = read_input(&self.input)?; + let iv_in = read_iv(&self.iv_input)?; + + let data = + MaxBuffer::try_from(ciphertext).map_err(|e| anyhow::anyhow!("input too large: {e}"))?; + + let session_path = self.session.as_deref(); + let (plaintext, iv_out) = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.encrypt_decrypt_2(key_handle, true, mode, data.clone(), iv_in.clone()) + }) + .context("TPM2_EncryptDecrypt2 (decrypt) failed")?; + + if let Some(ref path) = self.output { + output::write_to_file(path, plaintext.value())?; + info!("plaintext written to {}", path.display()); + } else { + output::write_binary_stdout(plaintext.value())?; + } + + if let Some(ref path) = self.iv_output { + output::write_to_file(path, iv_out.value())?; + info!("IV written to {}", path.display()); + } + + Ok(()) + } +} + +fn read_input(path: &Option) -> anyhow::Result> { + match path { + Some(p) => std::fs::read(p).with_context(|| format!("reading input: {}", p.display())), + None => { + let mut buf = Vec::new(); + std::io::stdin() + .read_to_end(&mut buf) + .context("reading stdin")?; + Ok(buf) + } + } +} + +fn read_iv(path: &Option) -> anyhow::Result { + match path { + Some(p) => { + let data = std::fs::read(p).with_context(|| format!("reading IV: {}", p.display()))?; + InitialValue::try_from(data).map_err(|e| anyhow::anyhow!("invalid IV: {e}")) + } + None => Ok(InitialValue::default()), + } +} diff --git a/src/cmd/dictionarylockout.rs b/src/cmd/dictionarylockout.rs new file mode 100644 index 0000000..d4f578d --- /dev/null +++ b/src/cmd/dictionarylockout.rs @@ -0,0 +1,91 @@ +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Reset the dictionary attack lockout or configure DA parameters. +/// +/// Wraps TPM2_DictionaryAttackLockReset and TPM2_DictionaryAttackParameters (raw FFI). +#[derive(Parser)] +pub struct DictionaryLockoutCmd { + /// Auth value for the lockout hierarchy + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Reset the DA lockout counter + #[arg(short = 'c', long = "clear-lockout")] + pub clear_lockout: bool, + + /// Max number of authorization failures before lockout + #[arg(long = "max-tries")] + pub max_tries: Option, + + /// Lockout recovery time in seconds + #[arg(long = "recovery-time")] + pub recovery_time: Option, + + /// Lockout auth failure recovery time in seconds + #[arg(long = "lockout-recovery-time")] + pub lockout_recovery_time: Option, + + /// Setup mode: configure DA parameters (requires --max-tries, --recovery-time, --lockout-recovery-time) + #[arg(short = 's', long = "setup-parameters")] + pub setup_parameters: bool, +} + +impl DictionaryLockoutCmd { + 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_LOCKOUT, auth.value())?; + } + + if self.clear_lockout { + unsafe { + let rc = Esys_DictionaryAttackLockReset( + raw.ptr(), + ESYS_TR_RH_LOCKOUT, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + ); + if rc != 0 { + anyhow::bail!("Esys_DictionaryAttackLockReset failed: 0x{rc:08x}"); + } + } + info!("DA lockout counter cleared"); + } + + if self.setup_parameters { + let max_tries = self.max_tries.unwrap_or(32); + let recovery_time = self.recovery_time.unwrap_or(10); + let lockout_recovery = self.lockout_recovery_time.unwrap_or(10); + + unsafe { + let rc = Esys_DictionaryAttackParameters( + raw.ptr(), + ESYS_TR_RH_LOCKOUT, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + max_tries, + recovery_time, + lockout_recovery, + ); + if rc != 0 { + anyhow::bail!("Esys_DictionaryAttackParameters failed: 0x{rc:08x}"); + } + } + info!( + "DA parameters set: max_tries={max_tries}, recovery_time={recovery_time}, lockout_recovery={lockout_recovery}" + ); + } + + Ok(()) + } +} diff --git a/src/cmd/duplicate.rs b/src/cmd/duplicate.rs new file mode 100644 index 0000000..137cb63 --- /dev/null +++ b/src/cmd/duplicate.rs @@ -0,0 +1,167 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{Data, SymmetricDefinitionObject}; + +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; + +/// Duplicate a loaded object for use in a different hierarchy. +/// +/// Wraps TPM2_Duplicate. +#[derive(Parser)] +pub struct DuplicateCmd { + /// Object to duplicate (context file path) + #[arg( + short = 'c', + long = "object-context", + conflicts_with = "object_context_handle" + )] + pub object_context: Option, + + /// Object to duplicate (hex handle, e.g. 0x81000001) + #[arg(long = "object-context-handle", value_parser = parse_hex_u32, conflicts_with = "object_context")] + pub object_context_handle: Option, + + /// New parent key context file path + #[arg(short = 'C', long = "parent-context", conflicts_with_all = ["parent_context_handle", "parent_context_null"])] + pub parent_context: Option, + + /// New parent key handle (hex, e.g. 0x81000001) + #[arg(long = "parent-context-handle", value_parser = parse_hex_u32, conflicts_with_all = ["parent_context", "parent_context_null"])] + pub parent_context_handle: Option, + + /// Use a null parent handle + #[arg(long = "parent-context-null", conflicts_with_all = ["parent_context", "parent_context_handle"])] + pub parent_context_null: bool, + + /// Auth value for the object + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Symmetric algorithm for inner wrapper (aes128cfb, null) + #[arg(short = 'G', long = "wrapper-algorithm", default_value = "null")] + pub wrapper_algorithm: String, + + /// Input encryption key file (optional) + #[arg(short = 'i', long = "encryptionkey-in")] + pub encryption_key_in: Option, + + /// Output file for the encrypted duplicate + #[arg(short = 'r', long = "private")] + pub private_out: PathBuf, + + /// Output file for the encryption key (if generated) + #[arg(short = 'k', long = "encryptionkey-out")] + pub encryption_key_out: Option, + + /// Output file for the encrypted seed + #[arg(short = 's', long = "encrypted-seed")] + pub encrypted_seed: PathBuf, + + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl DuplicateCmd { + 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 object_handle = load_object_from_source(&mut ctx, &self.object_context_source()?)?; + let parent_handle = if self.parent_context_null { + tss_esapi::handles::ObjectHandle::Null + } else { + load_object_from_source(&mut ctx, &self.parent_context_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 encryption_key = match &self.encryption_key_in { + Some(path) => { + let data = std::fs::read(path) + .with_context(|| format!("reading encryption key from {}", path.display()))?; + Some( + Data::try_from(data) + .map_err(|e| anyhow::anyhow!("encryption key too large: {e}"))?, + ) + } + None => None, + }; + + let sym_alg = parse_wrapper_algorithm(&self.wrapper_algorithm)?; + + let session_path = self.session.as_deref(); + let (enc_key, duplicate_private, encrypted_secret) = + execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.duplicate( + object_handle, + parent_handle, + encryption_key.clone(), + sym_alg, + ) + }) + .context("TPM2_Duplicate failed")?; + + std::fs::write(&self.private_out, duplicate_private.value()) + .with_context(|| format!("writing private to {}", self.private_out.display()))?; + info!("duplicate private saved to {}", self.private_out.display()); + + std::fs::write(&self.encrypted_seed, encrypted_secret.value()) + .with_context(|| format!("writing seed to {}", self.encrypted_seed.display()))?; + info!("encrypted seed saved to {}", self.encrypted_seed.display()); + + if let Some(ref path) = self.encryption_key_out { + std::fs::write(path, enc_key.value()) + .with_context(|| format!("writing encryption key to {}", path.display()))?; + info!("encryption key saved to {}", path.display()); + } + + Ok(()) + } +} + +fn parse_wrapper_algorithm(s: &str) -> anyhow::Result { + match s.to_lowercase().as_str() { + "null" => Ok(SymmetricDefinitionObject::Null), + "aes128cfb" | "aes" => Ok(SymmetricDefinitionObject::Aes { + key_bits: tss_esapi::interface_types::key_bits::AesKeyBits::Aes128, + mode: tss_esapi::interface_types::algorithm::SymmetricMode::Cfb, + }), + "aes256cfb" => Ok(SymmetricDefinitionObject::Aes { + key_bits: tss_esapi::interface_types::key_bits::AesKeyBits::Aes256, + mode: tss_esapi::interface_types::algorithm::SymmetricMode::Cfb, + }), + _ => anyhow::bail!("unsupported wrapper algorithm: {s}"), + } +} diff --git a/src/cmd/ecdhkeygen.rs b/src/cmd/ecdhkeygen.rs new file mode 100644 index 0000000..ae8df53 --- /dev/null +++ b/src/cmd/ecdhkeygen.rs @@ -0,0 +1,73 @@ +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_key_from_source}; +use crate::parse::parse_hex_u32; + +/// Generate an ephemeral ECDH key pair and compute a shared secret. +/// +/// Wraps TPM2_ECDH_KeyGen: creates an ephemeral key and computes the +/// shared secret Z point from the loaded ECC public key. +#[derive(Parser)] +pub struct EcdhKeygenCmd { + /// ECC key context file path + #[arg(short = 'c', long = "context", conflicts_with = "context_handle")] + pub context: Option, + + /// ECC key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "context-handle", value_parser = parse_hex_u32, conflicts_with = "context")] + pub context_handle: Option, + + /// Output file for the ephemeral public point Q + #[arg(short = 'u', long = "public")] + pub public: PathBuf, + + /// Output file for the shared secret Z point + #[arg(short = 'o', long = "output")] + pub output: PathBuf, +} + +impl EcdhKeygenCmd { + 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 mut ctx = create_context(global.tcti.as_deref())?; + + let key_handle = load_key_from_source(&mut ctx, &self.context_source()?)?; + + let (z_point, pub_point) = ctx + .execute_without_session(|ctx| ctx.ecdh_key_gen(key_handle)) + .context("TPM2_ECDH_KeyGen failed")?; + + // Serialize ECC points as x || y (raw concatenated coordinates). + let pub_bytes = ecc_point_to_bytes(&pub_point); + std::fs::write(&self.public, &pub_bytes) + .with_context(|| format!("writing public point to {}", self.public.display()))?; + info!("public point Q saved to {}", self.public.display()); + + let z_bytes = ecc_point_to_bytes(&z_point); + std::fs::write(&self.output, &z_bytes) + .with_context(|| format!("writing shared secret to {}", self.output.display()))?; + info!("shared secret Z saved to {}", self.output.display()); + + Ok(()) + } +} + +fn ecc_point_to_bytes(point: &tss_esapi::structures::EccPoint) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(point.x().value()); + out.extend_from_slice(point.y().value()); + out +} diff --git a/src/cmd/ecdhzgen.rs b/src/cmd/ecdhzgen.rs new file mode 100644 index 0000000..3951ac3 --- /dev/null +++ b/src/cmd/ecdhzgen.rs @@ -0,0 +1,99 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::structures::{EccParameter, EccPoint}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_key_from_source}; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Compute a shared secret from an ECC key and a public point. +/// +/// Wraps TPM2_ECDH_ZGen: uses the private portion of the loaded ECC +/// key and the caller-supplied public point to compute the shared Z. +#[derive(Parser)] +pub struct EcdhZgenCmd { + /// ECC key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// ECC key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Auth value for the key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Input file containing the public point (raw x||y bytes) + #[arg(short = 'u', long = "public")] + pub public: PathBuf, + + /// Output file for the shared secret Z point (raw x||y bytes) + #[arg(short = 'o', long = "output")] + pub output: PathBuf, + + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl EcdhZgenCmd { + fn context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-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.context_source()?)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(key_handle.into(), auth) + .context("tr_set_auth failed")?; + } + + let point_data = std::fs::read(&self.public) + .with_context(|| format!("reading public point from {}", self.public.display()))?; + if point_data.len() < 2 { + bail!("public point file too short"); + } + let half = point_data.len() / 2; + let x = EccParameter::try_from(&point_data[..half]) + .map_err(|e| anyhow::anyhow!("invalid x coordinate: {e}"))?; + let y = EccParameter::try_from(&point_data[half..]) + .map_err(|e| anyhow::anyhow!("invalid y coordinate: {e}"))?; + let in_point = EccPoint::new(x, y); + + let session_path = self.session.as_deref(); + let z_point = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.ecdh_z_gen(key_handle, in_point.clone()) + }) + .context("TPM2_ECDH_ZGen failed")?; + + let mut z_bytes = Vec::new(); + z_bytes.extend_from_slice(z_point.x().value()); + z_bytes.extend_from_slice(z_point.y().value()); + + std::fs::write(&self.output, &z_bytes) + .with_context(|| format!("writing Z point to {}", self.output.display()))?; + info!("shared secret Z saved to {}", self.output.display()); + + Ok(()) + } +} diff --git a/src/cmd/ecephemeral.rs b/src/cmd/ecephemeral.rs new file mode 100644 index 0000000..b1c4e82 --- /dev/null +++ b/src/cmd/ecephemeral.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; + +use crate::cli::GlobalOpts; +use crate::raw_esys; + +/// Create an ephemeral key for two-phase key exchange. +/// +/// Wraps TPM2_EC_Ephemeral: generates an ephemeral public point and +/// counter for the given ECC curve. Unlike ecdhkeygen, this does not +/// require a loaded key. +#[derive(Parser)] +pub struct EcEphemeralCmd { + /// ECC curve (e.g. ecc256, ecc384, ecc521) + #[arg()] + pub curve: String, + + /// Output file for the ephemeral public point Q + #[arg(short = 'u', long = "public")] + pub public: PathBuf, + + /// Output file for the counter value + #[arg(short = 't', long = "counter")] + pub counter: Option, +} + +impl EcEphemeralCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let curve_id = parse_ecc_curve(&self.curve)?; + + let (q_bytes, counter) = raw_esys::ec_ephemeral(global.tcti.as_deref(), curve_id) + .context("TPM2_EC_Ephemeral failed")?; + + std::fs::write(&self.public, &q_bytes) + .with_context(|| format!("writing public point to {}", self.public.display()))?; + info!( + "ephemeral public point Q saved to {}", + self.public.display() + ); + + if let Some(ref path) = self.counter { + std::fs::write(path, counter.to_le_bytes()) + .with_context(|| format!("writing counter to {}", path.display()))?; + info!("counter saved to {}", path.display()); + } + + info!("ec_ephemeral succeeded (counter={counter})"); + Ok(()) + } +} + +fn parse_ecc_curve(s: &str) -> anyhow::Result { + use tss_esapi::constants::tss::*; + match s.to_lowercase().as_str() { + "ecc192" | "nistp192" => Ok(TPM2_ECC_NIST_P192), + "ecc224" | "nistp224" => Ok(TPM2_ECC_NIST_P224), + "ecc256" | "nistp256" => Ok(TPM2_ECC_NIST_P256), + "ecc384" | "nistp384" => Ok(TPM2_ECC_NIST_P384), + "ecc521" | "nistp521" => Ok(TPM2_ECC_NIST_P521), + "bnp256" => Ok(TPM2_ECC_BN_P256), + "bnp638" => Ok(TPM2_ECC_BN_P638), + "sm2p256" | "sm2" => Ok(TPM2_ECC_SM2_P256), + _ => bail!("unsupported ECC curve: {s}"), + } +} diff --git a/src/cmd/encrypt.rs b/src/cmd/encrypt.rs new file mode 100644 index 0000000..b34f459 --- /dev/null +++ b/src/cmd/encrypt.rs @@ -0,0 +1,135 @@ +use std::io::Read; +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{InitialValue, MaxBuffer}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_key_from_source}; +use crate::output; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Encrypt data with a symmetric key held by the TPM. +/// +/// Reads plaintext from a file (or stdin) and writes ciphertext to the +/// output file (or stdout). +#[derive(Parser)] +pub struct EncryptCmd { + /// Symmetric key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// Symmetric key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Authorization value for the key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Cipher mode (cfb, cbc, ecb, ofb, ctr) + #[arg(short = 'G', long = "mode", default_value = "cfb")] + pub mode: String, + + /// Initialization vector input file (default: all zeros) + #[arg(short = 'i', long = "iv")] + pub iv_input: Option, + + /// Output file for the IV produced by the TPM (for chaining) + #[arg(long = "iv-out")] + pub iv_output: Option, + + /// Output file for the encrypted data (default: stdout) + #[arg(short = 'o', long = "output")] + pub output: Option, + + /// Input file to encrypt (default: stdin) + #[arg()] + pub input: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl EncryptCmd { + fn context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-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.context_source()?)?; + let mode = parse::parse_symmetric_mode(&self.mode)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(key_handle.into(), auth) + .context("tr_set_auth failed")?; + } + + let plaintext = read_input(&self.input)?; + let iv_in = read_iv(&self.iv_input)?; + + let data = + MaxBuffer::try_from(plaintext).map_err(|e| anyhow::anyhow!("input too large: {e}"))?; + + let session_path = self.session.as_deref(); + let (ciphertext, iv_out) = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.encrypt_decrypt_2(key_handle, false, mode, data.clone(), iv_in.clone()) + }) + .context("TPM2_EncryptDecrypt2 (encrypt) failed")?; + + if let Some(ref path) = self.output { + output::write_to_file(path, ciphertext.value())?; + info!("ciphertext written to {}", path.display()); + } else { + output::write_binary_stdout(ciphertext.value())?; + } + + if let Some(ref path) = self.iv_output { + output::write_to_file(path, iv_out.value())?; + info!("IV written to {}", path.display()); + } + + Ok(()) + } +} + +fn read_input(path: &Option) -> anyhow::Result> { + match path { + Some(p) => std::fs::read(p).with_context(|| format!("reading input: {}", p.display())), + None => { + let mut buf = Vec::new(); + std::io::stdin() + .read_to_end(&mut buf) + .context("reading stdin")?; + Ok(buf) + } + } +} + +fn read_iv(path: &Option) -> anyhow::Result { + match path { + Some(p) => { + let data = std::fs::read(p).with_context(|| format!("reading IV: {}", p.display()))?; + InitialValue::try_from(data).map_err(|e| anyhow::anyhow!("invalid IV: {e}")) + } + None => Ok(InitialValue::default()), + } +} diff --git a/src/cmd/encryptdecrypt.rs b/src/cmd/encryptdecrypt.rs new file mode 100644 index 0000000..ba04ccd --- /dev/null +++ b/src/cmd/encryptdecrypt.rs @@ -0,0 +1,131 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{InitialValue, MaxBuffer}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_key_from_source}; +use crate::output; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Symmetric encryption or decryption using a TPM-loaded key. +/// +/// Wraps TPM2_EncryptDecrypt2. Use `-d` for decryption, omit for encryption. +#[derive(Parser)] +pub struct EncryptDecryptCmd { + /// Key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// Key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Auth value for the key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Decrypt mode (default: encrypt) + #[arg(short = 'd', long = "decrypt")] + pub decrypt: bool, + + /// Cipher mode (cfb, cbc, ecb, ofb, ctr, null) + #[arg(short = 'G', long = "mode", default_value = "null")] + pub mode: String, + + /// Initial value / IV input file + #[arg(short = 'i', long = "iv")] + pub iv: Option, + + /// Output file for the processed data + #[arg(short = 'o', long = "output")] + pub output: PathBuf, + + /// Output file for the IV out + #[arg(long = "iv-out")] + pub iv_out: Option, + + /// Input data file + #[arg()] + pub input: PathBuf, + + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl EncryptDecryptCmd { + fn context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-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.context_source()?)?; + let mode = parse::parse_symmetric_mode(&self.mode)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(key_handle.into(), auth) + .context("tr_set_auth failed")?; + } + + let data = std::fs::read(&self.input) + .with_context(|| format!("reading input from {}", self.input.display()))?; + let in_data = + MaxBuffer::try_from(data).map_err(|e| anyhow::anyhow!("input too large: {e}"))?; + + let iv_in = match &self.iv { + Some(path) => { + let iv_data = std::fs::read(path) + .with_context(|| format!("reading IV from {}", path.display()))?; + InitialValue::try_from(iv_data).map_err(|e| anyhow::anyhow!("invalid IV: {e}"))? + } + None => InitialValue::default(), + }; + + let session_path = self.session.as_deref(); + let (out_data, iv_out) = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.encrypt_decrypt_2( + key_handle, + self.decrypt, + mode, + in_data.clone(), + iv_in.clone(), + ) + }) + .context("TPM2_EncryptDecrypt2 failed")?; + + output::write_to_file(&self.output, out_data.value())?; + info!( + "{} data saved to {}", + if self.decrypt { + "decrypted" + } else { + "encrypted" + }, + self.output.display() + ); + + if let Some(ref path) = self.iv_out { + output::write_to_file(path, iv_out.value())?; + info!("IV out saved to {}", path.display()); + } + + Ok(()) + } +} diff --git a/src/cmd/eventlog.rs b/src/cmd/eventlog.rs new file mode 100644 index 0000000..215de34 --- /dev/null +++ b/src/cmd/eventlog.rs @@ -0,0 +1,288 @@ +use std::io::{Cursor, Read as IoRead}; +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use serde_json::{Value, json}; + +use crate::cli::GlobalOpts; + +/// Parse and display a binary TPM2 event log. +/// +/// Reads a TCG PC Client Platform Firmware Profile event log +/// (binary_bios_measurements) and prints the entries as JSON. +/// Default path: /sys/kernel/security/tpm0/binary_bios_measurements +#[derive(Parser)] +pub struct EventLogCmd { + /// Path to the binary event log file + #[arg(default_value = "/sys/kernel/security/tpm0/binary_bios_measurements")] + pub file: PathBuf, +} + +impl EventLogCmd { + pub fn execute(&self, _global: &GlobalOpts) -> anyhow::Result<()> { + let data = std::fs::read(&self.file) + .with_context(|| format!("reading event log: {}", self.file.display()))?; + + let events = parse_event_log(&data)?; + println!("{}", serde_json::to_string_pretty(&events)?); + Ok(()) + } +} + +// ----------------------------------------------------------------------- +// Event type names +// ----------------------------------------------------------------------- + +fn event_type_name(ty: u32) -> &'static str { + match ty { + 0x00000000 => "EV_PREBOOT_CERT", + 0x00000001 => "EV_POST_CODE", + 0x00000002 => "EV_UNUSED", + 0x00000003 => "EV_NO_ACTION", + 0x00000004 => "EV_SEPARATOR", + 0x00000005 => "EV_ACTION", + 0x00000006 => "EV_EVENT_TAG", + 0x00000007 => "EV_S_CRTM_CONTENTS", + 0x00000008 => "EV_S_CRTM_VERSION", + 0x00000009 => "EV_CPU_MICROCODE", + 0x0000000A => "EV_PLATFORM_CONFIG_FLAGS", + 0x0000000B => "EV_TABLE_OF_DEVICES", + 0x0000000C => "EV_COMPACT_HASH", + 0x0000000D => "EV_IPL", + 0x0000000E => "EV_IPL_PARTITION_DATA", + 0x0000000F => "EV_NONHOST_CODE", + 0x00000010 => "EV_NONHOST_CONFIG", + 0x00000011 => "EV_NONHOST_INFO", + 0x00000012 => "EV_OMIT_BOOT_DEVICE_EVENTS", + 0x80000001 => "EV_EFI_VARIABLE_DRIVER_CONFIG", + 0x80000002 => "EV_EFI_VARIABLE_BOOT", + 0x80000003 => "EV_EFI_BOOT_SERVICES_APPLICATION", + 0x80000004 => "EV_EFI_BOOT_SERVICES_DRIVER", + 0x80000005 => "EV_EFI_RUNTIME_SERVICES_DRIVER", + 0x80000006 => "EV_EFI_GPT_EVENT", + 0x80000007 => "EV_EFI_ACTION", + 0x80000008 => "EV_EFI_PLATFORM_FIRMWARE_BLOB", + 0x80000009 => "EV_EFI_HANDOFF_TABLES", + 0x8000000A => "EV_EFI_PLATFORM_FIRMWARE_BLOB2", + 0x8000000B => "EV_EFI_HANDOFF_TABLES2", + 0x8000000C => "EV_EFI_VARIABLE_BOOT2", + 0x80000010 => "EV_EFI_HCRTM_EVENT", + 0x800000E0 => "EV_EFI_VARIABLE_AUTHORITY", + 0x800000E1 => "EV_EFI_SPDM_FIRMWARE_BLOB", + 0x800000E2 => "EV_EFI_SPDM_FIRMWARE_CONFIG", + _ => "UNKNOWN", + } +} + +fn hash_alg_name(alg: u16) -> &'static str { + match alg { + 0x0004 => "sha1", + 0x000B => "sha256", + 0x000C => "sha384", + 0x000D => "sha512", + 0x0012 => "sm3_256", + 0x0027 => "sha3_256", + 0x0028 => "sha3_384", + 0x0029 => "sha3_512", + _ => "unknown", + } +} + +fn hash_alg_digest_size(alg: u16) -> Option { + match alg { + 0x0004 => Some(20), // SHA-1 + 0x000B => Some(32), // SHA-256 + 0x000C => Some(48), // SHA-384 + 0x000D => Some(64), // SHA-512 + 0x0012 => Some(32), // SM3-256 + 0x0027 => Some(32), // SHA3-256 + 0x0028 => Some(48), // SHA3-384 + 0x0029 => Some(64), // SHA3-512 + _ => None, + } +} + +// ----------------------------------------------------------------------- +// Binary reader helpers +// ----------------------------------------------------------------------- + +fn read_u16(cur: &mut Cursor<&[u8]>) -> anyhow::Result { + let mut buf = [0u8; 2]; + cur.read_exact(&mut buf) + .context("unexpected end of event log")?; + Ok(u16::from_le_bytes(buf)) +} + +fn read_u32(cur: &mut Cursor<&[u8]>) -> anyhow::Result { + let mut buf = [0u8; 4]; + cur.read_exact(&mut buf) + .context("unexpected end of event log")?; + Ok(u32::from_le_bytes(buf)) +} + +fn read_bytes(cur: &mut Cursor<&[u8]>, n: usize) -> anyhow::Result> { + let mut buf = vec![0u8; n]; + cur.read_exact(&mut buf) + .context("unexpected end of event log")?; + Ok(buf) +} + +// ----------------------------------------------------------------------- +// Spec ID event parsing (determines crypto-agile vs legacy format) +// ----------------------------------------------------------------------- + +struct DigestSpec { + alg_id: u16, + digest_size: u16, +} + +/// Parse the TCG_EfiSpecIDEvent to determine digest algorithms in use. +fn parse_spec_id_event(event_data: &[u8]) -> anyhow::Result> { + let mut cur = Cursor::new(event_data); + + // Signature: 16 bytes "Spec ID Event03\0" + let sig = read_bytes(&mut cur, 16)?; + let sig_str = String::from_utf8_lossy(&sig); + if !sig_str.starts_with("Spec ID Event") { + bail!("not a TCG Spec ID Event: {sig_str:?}"); + } + + // platformClass (u32), specVersionMinor (u8), specVersionMajor (u8), + // specErrata (u8), uintnSize (u8) + let _platform_class = read_u32(&mut cur)?; + let mut ver = [0u8; 4]; + cur.read_exact(&mut ver)?; + + let num_algorithms = read_u32(&mut cur)?; + let mut specs = Vec::new(); + for _ in 0..num_algorithms { + let alg_id = read_u16(&mut cur)?; + let digest_size = read_u16(&mut cur)?; + specs.push(DigestSpec { + alg_id, + digest_size, + }); + } + + Ok(specs) +} + +// ----------------------------------------------------------------------- +// Event log parser +// ----------------------------------------------------------------------- + +fn parse_event_log(data: &[u8]) -> anyhow::Result { + if data.len() < 32 { + bail!("event log too short ({} bytes)", data.len()); + } + + let mut cur = Cursor::new(data); + let mut events = Vec::new(); + + // First event is always legacy format (TCG_PCClientPCREvent). + let first = parse_legacy_event(&mut cur)?; + let digest_specs = parse_spec_id_event_from_entry(&first)?; + events.push(first); + + // Remaining events use the crypto-agile format if we got specs. + if digest_specs.is_empty() { + // Legacy mode: all events are SHA-1 only. + while cur.position() < data.len() as u64 { + match parse_legacy_event(&mut cur) { + Ok(ev) => events.push(ev), + Err(_) => break, + } + } + } else { + while cur.position() < data.len() as u64 { + match parse_crypto_agile_event(&mut cur, &digest_specs) { + Ok(ev) => events.push(ev), + Err(_) => break, + } + } + } + + Ok(Value::Array(events)) +} + +fn parse_spec_id_event_from_entry(event: &Value) -> anyhow::Result> { + let event_type = event.get("EventType").and_then(|v| v.as_u64()).unwrap_or(0) as u32; + + if event_type != 0x03 { + // Not EV_NO_ACTION — return empty (legacy mode). + return Ok(Vec::new()); + } + + let event_data_hex = event + .get("EventData") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if let Ok(event_data) = hex::decode(event_data_hex) + && let Ok(specs) = parse_spec_id_event(&event_data) + { + return Ok(specs); + } + + Ok(Vec::new()) +} + +/// Parse a legacy TCG_PCClientPCREvent (SHA-1 only, first event). +fn parse_legacy_event(cur: &mut Cursor<&[u8]>) -> anyhow::Result { + let pcr_index = read_u32(cur)?; + let event_type = read_u32(cur)?; + let sha1_digest = read_bytes(cur, 20)?; + let event_size = read_u32(cur)? as usize; + let event_data = read_bytes(cur, event_size)?; + + Ok(json!({ + "PCRIndex": pcr_index, + "EventType": event_type, + "EventTypeName": event_type_name(event_type), + "Digests": { + "sha1": hex::encode(&sha1_digest), + }, + "EventSize": event_size, + "EventData": hex::encode(&event_data), + })) +} + +/// Parse a TCG_PCR_EVENT2 (crypto-agile format). +fn parse_crypto_agile_event( + cur: &mut Cursor<&[u8]>, + specs: &[DigestSpec], +) -> anyhow::Result { + let pcr_index = read_u32(cur)?; + let event_type = read_u32(cur)?; + let digest_count = read_u32(cur)?; + + let mut digests = serde_json::Map::new(); + for _ in 0..digest_count { + let alg_id = read_u16(cur)?; + // Find digest size from spec or fall back to known sizes. + let size = specs + .iter() + .find(|s| s.alg_id == alg_id) + .map(|s| s.digest_size as usize) + .or_else(|| hash_alg_digest_size(alg_id)) + .ok_or_else(|| anyhow::anyhow!("unknown digest algorithm 0x{alg_id:04x}"))?; + let digest = read_bytes(cur, size)?; + digests.insert( + hash_alg_name(alg_id).to_string(), + Value::String(hex::encode(&digest)), + ); + } + + let event_size = read_u32(cur)? as usize; + let event_data = read_bytes(cur, event_size)?; + + Ok(json!({ + "PCRIndex": pcr_index, + "EventType": event_type, + "EventTypeName": event_type_name(event_type), + "Digests": Value::Object(digests), + "EventSize": event_size, + "EventData": hex::encode(&event_data), + })) +} diff --git a/src/cmd/evictcontrol.rs b/src/cmd/evictcontrol.rs new file mode 100644 index 0000000..8149706 --- /dev/null +++ b/src/cmd/evictcontrol.rs @@ -0,0 +1,94 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::handles::PersistentTpmHandle; +use tss_esapi::interface_types::dynamic_handles::Persistent; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_object_from_source}; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Make a transient object persistent, or evict a persistent object. +/// +/// When making persistent, pass the transient handle as `-c` and the desired +/// persistent handle as the positional argument. +#[derive(Parser)] +pub struct EvictControlCmd { + /// Persistent handle (hex, e.g. 0x81000001) + #[arg(value_parser = parse_hex_u32)] + pub persistent_handle: u32, + + /// Transient object context file + #[arg(short = 'c', long = "context", conflicts_with = "context_handle")] + pub context: Option, + + /// Transient object handle (hex, e.g. 0x80000001) + #[arg(long = "context-handle", value_parser = parse_hex_u32, conflicts_with = "context")] + pub context_handle: Option, + + /// Authorization hierarchy (o/owner, p/platform) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl EvictControlCmd { + fn context_source(&self) -> Option> { + match (&self.context, self.context_handle) { + (Some(path), None) => Some(Ok(ContextSource::File(path.clone()))), + (None, Some(handle)) => Some(Ok(ContextSource::Handle(handle))), + (None, None) => None, + _ => Some(Err(anyhow::anyhow!( + "only one of --context or --context-handle may be provided" + ))), + } + } + + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let provision = parse::parse_provision(&self.hierarchy)?; + let persistent_tpm_handle = PersistentTpmHandle::new(self.persistent_handle) + .map_err(|e| anyhow::anyhow!("invalid persistent handle: {e}"))?; + let persistent: Persistent = persistent_tpm_handle.into(); + + let session_path = self.session.as_deref(); + + // If a transient context is given, make it persistent + if let Some(source_result) = self.context_source() { + let source = source_result?; + let obj_handle = load_object_from_source(&mut ctx, &source)?; + execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.evict_control(provision, obj_handle, persistent) + }) + .context("TPM2_EvictControl failed")?; + info!( + "object persisted at 0x{:08x}", + u32::from(persistent_tpm_handle) + ); + } else { + // Evict the persistent object + let tpm_handle: tss_esapi::handles::TpmHandle = persistent_tpm_handle.into(); + let obj = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .context("failed to load persistent handle")?; + execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.evict_control(provision, obj, persistent) + }) + .context("TPM2_EvictControl (evict) failed")?; + info!( + "persistent object 0x{:08x} evicted", + u32::from(persistent_tpm_handle) + ); + } + + Ok(()) + } +} diff --git a/src/cmd/flushcontext.rs b/src/cmd/flushcontext.rs new file mode 100644 index 0000000..79d984b --- /dev/null +++ b/src/cmd/flushcontext.rs @@ -0,0 +1,141 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::constants::CapabilityType; +use tss_esapi::handles::ObjectHandle; +use tss_esapi::structures::CapabilityData; +use tss_esapi::utils::TpmsContext; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +const HR_TRANSIENT: u32 = 0x80000000; +const HR_LOADED_SESSION: u32 = 0x02000000; +const HR_SAVED_SESSION: u32 = 0x03000000; + +/// Flush a loaded handle from the TPM. +/// +/// Supports hex handles, context files, and bulk flags to flush all +/// objects of a given type. +#[derive(Parser)] +pub struct FlushContextCmd { + /// Context file path to flush + #[arg(long = "context", conflicts_with_all = ["handle_hex", "transient_object", "loaded_session", "saved_session"])] + pub handle: Option, + + /// Hex handle to flush (e.g. 0x80000000) + #[arg(long = "handle", value_parser = crate::parse::parse_hex_u32, conflicts_with_all = ["handle", "transient_object", "loaded_session", "saved_session"])] + pub handle_hex: Option, + + /// Flush all transient objects + #[arg(long = "transient-object", conflicts_with_all = ["handle", "loaded_session", "saved_session"])] + pub transient_object: bool, + + /// Flush all loaded sessions + #[arg(long = "loaded-session", conflicts_with_all = ["handle", "transient_object", "saved_session"])] + pub loaded_session: bool, + + /// Flush all saved sessions + #[arg(long = "saved-session", conflicts_with_all = ["handle", "transient_object", "loaded_session"])] + pub saved_session: bool, +} + +impl FlushContextCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + if self.transient_object { + return flush_all_handles(&mut ctx, HR_TRANSIENT, "transient objects"); + } + if self.loaded_session { + return flush_all_handles(&mut ctx, HR_LOADED_SESSION, "loaded sessions"); + } + if self.saved_session { + return flush_all_handles(&mut ctx, HR_SAVED_SESSION, "saved sessions"); + } + + if let Some(raw) = self.handle_hex { + let handle = ObjectHandle::from(raw); + ctx.flush_context(handle) + .context("TPM2_FlushContext failed")?; + info!("flushed handle 0x{raw:08x}"); + return Ok(()); + } + + let path = match &self.handle { + Some(p) => p, + None => bail!( + "provide --context, --handle, or a bulk flush flag (--transient-object, --loaded-session, --saved-session)" + ), + }; + + let data = std::fs::read(path) + .with_context(|| format!("reading context file: {}", path.display()))?; + let saved: TpmsContext = + serde_json::from_slice(&data).context("failed to deserialize context")?; + let obj_handle = ctx.context_load(saved).context("context_load failed")?; + ctx.flush_context(obj_handle) + .context("TPM2_FlushContext failed")?; + info!("flushed context from {}", path.display()); + + Ok(()) + } +} + +fn flush_all_handles( + ctx: &mut tss_esapi::Context, + range_start: u32, + label: &str, +) -> anyhow::Result<()> { + let handles = get_handles(ctx, range_start)?; + + if handles.is_empty() { + info!("no {label} to flush"); + return Ok(()); + } + + let mut flushed = 0u32; + for h in &handles { + let handle = ObjectHandle::from(*h); + match ctx.flush_context(handle) { + Ok(()) => { + info!("flushed 0x{h:08x}"); + flushed += 1; + } + Err(e) => { + log::warn!("failed to flush 0x{h:08x}: {e}"); + } + } + } + + info!("flushed {flushed}/{} {label}", handles.len()); + Ok(()) +} + +fn get_handles(ctx: &mut tss_esapi::Context, start: u32) -> anyhow::Result> { + let mut all_handles = Vec::new(); + let mut property = start; + loop { + let (data, more) = ctx + .execute_without_session(|ctx| { + ctx.get_capability(CapabilityType::Handles, property, 254) + }) + .context("TPM2_GetCapability (handles) failed")?; + + if let CapabilityData::Handles(list) = data { + let raw: Vec = list.into_inner().iter().map(|h| u32::from(*h)).collect(); + if let Some(&last) = raw.last() { + property = last.saturating_add(1); + } + all_handles.extend(raw); + } + + if !more { + break; + } + } + + Ok(all_handles) +} diff --git a/src/cmd/getcap.rs b/src/cmd/getcap.rs new file mode 100644 index 0000000..2578758 --- /dev/null +++ b/src/cmd/getcap.rs @@ -0,0 +1,231 @@ +use anyhow::{Context, bail}; +use clap::Parser; +use serde_json::{Value, json}; + +use tss_esapi::constants::CapabilityType; +use tss_esapi::structures::CapabilityData; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +// Well-known property/handle range start values from the TPM2 spec. +const PT_FIXED_START: u32 = 0x100; // TPM2_PT_FIXED +const PT_VAR_START: u32 = 0x200; // TPM2_PT_VAR +const CC_FIRST: u32 = 0x011F; // TPM2_CC_FIRST +const ALG_FIRST: u32 = 0x0001; // TPM2_ALG_FIRST +const HR_PCR: u32 = 0x00000000; +const HR_NV_INDEX: u32 = 0x01000000; +const HR_LOADED_SESSION: u32 = 0x02000000; +const HR_SAVED_SESSION: u32 = 0x03000000; +const HR_PERMANENT: u32 = 0x40000000; +const HR_TRANSIENT: u32 = 0x80000000; +const HR_PERSISTENT: u32 = 0x81000000; + +const ALL_CAPS: &[&str] = &[ + "algorithms", + "commands", + "pcrs", + "properties-fixed", + "properties-variable", + "ecc-curves", + "handles-transient", + "handles-persistent", + "handles-permanent", + "handles-pcr", + "handles-nv-index", + "handles-loaded-session", + "handles-saved-session", +]; + +/// Query the TPM for its capabilities and properties. +#[derive(Parser)] +pub struct GetCapCmd { + /// Capability to query (e.g. algorithms, properties-fixed, handles-persistent) + #[arg(required_unless_present = "list")] + pub capability: Option, + + /// List all supported capability names + #[arg(short = 'l', long = "list")] + pub list: bool, +} + +impl GetCapCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + if self.list { + for cap in ALL_CAPS { + println!("{cap}"); + } + return Ok(()); + } + + let cap = self.capability.as_deref().unwrap(); + let mut ctx = create_context(global.tcti.as_deref())?; + + let value = match cap { + "algorithms" => query_algorithms(&mut ctx)?, + "commands" => query_commands(&mut ctx)?, + "pcrs" => query_pcrs(&mut ctx)?, + "properties-fixed" => query_properties(&mut ctx, PT_FIXED_START)?, + "properties-variable" => query_properties(&mut ctx, PT_VAR_START)?, + "ecc-curves" => query_ecc_curves(&mut ctx)?, + "handles-transient" => query_handles(&mut ctx, HR_TRANSIENT)?, + "handles-persistent" => query_handles(&mut ctx, HR_PERSISTENT)?, + "handles-permanent" => query_handles(&mut ctx, HR_PERMANENT)?, + "handles-pcr" => query_handles(&mut ctx, HR_PCR)?, + "handles-nv-index" => query_handles(&mut ctx, HR_NV_INDEX)?, + "handles-loaded-session" => query_handles(&mut ctx, HR_LOADED_SESSION)?, + "handles-saved-session" => query_handles(&mut ctx, HR_SAVED_SESSION)?, + _ => bail!("unknown capability '{cap}'; use -l to list supported capabilities"), + }; + + println!("{}", serde_json::to_string_pretty(&value)?); + Ok(()) + } +} + +/// Fetch all capability data by looping while the TPM sets more_data. +fn fetch_all( + ctx: &mut tss_esapi::Context, + cap_type: CapabilityType, + start: u32, +) -> anyhow::Result> { + const BATCH: u32 = 0xFE; // 254 per call — stays well within TPM limits + let mut results = Vec::new(); + let mut property = start; + loop { + let (data, more) = ctx + .execute_without_session(|ctx| ctx.get_capability(cap_type, property, BATCH)) + .context("TPM2_GetCapability failed")?; + let last = last_property_u32(&data); + results.push(data); + if !more { + break; + } + match last { + Some(p) => property = p.saturating_add(1), + None => break, + } + } + Ok(results) +} + +/// Return the discriminant value of the last element in a capability chunk. +/// Used to compute the start property for the next call when more_data is set. +fn last_property_u32(data: &CapabilityData) -> Option { + match data { + CapabilityData::Algorithms(list) => list.last().map(|a| a.algorithm_identifier() as u32), + CapabilityData::Handles(list) => list.last().map(|h| u32::from(*h)), + CapabilityData::Commands(list) => list.last().map(|c| c.command_index() as u32), + CapabilityData::TpmProperties(list) => list.last().map(|p| p.property() as u32), + CapabilityData::EccCurves(list) => list.last().map(|c| *c as u32), + _ => None, + } +} + +fn query_algorithms(ctx: &mut tss_esapi::Context) -> anyhow::Result { + let chunks = fetch_all(ctx, CapabilityType::Algorithms, ALG_FIRST)?; + let mut arr = Vec::new(); + for chunk in chunks { + if let CapabilityData::Algorithms(list) = chunk { + for alg in list { + let attrs = alg.algorithm_properties(); + arr.push(json!({ + "algorithm": format!("{:?}", alg.algorithm_identifier()), + "asymmetric": attrs.asymmetric(), + "symmetric": attrs.symmetric(), + "hash": attrs.hash(), + "object": attrs.object(), + "signing": attrs.signing(), + "encrypting": attrs.encrypting(), + "method": attrs.method(), + })); + } + } + } + Ok(Value::Array(arr)) +} + +fn query_commands(ctx: &mut tss_esapi::Context) -> anyhow::Result { + let chunks = fetch_all(ctx, CapabilityType::Command, CC_FIRST)?; + let mut arr = Vec::new(); + for chunk in chunks { + if let CapabilityData::Commands(list) = chunk { + for cmd in list { + arr.push(json!({ + "command": format!("0x{:04x}", cmd.command_index()), + "nv": cmd.nv(), + "extensive": cmd.extensive(), + "flushed": cmd.flushed(), + "handles": cmd.c_handles(), + "r_handle": cmd.r_handle(), + "vendor_specific": cmd.is_vendor_specific(), + })); + } + } + } + Ok(Value::Array(arr)) +} + +fn query_pcrs(ctx: &mut tss_esapi::Context) -> anyhow::Result { + let chunks = fetch_all(ctx, CapabilityType::AssignedPcr, 0)?; + let mut map = serde_json::Map::new(); + for chunk in chunks { + if let CapabilityData::AssignedPcr(psl) = chunk { + for sel in psl.get_selections() { + let alg = format!("{:?}", sel.hashing_algorithm()); + let indices: Vec = sel + .selected() + .iter() + .map(|s| { + // PcrSlot values are powers of 2 (bitmask); trailing_zeros gives the index. + Value::Number((u32::from(*s).trailing_zeros() as u64).into()) + }) + .collect(); + map.insert(alg, Value::Array(indices)); + } + } + } + Ok(Value::Object(map)) +} + +fn query_properties(ctx: &mut tss_esapi::Context, start: u32) -> anyhow::Result { + let chunks = fetch_all(ctx, CapabilityType::TpmProperties, start)?; + let mut arr = Vec::new(); + for chunk in chunks { + if let CapabilityData::TpmProperties(list) = chunk { + for prop in list { + arr.push(json!({ + "property": format!("{:?}", prop.property()), + "value": prop.value(), + })); + } + } + } + Ok(Value::Array(arr)) +} + +fn query_ecc_curves(ctx: &mut tss_esapi::Context) -> anyhow::Result { + let chunks = fetch_all(ctx, CapabilityType::EccCurves, 0)?; + let mut arr = Vec::new(); + for chunk in chunks { + if let CapabilityData::EccCurves(list) = chunk { + for curve in list.into_inner() { + arr.push(Value::String(format!("{:?}", curve))); + } + } + } + Ok(Value::Array(arr)) +} + +fn query_handles(ctx: &mut tss_esapi::Context, start: u32) -> anyhow::Result { + let chunks = fetch_all(ctx, CapabilityType::Handles, start)?; + let mut arr = Vec::new(); + for chunk in chunks { + if let CapabilityData::Handles(list) = chunk { + for h in list.into_inner() { + arr.push(Value::String(format!("0x{:08x}", u32::from(h)))); + } + } + } + Ok(Value::Array(arr)) +} diff --git a/src/cmd/getcommandauditdigest.rs b/src/cmd/getcommandauditdigest.rs new file mode 100644 index 0000000..b030e80 --- /dev/null +++ b/src/cmd/getcommandauditdigest.rs @@ -0,0 +1,129 @@ +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}; + +/// Get the current command audit digest signed by a key. +/// +/// Wraps TPM2_GetCommandAuditDigest (raw FFI). +#[derive(Parser)] +pub struct GetCommandAuditDigestCmd { + /// Signing key context file path + #[arg( + short = 'c', + long = "signing-key-context", + conflicts_with = "signing_key_context_handle" + )] + pub signing_key_context: Option, + + /// Signing key handle (hex, e.g. 0x81000001) + #[arg(long = "signing-key-context-handle", value_parser = parse_hex_u32, conflicts_with = "signing_key_context")] + pub signing_key_context_handle: Option, + + /// Auth hierarchy for the privacy admin (e/endorsement) + #[arg(short = 'C', long = "privacy-admin", default_value = "e")] + pub privacy_admin: String, + + /// Auth for the signing key + #[arg(short = 'P', long = "signing-key-auth")] + pub signing_key_auth: Option, + + /// Auth for the privacy admin hierarchy + #[arg(short = 'p', long = "hierarchy-auth")] + pub hierarchy_auth: Option, + + /// Qualifying data (nonce, hex) + #[arg(short = 'q', long = "qualification")] + pub qualification: Option, + + /// Output file for the attestation data + #[arg(short = 'o', long = "attestation")] + pub attestation: Option, + + /// Output file for the signature + #[arg(long = "signature")] + pub signature: Option, +} + +impl GetCommandAuditDigestCmd { + fn signing_key_context_source(&self) -> anyhow::Result { + match (&self.signing_key_context, self.signing_key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --signing-key-context or --signing-key-context-handle must be provided" + ), + } + } + + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let privacy_handle = RawEsysContext::resolve_hierarchy(&self.privacy_admin)?; + let sign_handle = raw.resolve_handle_from_source(&self.signing_key_context_source()?)?; + + if let Some(ref auth_str) = self.hierarchy_auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(privacy_handle, auth.value())?; + } + if let Some(ref auth_str) = self.signing_key_auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(sign_handle, auth.value())?; + } + + let mut qualifying_data = TPM2B_DATA::default(); + if let Some(ref q) = self.qualification { + let bytes = hex::decode(q).context("invalid qualifying data hex")?; + qualifying_data.size = bytes.len() as u16; + qualifying_data.buffer[..bytes.len()].copy_from_slice(&bytes); + } + + let scheme = TPMT_SIG_SCHEME { + scheme: TPM2_ALG_NULL, + details: Default::default(), + }; + + unsafe { + let mut audit_info: *mut TPM2B_ATTEST = std::ptr::null_mut(); + let mut sig: *mut TPMT_SIGNATURE = std::ptr::null_mut(); + + let rc = Esys_GetCommandAuditDigest( + raw.ptr(), + privacy_handle, + sign_handle, + ESYS_TR_PASSWORD, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + &qualifying_data, + &scheme, + &mut audit_info, + &mut sig, + ); + if rc != 0 { + anyhow::bail!("Esys_GetCommandAuditDigest failed: 0x{rc:08x}"); + } + + if let Some(ref path) = self.attestation { + raw_esys::write_raw_attestation(audit_info, path)?; + info!("audit 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(audit_info as *mut _); + Esys_Free(sig as *mut _); + } + + info!("command audit digest retrieved"); + Ok(()) + } +} diff --git a/src/cmd/geteccparameters.rs b/src/cmd/geteccparameters.rs new file mode 100644 index 0000000..323042e --- /dev/null +++ b/src/cmd/geteccparameters.rs @@ -0,0 +1,75 @@ +use clap::Parser; +use serde_json::json; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::raw_esys::RawEsysContext; + +/// Get the ECC curve parameters for a given curve. +/// +/// Wraps TPM2_ECC_Parameters (raw FFI). +#[derive(Parser)] +pub struct GetEccParametersCmd { + /// ECC curve (ecc256, ecc384, ecc521, etc.) + #[arg()] + pub curve: String, +} + +impl GetEccParametersCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + + let curve_id = parse_ecc_curve(&self.curve)?; + + unsafe { + let mut params: *mut TPMS_ALGORITHM_DETAIL_ECC = std::ptr::null_mut(); + let rc = Esys_ECC_Parameters( + raw.ptr(), + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + curve_id, + &mut params, + ); + if rc != 0 { + anyhow::bail!("Esys_ECC_Parameters failed: 0x{rc:08x}"); + } + + let p = &*params; + let output = json!({ + "curve_id": format!("0x{:04x}", p.curveID), + "key_size": p.keySize, + "kdf_scheme": format!("0x{:04x}", p.kdf.scheme), + "sign_scheme": format!("0x{:04x}", p.sign.scheme), + "p": hex::encode(&p.p.buffer[..p.p.size as usize]), + "a": hex::encode(&p.a.buffer[..p.a.size as usize]), + "b": hex::encode(&p.b.buffer[..p.b.size as usize]), + "gX": hex::encode(&p.gX.buffer[..p.gX.size as usize]), + "gY": hex::encode(&p.gY.buffer[..p.gY.size as usize]), + "n": hex::encode(&p.n.buffer[..p.n.size as usize]), + "h": hex::encode(&p.h.buffer[..p.h.size as usize]), + }); + + Esys_Free(params as *mut _); + + println!("{}", serde_json::to_string_pretty(&output)?); + } + + Ok(()) + } +} + +fn parse_ecc_curve(s: &str) -> anyhow::Result { + use tss_esapi::constants::tss::*; + match s.to_lowercase().as_str() { + "ecc192" | "nistp192" => Ok(TPM2_ECC_NIST_P192), + "ecc224" | "nistp224" => Ok(TPM2_ECC_NIST_P224), + "ecc256" | "nistp256" => Ok(TPM2_ECC_NIST_P256), + "ecc384" | "nistp384" => Ok(TPM2_ECC_NIST_P384), + "ecc521" | "nistp521" => Ok(TPM2_ECC_NIST_P521), + "bnp256" => Ok(TPM2_ECC_BN_P256), + "bnp638" => Ok(TPM2_ECC_BN_P638), + "sm2p256" | "sm2" => Ok(TPM2_ECC_SM2_P256), + _ => anyhow::bail!("unsupported ECC curve: {s}"), + } +} diff --git a/src/cmd/getekcertificate.rs b/src/cmd/getekcertificate.rs new file mode 100644 index 0000000..8894098 --- /dev/null +++ b/src/cmd/getekcertificate.rs @@ -0,0 +1,117 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::handles::NvIndexTpmHandle; +use tss_esapi::interface_types::resource_handles::NvAuth; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::output; + +/// Well-known NV indices for EK certificates (TCG EK Credential Profile). +const NV_RSA_EK_CERT: u32 = 0x01C00002; +const NV_ECC_EK_CERT: u32 = 0x01C0000A; + +/// Retrieve the Endorsement Key (EK) certificate from TPM NV storage. +/// +/// The TCG EK Credential Profile reserves well-known NV indices for +/// storing EK certificates provisioned during manufacturing: +/// - RSA 2048: 0x01C00002 +/// - ECC P-256: 0x01C0000A +/// +/// This command reads the certificate from the appropriate NV index +/// and writes it to the specified output file (DER-encoded X.509). +#[derive(Parser)] +pub struct GetEkCertificateCmd { + /// Key algorithm (rsa, ecc) + #[arg(short = 'a', long = "algorithm", default_value = "rsa")] + pub algorithm: String, + + /// Override NV index (hex, e.g. 0x01C00002) + #[arg(short = 'x', long = "nv-index")] + pub nv_index: Option, + + /// Output file for the EK certificate (DER-encoded X.509) + #[arg(short = 'o', long = "output")] + pub output: Option, +} + +impl GetEkCertificateCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let nv_index = match &self.nv_index { + Some(s) => { + let stripped = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s); + u32::from_str_radix(stripped, 16) + .map_err(|_| anyhow::anyhow!("invalid NV index: {s}"))? + } + None => match self.algorithm.to_lowercase().as_str() { + "rsa" => NV_RSA_EK_CERT, + "ecc" => NV_ECC_EK_CERT, + _ => bail!( + "unsupported algorithm '{}'; use 'rsa' or 'ecc'", + self.algorithm + ), + }, + }; + + info!("reading EK certificate from NV index 0x{nv_index:08x}"); + + let mut ctx = create_context(global.tcti.as_deref())?; + + let nv_handle = NvIndexTpmHandle::new(nv_index) + .map_err(|e| anyhow::anyhow!("invalid NV index 0x{nv_index:08x}: {e}"))?; + + // Resolve the NV index to an ESYS_TR. + let tpm_handle: tss_esapi::handles::TpmHandle = nv_handle.into(); + let nv_idx = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .with_context(|| format!("failed to load NV index 0x{nv_index:08x}"))?; + + // Read the NV public area to determine the certificate size. + let (nv_public, _) = ctx + .execute_without_session(|ctx| ctx.nv_read_public(nv_idx.into())) + .context("TPM2_NV_ReadPublic failed")?; + + let total_size = nv_public.data_size() as u16; + info!("EK certificate size: {total_size} bytes"); + + // Read the certificate data. TPMs often limit NV reads to + // MAX_NV_BUFFER_SIZE (~1024 bytes), so read in chunks. + let mut cert_data = Vec::with_capacity(total_size as usize); + let chunk_size: u16 = 512; + let mut offset: u16 = 0; + + while offset < total_size { + let remaining = total_size - offset; + let to_read = remaining.min(chunk_size); + + let data = ctx + .execute_with_nullauth_session(|ctx| { + ctx.nv_read(NvAuth::Owner, nv_idx.into(), to_read, offset) + }) + .with_context(|| format!("TPM2_NV_Read failed at offset {offset}"))?; + + cert_data.extend_from_slice(data.value()); + offset += to_read; + } + + if let Some(ref path) = self.output { + output::write_to_file(path, &cert_data)?; + info!( + "EK certificate ({}) saved to {} ({} bytes)", + self.algorithm, + path.display(), + cert_data.len() + ); + } else { + output::print_hex(&cert_data); + } + + Ok(()) + } +} diff --git a/src/cmd/getrandom.rs b/src/cmd/getrandom.rs new file mode 100644 index 0000000..dd8f893 --- /dev/null +++ b/src/cmd/getrandom.rs @@ -0,0 +1,66 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::{info, warn}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::output; +use crate::session::execute_with_optional_session; + +/// Get random bytes from the TPM. +#[derive(Parser)] +pub struct GetRandomCmd { + /// Number of random bytes to retrieve + pub num_bytes: u16, + + /// Output file path (default: stdout) + #[arg(short = 'o', long)] + pub output: Option, + + /// Print output as a hex string + #[arg(long)] + pub hex: bool, + + /// Bypass the TPM max-digest size check + #[arg(short = 'f', long)] + pub force: bool, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl GetRandomCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let num_bytes = self.num_bytes; + let session_path = self.session.as_deref(); + let random = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.get_random(num_bytes.into()) + }) + .context("TPM2_GetRandom failed")?; + + let bytes = random.value(); + if bytes.len() < num_bytes as usize { + warn!( + "TPM returned fewer bytes than requested: expected {}, got {}", + num_bytes, + bytes.len(), + ); + } + + if let Some(ref path) = self.output { + output::write_to_file(path, bytes)?; + info!("wrote {} bytes to {}", bytes.len(), path.display()); + } else if self.hex { + output::print_hex(bytes); + } else { + output::write_binary_stdout(bytes)?; + } + + Ok(()) + } +} diff --git a/src/cmd/getsessionauditdigest.rs b/src/cmd/getsessionauditdigest.rs new file mode 100644 index 0000000..cbefa85 --- /dev/null +++ b/src/cmd/getsessionauditdigest.rs @@ -0,0 +1,141 @@ +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}; + +/// Get the session audit digest signed by a key. +/// +/// Wraps TPM2_GetSessionAuditDigest (raw FFI). +#[derive(Parser)] +pub struct GetSessionAuditDigestCmd { + /// Signing key context file path + #[arg( + short = 'c', + long = "signing-key-context", + conflicts_with = "signing_key_context_handle" + )] + pub signing_key_context: Option, + + /// Signing key handle (hex, e.g. 0x81000001) + #[arg(long = "signing-key-context-handle", value_parser = parse_hex_u32, conflicts_with = "signing_key_context")] + pub signing_key_context_handle: Option, + + /// Auth hierarchy for the privacy admin (e/endorsement) + #[arg(short = 'C', long = "privacy-admin", default_value = "e")] + pub privacy_admin: String, + + /// Session context file to audit + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Auth for the signing key + #[arg(short = 'P', long = "signing-key-auth")] + pub signing_key_auth: Option, + + /// Auth for the privacy admin hierarchy + #[arg(short = 'p', long = "hierarchy-auth")] + pub hierarchy_auth: Option, + + /// Qualifying data (nonce, hex) + #[arg(short = 'q', long = "qualification")] + pub qualification: Option, + + /// Output file for the attestation data + #[arg(short = 'o', long = "attestation")] + pub attestation: Option, + + /// Output file for the signature + #[arg(long = "signature")] + pub signature: Option, +} + +impl GetSessionAuditDigestCmd { + fn signing_key_context_source(&self) -> anyhow::Result { + match (&self.signing_key_context, self.signing_key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --signing-key-context or --signing-key-context-handle must be provided" + ), + } + } + + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let privacy_handle = RawEsysContext::resolve_hierarchy(&self.privacy_admin)?; + let sign_handle = raw.resolve_handle_from_source(&self.signing_key_context_source()?)?; + + // Load session via raw context_load + let session_handle = raw.context_load( + self.session + .to_str() + .ok_or_else(|| anyhow::anyhow!("invalid session path"))?, + )?; + + if let Some(ref auth_str) = self.hierarchy_auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(privacy_handle, auth.value())?; + } + if let Some(ref auth_str) = self.signing_key_auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(sign_handle, auth.value())?; + } + + let mut qualifying_data = TPM2B_DATA::default(); + if let Some(ref q) = self.qualification { + let bytes = hex::decode(q).context("invalid qualifying data hex")?; + qualifying_data.size = bytes.len() as u16; + qualifying_data.buffer[..bytes.len()].copy_from_slice(&bytes); + } + + let scheme = TPMT_SIG_SCHEME { + scheme: TPM2_ALG_NULL, + details: Default::default(), + }; + + unsafe { + let mut audit_info: *mut TPM2B_ATTEST = std::ptr::null_mut(); + let mut sig: *mut TPMT_SIGNATURE = std::ptr::null_mut(); + + let rc = Esys_GetSessionAuditDigest( + raw.ptr(), + privacy_handle, + sign_handle, + session_handle, + ESYS_TR_PASSWORD, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + &qualifying_data, + &scheme, + &mut audit_info, + &mut sig, + ); + if rc != 0 { + anyhow::bail!("Esys_GetSessionAuditDigest failed: 0x{rc:08x}"); + } + + if let Some(ref path) = self.attestation { + raw_esys::write_raw_attestation(audit_info, path)?; + info!("session audit 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(audit_info as *mut _); + Esys_Free(sig as *mut _); + } + + info!("session audit digest retrieved"); + Ok(()) + } +} diff --git a/src/cmd/gettestresult.rs b/src/cmd/gettestresult.rs new file mode 100644 index 0000000..3732c89 --- /dev/null +++ b/src/cmd/gettestresult.rs @@ -0,0 +1,34 @@ +use anyhow::Context; +use clap::Parser; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Get the results of a TPM self test. +/// +/// Wraps TPM2_GetTestResult: returns the test result data and the +/// overall pass/fail status. +#[derive(Parser)] +pub struct GetTestResultCmd {} + +impl GetTestResultCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let (data, result) = ctx + .execute_without_session(|ctx| ctx.get_test_result()) + .context("TPM2_GetTestResult failed")?; + + let status = match result { + Ok(()) => "success", + Err(_) => "failure", + }; + + println!("status: {status}"); + if !data.value().is_empty() { + println!("data: {}", hex::encode(data.value())); + } + + Ok(()) + } +} diff --git a/src/cmd/gettime.rs b/src/cmd/gettime.rs new file mode 100644 index 0000000..494d5bc --- /dev/null +++ b/src/cmd/gettime.rs @@ -0,0 +1,126 @@ +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}; +use crate::parse; +use crate::parse::parse_hex_u32; +use crate::session::execute_with_optional_session; + +/// Get a signed timestamp from the TPM. +/// +/// Wraps TPM2_GetTime: produces an attestation structure containing +/// the current time and clock values, signed by the specified key. +#[derive(Parser)] +pub struct GetTimeCmd { + /// 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, + + /// 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 + #[arg(short = 'o', long = "attestation")] + pub attestation: Option, + + /// Output file for the signature + #[arg(short = 's', long = "signature")] + pub signature: Option, + + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl GetTimeCmd { + 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 mut ctx = create_context(global.tcti.as_deref())?; + + let signing_key = load_key_from_source(&mut ctx, &self.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.auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(signing_key.into(), auth) + .context("tr_set_auth failed")?; + } + + let qualifying = match (&self.qualification, &self.qualification_file) { + (Some(q), None) => { + let bytes = parse::parse_qualification_hex(q)?; + Data::try_from(bytes).map_err(|e| anyhow::anyhow!("qualifying data: {e}"))? + } + (None, Some(path)) => { + let bytes = parse::parse_qualification_file(path)?; + Data::try_from(bytes).map_err(|e| anyhow::anyhow!("qualifying data: {e}"))? + } + _ => Data::default(), + }; + + let session_path = self.session.as_deref(); + let (attest, signature) = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.get_time(signing_key, qualifying.clone(), scheme) + }) + .context("TPM2_GetTime 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!("gettime succeeded"); + Ok(()) + } +} diff --git a/src/cmd/hash.rs b/src/cmd/hash.rs new file mode 100644 index 0000000..d290182 --- /dev/null +++ b/src/cmd/hash.rs @@ -0,0 +1,98 @@ +use std::io::{self, Read}; +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::MaxBuffer; +use tss_esapi::tss2_esys::TPMT_TK_HASHCHECK; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::output; +use crate::parse; + +/// Compute a hash using the TPM. +/// +/// Reads data from stdin or a file and produces a hash digest. +#[derive(Parser)] +pub struct HashCmd { + /// Hash algorithm (sha1, sha256, sha384, sha512) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub algorithm: String, + + /// Hierarchy for the ticket (owner, endorsement, platform, null) + #[arg(short = 'C', long = "hierarchy", default_value = "owner")] + pub hierarchy: String, + + /// Input file (default: stdin) + pub input_file: Option, + + /// Output hash as hex string + #[arg(long)] + pub hex: bool, + + /// Output file for the hash digest + #[arg(short = 'o', long)] + pub output: Option, + + /// Output file for the ticket + #[arg(short = 't', long = "ticket")] + pub ticket: Option, +} + +impl HashCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let alg = parse::parse_hashing_algorithm(&self.algorithm)?; + let hierarchy = parse::parse_hierarchy(&self.hierarchy)?; + + let data = read_input(&self.input_file)?; + let buffer = + MaxBuffer::try_from(data).map_err(|e| anyhow::anyhow!("input too large: {e}"))?; + + let (digest, ticket) = ctx + .execute_without_session(|ctx| ctx.hash(buffer.clone(), alg, hierarchy)) + .context("TPM2_Hash failed")?; + + let bytes = digest.value(); + + if let Some(ref path) = self.output { + output::write_to_file(path, bytes)?; + info!("hash saved to {}", path.display()); + } else if self.hex { + output::print_hex(bytes); + } else { + output::write_binary_stdout(bytes)?; + } + + if let Some(ref path) = self.ticket { + let tss_ticket: TPMT_TK_HASHCHECK = ticket + .try_into() + .map_err(|e| anyhow::anyhow!("failed to convert ticket: {e:?}"))?; + let bytes = unsafe { + std::slice::from_raw_parts( + &tss_ticket as *const TPMT_TK_HASHCHECK as *const u8, + std::mem::size_of::(), + ) + }; + std::fs::write(path, bytes) + .with_context(|| format!("writing ticket to {}", path.display()))?; + info!("ticket saved to {}", path.display()); + } + + Ok(()) + } +} + +fn read_input(path: &Option) -> anyhow::Result> { + match path { + Some(p) => std::fs::read(p).with_context(|| format!("reading {}", p.display())), + None => { + let mut buf = Vec::new(); + io::stdin().read_to_end(&mut buf).context("reading stdin")?; + Ok(buf) + } + } +} diff --git a/src/cmd/hierarchycontrol.rs b/src/cmd/hierarchycontrol.rs new file mode 100644 index 0000000..a4590f3 --- /dev/null +++ b/src/cmd/hierarchycontrol.rs @@ -0,0 +1,74 @@ +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; + +/// Enable or disable use of a hierarchy and its associated NV storage. +/// +/// Wraps TPM2_HierarchyControl (raw FFI). +#[derive(Parser)] +pub struct HierarchyControlCmd { + /// Auth hierarchy (p/platform or o/owner) + #[arg(short = 'C', long = "hierarchy")] + pub auth_hierarchy: String, + + /// Hierarchy to enable/disable (o/owner, e/endorsement, p/platform, n/null) + #[arg()] + pub enable: String, + + /// Auth value + #[arg(short = 'P', long = "auth")] + pub auth: Option, + + /// Set state (true=enable, false=disable) + #[arg(short = 's', long = "state", default_value = "true")] + pub state: bool, +} + +impl HierarchyControlCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let auth_handle = RawEsysContext::resolve_hierarchy(&self.auth_hierarchy)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(auth_handle, auth.value())?; + } + + let enable_hierarchy = match self.enable.to_lowercase().as_str() { + "o" | "owner" => TPM2_RH_OWNER, + "e" | "endorsement" => TPM2_RH_ENDORSEMENT, + "p" | "platform" => TPM2_RH_PLATFORM, + "n" | "null" => TPM2_RH_NULL, + _ => anyhow::bail!("unknown hierarchy: {}", self.enable), + }; + + let state: TPMI_YES_NO = if self.state { 1 } else { 0 }; + + unsafe { + let rc = Esys_HierarchyControl( + raw.ptr(), + auth_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + enable_hierarchy, + state, + ); + if rc != 0 { + anyhow::bail!("Esys_HierarchyControl failed: 0x{rc:08x}"); + } + } + + info!( + "hierarchy {} {}", + self.enable, + if self.state { "enabled" } else { "disabled" } + ); + Ok(()) + } +} diff --git a/src/cmd/import.rs b/src/cmd/import.rs new file mode 100644 index 0000000..0670187 --- /dev/null +++ b/src/cmd/import.rs @@ -0,0 +1,151 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{Data, EncryptedSecret, Private, Public, SymmetricDefinitionObject}; +use tss_esapi::traits::UnMarshall; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_object_from_source}; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Import an external object into the TPM under a parent key. +/// +/// Wraps TPM2_Import. +#[derive(Parser)] +pub struct ImportCmd { + /// Parent key context file path + #[arg( + short = 'C', + long = "parent-context", + conflicts_with = "parent_context_handle" + )] + pub parent_context: Option, + + /// Parent key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "parent-context-handle", value_parser = parse_hex_u32, conflicts_with = "parent_context")] + pub parent_context_handle: Option, + + /// Auth value for the parent key + #[arg(short = 'P', long = "parent-auth")] + pub parent_auth: Option, + + /// Input public file (marshaled TPM2B_PUBLIC) + #[arg(short = 'u', long = "public")] + pub public: PathBuf, + + /// Input duplicate private file (marshaled TPM2B_PRIVATE) + #[arg(short = 'r', long = "private")] + pub private: PathBuf, + + /// Input encrypted seed file (marshaled TPM2B_ENCRYPTED_SECRET) + #[arg(short = 's', long = "encrypted-seed")] + pub encrypted_seed: PathBuf, + + /// Input encryption key file (optional) + #[arg(short = 'k', long = "encryption-key")] + pub encryption_key: Option, + + /// Symmetric algorithm for inner wrapper (aes128cfb, null) + #[arg(short = 'G', long = "wrapper-algorithm", default_value = "null")] + pub wrapper_algorithm: String, + + /// Output file for the imported private + #[arg(short = 'o', long = "output")] + pub output: PathBuf, + + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl ImportCmd { + 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 parent_handle = load_object_from_source(&mut ctx, &self.parent_context_source()?)?; + + if let Some(ref auth_str) = self.parent_auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(parent_handle, auth) + .context("tr_set_auth failed")?; + } + + let pub_data = std::fs::read(&self.public) + .with_context(|| format!("reading public from {}", self.public.display()))?; + let public = Public::unmarshall(&pub_data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal public: {e}"))?; + + let priv_data = std::fs::read(&self.private) + .with_context(|| format!("reading private from {}", self.private.display()))?; + let duplicate = Private::try_from(priv_data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal private: {e}"))?; + + let seed_data = std::fs::read(&self.encrypted_seed) + .with_context(|| format!("reading seed from {}", self.encrypted_seed.display()))?; + let encrypted_secret = EncryptedSecret::try_from(seed_data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal encrypted seed: {e}"))?; + + let enc_key = match &self.encryption_key { + Some(path) => { + let data = std::fs::read(path) + .with_context(|| format!("reading encryption key from {}", path.display()))?; + Some( + Data::try_from(data) + .map_err(|e| anyhow::anyhow!("encryption key too large: {e}"))?, + ) + } + None => None, + }; + + let sym_alg = parse_wrapper_algorithm(&self.wrapper_algorithm)?; + + let session_path = self.session.as_deref(); + let imported_private = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.import( + parent_handle, + enc_key.clone(), + public.clone(), + duplicate.clone(), + encrypted_secret.clone(), + sym_alg, + ) + }) + .context("TPM2_Import failed")?; + + let out_bytes = imported_private.value(); + std::fs::write(&self.output, out_bytes) + .with_context(|| format!("writing output to {}", self.output.display()))?; + info!("imported private saved to {}", self.output.display()); + + Ok(()) + } +} + +fn parse_wrapper_algorithm(s: &str) -> anyhow::Result { + match s.to_lowercase().as_str() { + "null" => Ok(SymmetricDefinitionObject::Null), + "aes128cfb" | "aes" => Ok(SymmetricDefinitionObject::Aes { + key_bits: tss_esapi::interface_types::key_bits::AesKeyBits::Aes128, + mode: tss_esapi::interface_types::algorithm::SymmetricMode::Cfb, + }), + "aes256cfb" => Ok(SymmetricDefinitionObject::Aes { + key_bits: tss_esapi::interface_types::key_bits::AesKeyBits::Aes256, + mode: tss_esapi::interface_types::algorithm::SymmetricMode::Cfb, + }), + _ => anyhow::bail!("unsupported wrapper algorithm: {s}"), + } +} diff --git a/src/cmd/incrementalselftest.rs b/src/cmd/incrementalselftest.rs new file mode 100644 index 0000000..fe2eeb2 --- /dev/null +++ b/src/cmd/incrementalselftest.rs @@ -0,0 +1,77 @@ +use anyhow::bail; +use clap::Parser; +use log::info; +use tss_esapi::constants::tss::*; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::raw_esys::RawEsysContext; + +/// Run incremental self test on specified algorithms. +/// +/// Wraps TPM2_IncrementalSelfTest (raw FFI). +#[derive(Parser)] +pub struct IncrementalSelfTestCmd { + /// Algorithms to test (comma-separated: sha1,sha256,rsa,ecc,aes) + #[arg(default_value = "sha256")] + pub algorithms: String, +} + +impl IncrementalSelfTestCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + + let alg_ids = parse_algorithm_list(&self.algorithms)?; + + let mut alg_list = TPML_ALG { + count: alg_ids.len() as u32, + ..Default::default() + }; + for (i, &alg) in alg_ids.iter().enumerate() { + alg_list.algorithms[i] = alg; + } + + unsafe { + let mut to_do_list: *mut TPML_ALG = std::ptr::null_mut(); + let rc = Esys_IncrementalSelfTest( + raw.ptr(), + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &alg_list, + &mut to_do_list, + ); + if rc != 0 { + bail!("Esys_IncrementalSelfTest failed: 0x{rc:08x}"); + } + + if !to_do_list.is_null() { + let todo = &*to_do_list; + if todo.count > 0 { + info!("{} algorithms still need testing", todo.count); + } else { + info!("all requested algorithms tested"); + } + Esys_Free(to_do_list as *mut _); + } + } + + Ok(()) + } +} + +fn parse_algorithm_list(s: &str) -> anyhow::Result> { + s.split(',') + .map(|alg| match alg.trim().to_lowercase().as_str() { + "sha1" | "sha" => Ok(TPM2_ALG_SHA1), + "sha256" => Ok(TPM2_ALG_SHA256), + "sha384" => Ok(TPM2_ALG_SHA384), + "sha512" => Ok(TPM2_ALG_SHA512), + "rsa" => Ok(TPM2_ALG_RSA), + "ecc" => Ok(TPM2_ALG_ECC), + "aes" => Ok(TPM2_ALG_AES), + "hmac" => Ok(TPM2_ALG_HMAC), + _ => bail!("unknown algorithm: {alg}"), + }) + .collect() +} diff --git a/src/cmd/load.rs b/src/cmd/load.rs new file mode 100644 index 0000000..8112819 --- /dev/null +++ b/src/cmd/load.rs @@ -0,0 +1,97 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{Private, PublicBuffer}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_key_from_source}; +use crate::parse::parse_hex_u32; +use crate::session::execute_with_optional_session; + +/// Load a key (private + public) into the TPM under a parent. +/// +/// The private and public files should be in raw TPM marshaled binary format +/// as produced by `tpm2 create`. +#[derive(Parser)] +pub struct LoadCmd { + /// Parent key context file path + #[arg( + short = 'C', + long = "parent-context", + conflicts_with = "parent_context_handle" + )] + pub parent_context: Option, + + /// Parent key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "parent-context-handle", value_parser = parse_hex_u32, conflicts_with = "parent_context")] + pub parent_context_handle: Option, + + /// Private key file (raw binary) + #[arg(short = 'r', long = "private")] + pub private: PathBuf, + + /// Public key file (raw binary) + #[arg(short = 'u', long = "public")] + pub public: PathBuf, + + /// Output context file for the loaded key + #[arg(short = 'c', long = "context")] + pub context: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl LoadCmd { + 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 parent_handle = load_key_from_source(&mut ctx, &self.parent_context_source()?)?; + + let priv_bytes = std::fs::read(&self.private) + .with_context(|| format!("reading private file: {}", self.private.display()))?; + let pub_bytes = std::fs::read(&self.public) + .with_context(|| format!("reading public file: {}", self.public.display()))?; + + let private = Private::try_from(priv_bytes.as_slice()) + .map_err(|e| anyhow::anyhow!("invalid private: {e}"))?; + let pub_buffer = PublicBuffer::try_from(pub_bytes) + .map_err(|e| anyhow::anyhow!("invalid public buffer: {e}"))?; + let public = tss_esapi::structures::Public::try_from(pub_buffer) + .map_err(|e| anyhow::anyhow!("invalid public: {e}"))?; + + let session_path = self.session.as_deref(); + let key_handle = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.load(parent_handle, private.clone(), public.clone()) + }) + .context("TPM2_Load failed")?; + + println!("handle: 0x{:08x}", u32::from(key_handle)); + + if let Some(ref path) = self.context { + let saved = ctx + .context_save(key_handle.into()) + .context("context_save failed")?; + let json = serde_json::to_string(&saved)?; + std::fs::write(path, json) + .with_context(|| format!("writing context to {}", path.display()))?; + info!("context saved to {}", path.display()); + } + + Ok(()) + } +} diff --git a/src/cmd/loadexternal.rs b/src/cmd/loadexternal.rs new file mode 100644 index 0000000..fb466ff --- /dev/null +++ b/src/cmd/loadexternal.rs @@ -0,0 +1,113 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{Public, Sensitive}; +use tss_esapi::traits::UnMarshall; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse; + +/// Load an external key into the TPM. +/// +/// Wraps TPM2_LoadExternal: loads a key that was not created by the TPM +/// into a transient handle. This is useful for importing external public +/// keys for signature verification. +#[derive(Parser)] +pub struct LoadExternalCmd { + /// Input file for the public portion (marshaled TPM2B_PUBLIC) + #[arg(short = 'u', long = "public")] + pub public: PathBuf, + + /// Input file for the private/sensitive portion (marshaled TPMT_SENSITIVE) + #[arg(short = 'r', long = "private")] + pub private: Option, + + /// Hierarchy (o/owner, p/platform, e/endorsement, n/null) + #[arg(short = 'a', long = "hierarchy", default_value = "n")] + pub hierarchy: String, + + /// Output file for the loaded key context + #[arg(short = 'c', long = "key-context")] + pub key_context: Option, + + /// Print name of the loaded object + #[arg(short = 'n', long = "name")] + pub name: Option, +} + +impl LoadExternalCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + let hierarchy = parse::parse_hierarchy(&self.hierarchy)?; + + let pub_data = std::fs::read(&self.public) + .with_context(|| format!("reading public from {}", self.public.display()))?; + let public = Public::unmarshall(&pub_data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal public: {e}"))?; + + let sensitive = match &self.private { + Some(path) => { + let priv_data = std::fs::read(path) + .with_context(|| format!("reading private from {}", path.display()))?; + Sensitive::unmarshall(&priv_data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal sensitive: {e}"))? + } + None => { + // Load public-only (for verification keys etc). + // Use an empty sensitive with matching type. + return self.load_public_only(&mut ctx, public, hierarchy); + } + }; + + let key_handle = ctx + .execute_without_session(|ctx| ctx.load_external(sensitive, public, hierarchy)) + .context("TPM2_LoadExternal failed")?; + + self.save_context(&mut ctx, key_handle.into())?; + info!("loaded external key"); + Ok(()) + } + + fn load_public_only( + &self, + ctx: &mut tss_esapi::Context, + public: Public, + hierarchy: tss_esapi::interface_types::resource_handles::Hierarchy, + ) -> anyhow::Result<()> { + let key_handle = ctx + .execute_without_session(|ctx| ctx.load_external_public(public, hierarchy)) + .context("TPM2_LoadExternal (public only) failed")?; + + self.save_context(ctx, key_handle.into())?; + info!("loaded external public key"); + Ok(()) + } + + fn save_context( + &self, + ctx: &mut tss_esapi::Context, + handle: tss_esapi::handles::ObjectHandle, + ) -> anyhow::Result<()> { + if let Some(ref path) = self.key_context { + let saved = ctx.context_save(handle).context("context_save failed")?; + let json = serde_json::to_string(&saved)?; + std::fs::write(path, json) + .with_context(|| format!("writing context to {}", path.display()))?; + info!("key context saved to {}", path.display()); + } + + if let Some(ref path) = self.name { + let (_, name, _) = ctx + .execute_without_session(|ctx| ctx.read_public(handle.into())) + .context("TPM2_ReadPublic failed")?; + std::fs::write(path, name.value()) + .with_context(|| format!("writing name to {}", path.display()))?; + info!("name saved to {}", path.display()); + } + + Ok(()) + } +} diff --git a/src/cmd/makecredential.rs b/src/cmd/makecredential.rs new file mode 100644 index 0000000..89dcf54 --- /dev/null +++ b/src/cmd/makecredential.rs @@ -0,0 +1,95 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::interface_types::resource_handles::Hierarchy; +use tss_esapi::structures::{Digest, Name, Public}; +use tss_esapi::traits::UnMarshall; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Create a credential blob for a TPM key. +/// +/// Wraps `TPM2_MakeCredential`: encrypts a caller-chosen secret so that only +/// the TPM holding the matching private key (typically an EK) can recover it, +/// and only when the correct object name (typically an AK name) is presented. +#[derive(Parser)] +pub struct MakeCredentialCmd { + /// Public key file (TPM2B_PUBLIC, marshaled binary) used to wrap the seed + #[arg(short = 'u', long = "public")] + pub public: PathBuf, + + /// File containing the secret to protect + #[arg(short = 's', long = "secret")] + pub secret: PathBuf, + + /// File containing the name of the key the credential is bound to (binary) + #[arg(short = 'n', long = "name")] + pub name: PathBuf, + + /// Output credential blob file + #[arg(short = 'o', long = "credential-blob")] + pub credential_blob: PathBuf, +} + +impl MakeCredentialCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + // Read inputs. + let pub_bytes = std::fs::read(&self.public) + .with_context(|| format!("reading public key: {}", self.public.display()))?; + let secret_bytes = std::fs::read(&self.secret) + .with_context(|| format!("reading secret: {}", self.secret.display()))?; + let name_bytes = std::fs::read(&self.name) + .with_context(|| format!("reading name: {}", self.name.display()))?; + + let public = Public::unmarshall(&pub_bytes).context("failed to parse TPM2B_PUBLIC")?; + let credential = + Digest::try_from(secret_bytes).map_err(|e| anyhow::anyhow!("invalid secret: {e}"))?; + let object_name = + Name::try_from(name_bytes).map_err(|e| anyhow::anyhow!("invalid name: {e}"))?; + + // Load the public key into the TPM (public-only, no private part). + let key_handle = ctx + .execute_without_session(|ctx| { + ctx.load_external_public(public.clone(), Hierarchy::Null) + }) + .context("TPM2_LoadExternal failed")?; + + // Make credential. + let (id_object, encrypted_secret) = ctx + .execute_without_session(|ctx| { + ctx.make_credential(key_handle, credential.clone(), object_name.clone()) + }) + .context("TPM2_MakeCredential failed")?; + + // Write credential blob: [u16 id_len][id_data][u16 secret_len][secret_data]. + let id_data = id_object.value(); + let secret_data = encrypted_secret.value(); + let mut blob = Vec::with_capacity(4 + id_data.len() + secret_data.len()); + blob.extend_from_slice(&(id_data.len() as u16).to_be_bytes()); + blob.extend_from_slice(id_data); + blob.extend_from_slice(&(secret_data.len() as u16).to_be_bytes()); + blob.extend_from_slice(secret_data); + + std::fs::write(&self.credential_blob, &blob).with_context(|| { + format!( + "writing credential blob to {}", + self.credential_blob.display() + ) + })?; + info!( + "credential blob saved to {}", + self.credential_blob.display() + ); + + // Flush the externally-loaded key. + ctx.flush_context(key_handle.into()) + .context("failed to flush loaded key")?; + + Ok(()) + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs new file mode 100644 index 0000000..fc2531b --- /dev/null +++ b/src/cmd/mod.rs @@ -0,0 +1,100 @@ +pub mod activatecredential; +pub mod certify; +pub mod certifycreation; +pub mod changeauth; +pub mod changeeps; +pub mod changepps; +pub mod checkquote; +pub mod clear; +pub mod clearcontrol; +pub mod clockrateadjust; +pub mod commit; +pub mod create; +pub mod createak; +pub mod createek; +pub mod createpolicy; +pub mod createprimary; +pub mod decrypt; +pub mod dictionarylockout; +pub mod duplicate; +pub mod ecdhkeygen; +pub mod ecdhzgen; +pub mod ecephemeral; +pub mod encrypt; +pub mod encryptdecrypt; +pub mod eventlog; +pub mod evictcontrol; +pub mod flushcontext; +pub mod getcap; +pub mod getcommandauditdigest; +pub mod geteccparameters; +pub mod getekcertificate; +pub mod getrandom; +pub mod getsessionauditdigest; +pub mod gettestresult; +pub mod gettime; +pub mod hash; +pub mod hierarchycontrol; +pub mod import; +pub mod incrementalselftest; +pub mod load; +pub mod loadexternal; +pub mod makecredential; +pub mod nvcertify; +pub mod nvdefine; +pub mod nvextend; +pub mod nvincrement; +pub mod nvread; +pub mod nvreadlock; +pub mod nvreadpublic; +pub mod nvsetbits; +pub mod nvundefine; +pub mod nvwrite; +pub mod nvwritelock; +pub mod pcrallocate; +pub mod pcrevent; +pub mod pcrextend; +pub mod pcrread; +pub mod pcrreset; +pub mod policyauthorize; +pub mod policyauthorizenv; +pub mod policyauthvalue; +pub mod policycommandcode; +pub mod policycountertimer; +pub mod policycphash; +pub mod policyduplicationselect; +pub mod policylocality; +pub mod policynamehash; +pub mod policynv; +pub mod policynvwritten; +pub mod policyor; +pub mod policypassword; +pub mod policypcr; +pub mod policyrestart; +pub mod policysecret; +pub mod policysigned; +pub mod policytemplate; +pub mod policyticket; +pub mod print; +pub mod quote; +pub mod rcdecode; +pub mod readclock; +pub mod readpublic; +pub mod rsadecrypt; +pub mod rsaencrypt; +pub mod selftest; +pub mod send; +pub mod sessionconfig; +pub mod setclock; +pub mod setcommandauditstatus; +pub mod setprimarypolicy; +pub mod shutdown; +pub mod sign; +pub mod startauthsession; +pub mod startup; +pub mod stirrandom; +pub mod testparms; +pub mod tpmhmac; +pub mod unseal; +pub mod verifysignature; +pub mod zgen2phase; diff --git a/src/cmd/nvcertify.rs b/src/cmd/nvcertify.rs new file mode 100644 index 0000000..4357e2b --- /dev/null +++ b/src/cmd/nvcertify.rs @@ -0,0 +1,155 @@ +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 contents of an NV index. +/// +/// Wraps TPM2_NV_Certify (raw FFI). +#[derive(Parser)] +pub struct NvCertifyCmd { + /// Signing key context file path + #[arg( + short = 'C', + long = "signing-key-context", + conflicts_with = "signing_key_context_handle" + )] + pub signing_key_context: Option, + + /// Signing key handle (hex, e.g. 0x81000001) + #[arg(long = "signing-key-context-handle", value_parser = parse_hex_u32, conflicts_with = "signing_key_context")] + pub signing_key_context_handle: Option, + + /// NV index to certify (hex, e.g. 0x01000001) + #[arg(short = 'i', long = "nv-index")] + pub nv_index: String, + + /// Auth hierarchy for the NV index (o/p/e) + #[arg(short = 'c', long = "nv-auth-hierarchy", default_value = "o")] + pub nv_auth_hierarchy: String, + + /// Auth value for the signing key + #[arg(short = 'P', long = "signing-key-auth")] + pub signing_key_auth: Option, + + /// Auth value for the NV index + #[arg(short = 'p', long = "nv-auth")] + pub nv_auth: Option, + + /// Hash algorithm for the signature + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Size of data to certify + #[arg(short = 's', long = "size", default_value = "0")] + pub size: u16, + + /// Offset within the NV index + #[arg(long = "offset", default_value = "0")] + pub offset: u16, + + /// Output file for the attestation data + #[arg(short = 'o', long = "attestation")] + pub attestation: Option, + + /// Output file for the signature + #[arg(long = "signature")] + pub signature: Option, + + /// Qualifying data (nonce) + #[arg(short = 'q', long = "qualification")] + pub qualification: Option, +} + +impl NvCertifyCmd { + fn signing_key_context_source(&self) -> anyhow::Result { + match (&self.signing_key_context, self.signing_key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --signing-key-context or --signing-key-context-handle must be provided" + ), + } + } + + 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_key_context_source()?)?; + + let nv_handle = raw.resolve_nv_index(&self.nv_index)?; + + let auth_handle = if self.nv_auth_hierarchy == "nv" { + nv_handle + } else { + RawEsysContext::resolve_hierarchy(&self.nv_auth_hierarchy)? + }; + + if let Some(ref auth_str) = self.signing_key_auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(sign_handle, auth.value())?; + } + if let Some(ref auth_str) = self.nv_auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(auth_handle, auth.value())?; + } + + let mut qualifying_data = TPM2B_DATA::default(); + if let Some(ref q) = self.qualification { + let bytes = hex::decode(q).context("invalid qualifying data hex")?; + qualifying_data.size = bytes.len() as u16; + qualifying_data.buffer[..bytes.len()].copy_from_slice(&bytes); + } + + let scheme = TPMT_SIG_SCHEME { + scheme: TPM2_ALG_NULL, + details: 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_NV_Certify( + raw.ptr(), + sign_handle, + auth_handle, + nv_handle, + ESYS_TR_PASSWORD, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + &qualifying_data, + &scheme, + self.size, + self.offset, + &mut certify_info, + &mut sig, + ); + if rc != 0 { + anyhow::bail!("Esys_NV_Certify 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!("NV certify succeeded"); + Ok(()) + } +} diff --git a/src/cmd/nvdefine.rs b/src/cmd/nvdefine.rs new file mode 100644 index 0000000..5bda32a --- /dev/null +++ b/src/cmd/nvdefine.rs @@ -0,0 +1,83 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::handles::NvIndexTpmHandle; +use tss_esapi::structures::NvPublicBuilder; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Define a new NV index. +#[derive(Parser)] +pub struct NvDefineCmd { + /// NV index handle (hex, e.g. 0x01400001) + #[arg(value_parser = parse_hex_u32)] + pub nv_index: u32, + + /// Authorization hierarchy (o/owner, p/platform) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Size of the NV area in bytes + #[arg(short = 's', long = "size", default_value = "0")] + pub size: u16, + + /// Hash algorithm for the NV index + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub algorithm: String, + + /// NV attributes as raw hex or symbolic names + #[arg( + short = 'a', + long = "attributes", + default_value = "ownerwrite|ownerread" + )] + pub attributes: String, + + /// Authorization value for the NV area + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl NvDefineCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let provision = parse::parse_provision(&self.hierarchy)?; + let nv_handle = NvIndexTpmHandle::new(self.nv_index) + .map_err(|e| anyhow::anyhow!("invalid NV index handle: {e}"))?; + let alg = parse::parse_hashing_algorithm(&self.algorithm)?; + + let nv_attributes = parse::parse_nv_attributes(&self.attributes)?; + + let auth = match &self.auth { + Some(a) => Some(parse::parse_auth(a)?), + None => None, + }; + + let nv_public = NvPublicBuilder::new() + .with_nv_index(nv_handle) + .with_index_name_algorithm(alg) + .with_index_attributes(nv_attributes) + .with_data_area_size(self.size as usize) + .build() + .context("failed to build NvPublic")?; + + let session_path = self.session.as_deref(); + execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.nv_define_space(provision, auth.clone(), nv_public.clone()) + }) + .context("TPM2_NV_DefineSpace failed")?; + + info!("NV index 0x{:08x} defined", self.nv_index); + Ok(()) + } +} diff --git a/src/cmd/nvextend.rs b/src/cmd/nvextend.rs new file mode 100644 index 0000000..04c1e20 --- /dev/null +++ b/src/cmd/nvextend.rs @@ -0,0 +1,83 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Extend an NV index with additional data. +/// +/// Wraps TPM2_NV_Extend (raw FFI). +#[derive(Parser)] +pub struct NvExtendCmd { + /// NV index (hex, e.g. 0x01000001) + #[arg()] + pub nv_index: String, + + /// Authorization hierarchy (o/owner, p/platform) or "index" + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Auth value + #[arg(short = 'P', long = "auth")] + pub auth: Option, + + /// Input data file to extend + #[arg(short = 'i', long = "input")] + pub input: PathBuf, +} + +impl NvExtendCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + + let nv_index_val = + parse::parse_hex_u32(&self.nv_index).map_err(|e| anyhow::anyhow!("{e}"))?; + + let nv_handle = raw.tr_from_tpm_public(nv_index_val)?; + + let auth_handle = match self.hierarchy.to_lowercase().as_str() { + "o" | "owner" => ESYS_TR_RH_OWNER, + "p" | "platform" => ESYS_TR_RH_PLATFORM, + _ => nv_handle, + }; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(auth_handle, auth.value())?; + } + + let data = std::fs::read(&self.input) + .with_context(|| format!("reading input from {}", self.input.display()))?; + + let mut nv_data = TPM2B_MAX_NV_BUFFER::default(); + let len = data.len().min(nv_data.buffer.len()); + nv_data.size = len as u16; + nv_data.buffer[..len].copy_from_slice(&data[..len]); + + unsafe { + let rc = Esys_NV_Extend( + raw.ptr(), + auth_handle, + nv_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &nv_data, + ); + if rc != 0 { + anyhow::bail!("Esys_NV_Extend failed: 0x{rc:08x}"); + } + } + + info!( + "NV index 0x{nv_index_val:08x} extended with {} bytes", + data.len() + ); + Ok(()) + } +} diff --git a/src/cmd/nvincrement.rs b/src/cmd/nvincrement.rs new file mode 100644 index 0000000..41745d1 --- /dev/null +++ b/src/cmd/nvincrement.rs @@ -0,0 +1,75 @@ +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::handles::NvIndexTpmHandle; +use tss_esapi::interface_types::resource_handles::NvAuth; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::resolve_nv_auth; +use crate::parse::parse_hex_u32; +use crate::session::execute_with_optional_session; + +/// Increment a monotonic counter NV index. +/// +/// Wraps TPM2_NV_Increment. +#[derive(Parser)] +pub struct NvIncrementCmd { + /// NV index (hex, e.g. 0x01000001) + #[arg(value_parser = parse_hex_u32)] + pub nv_index: u32, + + /// Authorization hierarchy (o/owner, p/platform) or NV index itself + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Auth value for the hierarchy or NV index + #[arg(short = 'P', long = "auth")] + pub auth: Option, + + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl NvIncrementCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let nv_handle = NvIndexTpmHandle::new(self.nv_index) + .map_err(|e| anyhow::anyhow!("invalid NV index 0x{:08x}: {e}", self.nv_index))?; + let tpm_handle: tss_esapi::handles::TpmHandle = nv_handle.into(); + let nv_idx = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .with_context(|| format!("failed to load NV index 0x{:08x}", self.nv_index))?; + + let nv_auth = resolve_nv_auth(&mut ctx, &self.hierarchy, nv_handle)?; + + if let Some(ref auth_str) = self.auth { + let auth = crate::parse::parse_auth(auth_str)?; + match &nv_auth { + NvAuth::Owner => { + ctx.tr_set_auth(tss_esapi::handles::ObjectHandle::Owner, auth) + .context("tr_set_auth failed")?; + } + NvAuth::Platform => { + ctx.tr_set_auth(tss_esapi::handles::ObjectHandle::Platform, auth) + .context("tr_set_auth failed")?; + } + NvAuth::NvIndex(h) => { + ctx.tr_set_auth((*h).into(), auth) + .context("tr_set_auth failed")?; + } + } + } + + let session_path = self.session.as_deref(); + execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.nv_increment(nv_auth, nv_idx.into()) + }) + .context("TPM2_NV_Increment failed")?; + + info!("NV index 0x{:08x} incremented", self.nv_index); + Ok(()) + } +} diff --git a/src/cmd/nvread.rs b/src/cmd/nvread.rs new file mode 100644 index 0000000..1590375 --- /dev/null +++ b/src/cmd/nvread.rs @@ -0,0 +1,89 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::handles::NvIndexTpmHandle; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::resolve_nv_auth; +use crate::output; +use crate::parse::parse_hex_u32; +use crate::session::execute_with_optional_session; + +/// Read data from an NV index. +#[derive(Parser)] +pub struct NvReadCmd { + /// NV index handle (hex, e.g. 0x01400001) + #[arg(value_parser = parse_hex_u32)] + pub nv_index: u32, + + /// Authorization hierarchy (o/owner, p/platform, or the NV index itself) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Number of bytes to read + #[arg(short = 's', long = "size")] + pub size: Option, + + /// Offset within the NV area + #[arg(long = "offset", default_value = "0")] + pub offset: u16, + + /// Output file + #[arg(short = 'o', long)] + pub output: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl NvReadCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let nv_handle = NvIndexTpmHandle::new(self.nv_index) + .map_err(|e| anyhow::anyhow!("invalid NV index handle: {e}"))?; + + // Determine size from NV public if not specified + let size = match self.size { + Some(s) => s, + None => { + let tpm_handle: tss_esapi::handles::TpmHandle = nv_handle.into(); + let nv_idx = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .context("failed to load NV index")?; + let (nv_public, _) = ctx + .execute_without_session(|ctx| ctx.nv_read_public(nv_idx.into())) + .context("TPM2_NV_ReadPublic failed")?; + nv_public.data_size() as u16 + } + }; + + let nv_auth = resolve_nv_auth(&mut ctx, &self.hierarchy, nv_handle)?; + + let tpm_handle: tss_esapi::handles::TpmHandle = nv_handle.into(); + let nv_idx = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .context("failed to load NV index")?; + + let session_path = self.session.as_deref(); + let data = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.nv_read(nv_auth, nv_idx.into(), size, self.offset) + }) + .context("TPM2_NV_Read failed")?; + + let bytes = data.value(); + + if let Some(ref path) = self.output { + output::write_to_file(path, bytes)?; + info!("wrote {} bytes to {}", bytes.len(), path.display()); + } else { + output::print_hex(bytes); + } + + Ok(()) + } +} diff --git a/src/cmd/nvreadlock.rs b/src/cmd/nvreadlock.rs new file mode 100644 index 0000000..29a9141 --- /dev/null +++ b/src/cmd/nvreadlock.rs @@ -0,0 +1,62 @@ +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Lock an NV index for reading (until next TPM reset). +/// +/// Wraps TPM2_NV_ReadLock (raw FFI). +#[derive(Parser)] +pub struct NvReadLockCmd { + /// NV index (hex) + #[arg()] + pub nv_index: String, + + /// Authorization hierarchy (o/owner, p/platform) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Auth value + #[arg(short = 'P', long = "auth")] + pub auth: Option, +} + +impl NvReadLockCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let nv_index_val = + parse::parse_hex_u32(&self.nv_index).map_err(|e| anyhow::anyhow!("{e}"))?; + let nv_handle = raw.tr_from_tpm_public(nv_index_val)?; + + let auth_handle = match self.hierarchy.to_lowercase().as_str() { + "o" | "owner" => ESYS_TR_RH_OWNER, + "p" | "platform" => ESYS_TR_RH_PLATFORM, + _ => nv_handle, + }; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(auth_handle, auth.value())?; + } + + unsafe { + let rc = Esys_NV_ReadLock( + raw.ptr(), + auth_handle, + nv_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + ); + if rc != 0 { + anyhow::bail!("Esys_NV_ReadLock failed: 0x{rc:08x}"); + } + } + + info!("NV index 0x{nv_index_val:08x} read-locked"); + Ok(()) + } +} diff --git a/src/cmd/nvreadpublic.rs b/src/cmd/nvreadpublic.rs new file mode 100644 index 0000000..0fad85a --- /dev/null +++ b/src/cmd/nvreadpublic.rs @@ -0,0 +1,48 @@ +use anyhow::Context; +use clap::Parser; +use serde_json::json; +use tss_esapi::handles::NvIndexTpmHandle; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse::parse_hex_u32; + +/// Read the public area of an NV index. +/// +/// Wraps TPM2_NV_ReadPublic: displays the NV index attributes, size, +/// hash algorithm, and name. +#[derive(Parser)] +pub struct NvReadPublicCmd { + /// NV index (hex, e.g. 0x01000001) + #[arg(value_parser = parse_hex_u32)] + pub nv_index: u32, +} + +impl NvReadPublicCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let nv_handle = NvIndexTpmHandle::new(self.nv_index) + .map_err(|e| anyhow::anyhow!("invalid NV index 0x{:08x}: {e}", self.nv_index))?; + + let tpm_handle: tss_esapi::handles::TpmHandle = nv_handle.into(); + let nv_idx = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .with_context(|| format!("failed to load NV index 0x{:08x}", self.nv_index))?; + + let (nv_public, name) = ctx + .execute_without_session(|ctx| ctx.nv_read_public(nv_idx.into())) + .context("TPM2_NV_ReadPublic failed")?; + + let output = json!({ + "nv_index": format!("0x{:08x}", self.nv_index), + "name": hex::encode(name.value()), + "hash_algorithm": format!("{:?}", nv_public.name_algorithm()), + "attributes": format!("{:?}", nv_public.attributes()), + "data_size": nv_public.data_size(), + }); + + println!("{}", serde_json::to_string_pretty(&output)?); + Ok(()) + } +} diff --git a/src/cmd/nvsetbits.rs b/src/cmd/nvsetbits.rs new file mode 100644 index 0000000..2ab60f6 --- /dev/null +++ b/src/cmd/nvsetbits.rs @@ -0,0 +1,75 @@ +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Set bits in a bit-field NV index. +/// +/// Wraps TPM2_NV_SetBits (raw FFI). +#[derive(Parser)] +pub struct NvSetBitsCmd { + /// NV index (hex) + #[arg()] + pub nv_index: String, + + /// Authorization hierarchy (o/owner, p/platform) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Auth value + #[arg(short = 'P', long = "auth")] + pub auth: Option, + + /// Bits to set (hex u64 value) + #[arg(short = 'i', long = "bits")] + pub bits: String, +} + +impl NvSetBitsCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let nv_index_val = + parse::parse_hex_u32(&self.nv_index).map_err(|e| anyhow::anyhow!("{e}"))?; + let nv_handle = raw.tr_from_tpm_public(nv_index_val)?; + + let auth_handle = match self.hierarchy.to_lowercase().as_str() { + "o" | "owner" => ESYS_TR_RH_OWNER, + "p" | "platform" => ESYS_TR_RH_PLATFORM, + _ => nv_handle, + }; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(auth_handle, auth.value())?; + } + + let stripped = self + .bits + .strip_prefix("0x") + .or_else(|| self.bits.strip_prefix("0X")) + .unwrap_or(&self.bits); + let bits: u64 = u64::from_str_radix(stripped, 16) + .map_err(|_| anyhow::anyhow!("invalid bits value: {}", self.bits))?; + + unsafe { + let rc = Esys_NV_SetBits( + raw.ptr(), + auth_handle, + nv_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + bits, + ); + if rc != 0 { + anyhow::bail!("Esys_NV_SetBits failed: 0x{rc:08x}"); + } + } + + info!("NV index 0x{nv_index_val:08x} bits set to 0x{bits:016x}"); + Ok(()) + } +} diff --git a/src/cmd/nvundefine.rs b/src/cmd/nvundefine.rs new file mode 100644 index 0000000..24a7356 --- /dev/null +++ b/src/cmd/nvundefine.rs @@ -0,0 +1,65 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::handles::{NvIndexTpmHandle, ObjectHandle, TpmHandle}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Remove an NV index from the TPM. +/// +/// The hierarchy used must match the one that authorized creation of the index +/// (`-C o` for owner-created indices, `-C p` for platform-created). +#[derive(Parser)] +pub struct NvUndefineCmd { + /// NV index handle to remove (hex, e.g. 0x01400001) + #[arg(value_parser = parse_hex_u32)] + pub nv_index: u32, + + /// Authorization hierarchy (o/owner, p/platform) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Authorization value for the hierarchy + #[arg(short = 'P', long = "auth")] + pub auth: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl NvUndefineCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let provision = parse::parse_provision(&self.hierarchy)?; + let nv_tpm_handle = NvIndexTpmHandle::new(self.nv_index) + .map_err(|e| anyhow::anyhow!("invalid NV index handle: {e}"))?; + + if let Some(ref auth_str) = self.auth { + let auth_value = parse::parse_auth(auth_str)?; + let hier_obj: ObjectHandle = parse::provision_to_hierarchy_auth(provision).into(); + ctx.tr_set_auth(hier_obj, auth_value) + .context("failed to set hierarchy auth")?; + } + + let tpm_handle: TpmHandle = nv_tpm_handle.into(); + let nv_index_handle = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .context("failed to load NV index handle")?; + + let session_path = self.session.as_deref(); + execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.nv_undefine_space(provision, nv_index_handle.into()) + }) + .context("TPM2_NV_UndefineSpace failed")?; + + info!("NV index 0x{:08x} undefined", self.nv_index); + Ok(()) + } +} diff --git a/src/cmd/nvwrite.rs b/src/cmd/nvwrite.rs new file mode 100644 index 0000000..8e62c30 --- /dev/null +++ b/src/cmd/nvwrite.rs @@ -0,0 +1,77 @@ +use std::io::{self, Read}; +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::handles::NvIndexTpmHandle; +use tss_esapi::structures::MaxNvBuffer; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::resolve_nv_auth; +use crate::parse::parse_hex_u32; +use crate::session::execute_with_optional_session; + +/// Write data to an NV index. +#[derive(Parser)] +pub struct NvWriteCmd { + /// NV index handle (hex, e.g. 0x01400001) + #[arg(value_parser = parse_hex_u32)] + pub nv_index: u32, + + /// Authorization hierarchy (o/owner, p/platform) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Input file (default: stdin) + #[arg(short = 'i', long = "input")] + pub input: Option, + + /// Offset within the NV area + #[arg(long = "offset", default_value = "0")] + pub offset: u16, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl NvWriteCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let nv_handle = NvIndexTpmHandle::new(self.nv_index) + .map_err(|e| anyhow::anyhow!("invalid NV index handle: {e}"))?; + let nv_auth = resolve_nv_auth(&mut ctx, &self.hierarchy, nv_handle)?; + + let data = read_input(&self.input)?; + let buffer = + MaxNvBuffer::try_from(data).map_err(|e| anyhow::anyhow!("data too large: {e}"))?; + + let tpm_handle: tss_esapi::handles::TpmHandle = nv_handle.into(); + let nv_idx = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .context("failed to load NV index")?; + + let session_path = self.session.as_deref(); + execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.nv_write(nv_auth, nv_idx.into(), buffer.clone(), self.offset) + }) + .context("TPM2_NV_Write failed")?; + + info!("data written to NV index 0x{:08x}", self.nv_index); + Ok(()) + } +} + +fn read_input(path: &Option) -> anyhow::Result> { + match path { + Some(p) => std::fs::read(p).with_context(|| format!("reading {}", p.display())), + None => { + let mut buf = Vec::new(); + io::stdin().read_to_end(&mut buf).context("reading stdin")?; + Ok(buf) + } + } +} diff --git a/src/cmd/nvwritelock.rs b/src/cmd/nvwritelock.rs new file mode 100644 index 0000000..828c2d4 --- /dev/null +++ b/src/cmd/nvwritelock.rs @@ -0,0 +1,62 @@ +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Lock an NV index for writing (until next TPM reset). +/// +/// Wraps TPM2_NV_WriteLock (raw FFI). +#[derive(Parser)] +pub struct NvWriteLockCmd { + /// NV index (hex) + #[arg()] + pub nv_index: String, + + /// Authorization hierarchy (o/owner, p/platform) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Auth value + #[arg(short = 'P', long = "auth")] + pub auth: Option, +} + +impl NvWriteLockCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let nv_index_val = + parse::parse_hex_u32(&self.nv_index).map_err(|e| anyhow::anyhow!("{e}"))?; + let nv_handle = raw.tr_from_tpm_public(nv_index_val)?; + + let auth_handle = match self.hierarchy.to_lowercase().as_str() { + "o" | "owner" => ESYS_TR_RH_OWNER, + "p" | "platform" => ESYS_TR_RH_PLATFORM, + _ => nv_handle, + }; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(auth_handle, auth.value())?; + } + + unsafe { + let rc = Esys_NV_WriteLock( + raw.ptr(), + auth_handle, + nv_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + ); + if rc != 0 { + anyhow::bail!("Esys_NV_WriteLock failed: 0x{rc:08x}"); + } + } + + info!("NV index 0x{nv_index_val:08x} write-locked"); + Ok(()) + } +} diff --git a/src/cmd/pcrallocate.rs b/src/cmd/pcrallocate.rs new file mode 100644 index 0000000..0b26a55 --- /dev/null +++ b/src/cmd/pcrallocate.rs @@ -0,0 +1,121 @@ +use anyhow::bail; +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; + +/// Allocate PCR banks with the specified hash algorithms. +/// +/// Wraps TPM2_PCR_Allocate (raw FFI). Changes take effect after TPM reset. +#[derive(Parser)] +pub struct PcrAllocateCmd { + /// Auth hierarchy (p/platform) + #[arg(short = 'C', long = "hierarchy", default_value = "p")] + pub hierarchy: String, + + /// Auth value + #[arg(short = 'P', long = "auth")] + pub auth: Option, + + /// PCR allocation (e.g. sha256:0,1,2+sha1:all) + #[arg()] + pub allocation: String, +} + +impl PcrAllocateCmd { + 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 pcr_allocation = build_pcr_allocation(&self.allocation)?; + + unsafe { + let mut allocation_success: TPMI_YES_NO = 0; + let mut max_pcr: u32 = 0; + let mut size_needed: u32 = 0; + let mut size_available: u32 = 0; + + let rc = Esys_PCR_Allocate( + raw.ptr(), + auth_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &pcr_allocation, + &mut allocation_success, + &mut max_pcr, + &mut size_needed, + &mut size_available, + ); + if rc != 0 { + bail!("Esys_PCR_Allocate failed: 0x{rc:08x}"); + } + + if allocation_success != 0 { + info!( + "PCR allocation succeeded (max_pcr={max_pcr}, needed={size_needed}, available={size_available})" + ); + } else { + info!( + "PCR allocation will take effect after TPM reset (needed={size_needed}, available={size_available})" + ); + } + } + + Ok(()) + } +} + +fn build_pcr_allocation(spec: &str) -> anyhow::Result { + let mut selection = TPML_PCR_SELECTION::default(); + let mut count = 0u32; + + for bank_spec in spec.split('+') { + let (alg_str, indices_str) = bank_spec + .split_once(':') + .ok_or_else(|| anyhow::anyhow!("invalid PCR spec: missing ':' in '{bank_spec}'"))?; + + let alg_id: u16 = match alg_str.to_lowercase().as_str() { + "sha1" | "sha" => TPM2_ALG_SHA1, + "sha256" => TPM2_ALG_SHA256, + "sha384" => TPM2_ALG_SHA384, + "sha512" => TPM2_ALG_SHA512, + _ => anyhow::bail!("unknown hash algorithm: {alg_str}"), + }; + + let mut pcr_select = [0u8; 4]; // 32 PCRs max, pcrSelect is [u8; 4] + if indices_str.eq_ignore_ascii_case("all") { + pcr_select = [0xFF, 0xFF, 0xFF, 0x00]; + } else { + for idx_str in indices_str.split(',') { + let idx: u8 = idx_str + .trim() + .parse() + .map_err(|_| anyhow::anyhow!("invalid PCR index: {idx_str}"))?; + if idx >= 24 { + bail!("PCR index out of range: {idx}"); + } + pcr_select[(idx / 8) as usize] |= 1 << (idx % 8); + } + } + + selection.pcrSelections[count as usize] = TPMS_PCR_SELECTION { + hash: alg_id, + sizeofSelect: 3, + pcrSelect: pcr_select, + }; + count += 1; + } + + selection.count = count; + Ok(selection) +} diff --git a/src/cmd/pcrevent.rs b/src/cmd/pcrevent.rs new file mode 100644 index 0000000..fb98676 --- /dev/null +++ b/src/cmd/pcrevent.rs @@ -0,0 +1,75 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Extend a PCR with event data (TPM hashes the data). +/// +/// Wraps TPM2_PCR_Event (raw FFI). Unlike pcrextend, the TPM hashes +/// the data rather than the caller. +#[derive(Parser)] +pub struct PcrEventCmd { + /// PCR index to extend + #[arg()] + pub pcr_index: u8, + + /// Auth value for the PCR (if needed) + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Input data file to hash and extend + #[arg(short = 'i', long = "input")] + pub input: PathBuf, +} + +impl PcrEventCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + + // Resolve PCR handle. + let pcr_handle = raw.tr_from_tpm_public(self.pcr_index as u32)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(pcr_handle, auth.value())?; + } + + let data = std::fs::read(&self.input) + .with_context(|| format!("reading input from {}", self.input.display()))?; + + let mut event_data = TPM2B_EVENT::default(); + let len = data.len().min(event_data.buffer.len()); + event_data.size = len as u16; + event_data.buffer[..len].copy_from_slice(&data[..len]); + + unsafe { + let mut digests: *mut TPML_DIGEST_VALUES = std::ptr::null_mut(); + let rc = Esys_PCR_Event( + raw.ptr(), + pcr_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &event_data, + &mut digests, + ); + if rc != 0 { + anyhow::bail!("Esys_PCR_Event failed: 0x{rc:08x}"); + } + + if !digests.is_null() { + let d = &*digests; + info!("PCR {} extended with {} digest(s)", self.pcr_index, d.count); + Esys_Free(digests as *mut _); + } + } + + Ok(()) + } +} diff --git a/src/cmd/pcrextend.rs b/src/cmd/pcrextend.rs new file mode 100644 index 0000000..b018470 --- /dev/null +++ b/src/cmd/pcrextend.rs @@ -0,0 +1,105 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::structures::DigestValues; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse; +use crate::session::execute_with_optional_session; + +/// Extend a PCR register with one or more digests. +/// +/// Format: `:=[+=...]` +/// +/// Example: `tpm2 pcrextend 0:sha256=<64-char-hex>` +#[derive(Parser)] +pub struct PcrExtendCmd { + /// PCR extension specification + pub extend_spec: String, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl PcrExtendCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let (pcr_index_str, digests_str) = self + .extend_spec + .split_once(':') + .ok_or_else(|| anyhow::anyhow!("expected format :="))?; + + let pcr_index: u8 = pcr_index_str.parse().context("invalid PCR index")?; + let pcr_handle = pcr_index_to_handle(pcr_index)?; + + let mut digest_values = DigestValues::new(); + + for part in digests_str.split('+') { + let (alg_str, hex_str) = part + .split_once('=') + .ok_or_else(|| anyhow::anyhow!("expected = in '{part}'"))?; + + let alg = parse::parse_hashing_algorithm(alg_str)?; + let bytes = + hex::decode(hex_str).with_context(|| format!("invalid hex in '{hex_str}'"))?; + let digest = bytes + .try_into() + .map_err(|e: tss_esapi::Error| anyhow::anyhow!("{e}"))?; + digest_values.set(alg, digest); + } + + let session_path = self.session.as_deref(); + execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.pcr_extend(pcr_handle, digest_values.clone()) + }) + .context("TPM2_PCR_Extend failed")?; + + info!("PCR {pcr_index} extended"); + Ok(()) + } +} + +fn pcr_index_to_handle(idx: u8) -> anyhow::Result { + use tss_esapi::handles::PcrHandle; + let handle = match idx { + 0 => PcrHandle::Pcr0, + 1 => PcrHandle::Pcr1, + 2 => PcrHandle::Pcr2, + 3 => PcrHandle::Pcr3, + 4 => PcrHandle::Pcr4, + 5 => PcrHandle::Pcr5, + 6 => PcrHandle::Pcr6, + 7 => PcrHandle::Pcr7, + 8 => PcrHandle::Pcr8, + 9 => PcrHandle::Pcr9, + 10 => PcrHandle::Pcr10, + 11 => PcrHandle::Pcr11, + 12 => PcrHandle::Pcr12, + 13 => PcrHandle::Pcr13, + 14 => PcrHandle::Pcr14, + 15 => PcrHandle::Pcr15, + 16 => PcrHandle::Pcr16, + 17 => PcrHandle::Pcr17, + 18 => PcrHandle::Pcr18, + 19 => PcrHandle::Pcr19, + 20 => PcrHandle::Pcr20, + 21 => PcrHandle::Pcr21, + 22 => PcrHandle::Pcr22, + 23 => PcrHandle::Pcr23, + 24 => PcrHandle::Pcr24, + 25 => PcrHandle::Pcr25, + 26 => PcrHandle::Pcr26, + 27 => PcrHandle::Pcr27, + 28 => PcrHandle::Pcr28, + 29 => PcrHandle::Pcr29, + 30 => PcrHandle::Pcr30, + 31 => PcrHandle::Pcr31, + _ => bail!("invalid PCR index: {idx}"), + }; + Ok(handle) +} diff --git a/src/cmd/pcrread.rs b/src/cmd/pcrread.rs new file mode 100644 index 0000000..a2a6809 --- /dev/null +++ b/src/cmd/pcrread.rs @@ -0,0 +1,71 @@ +use std::path::PathBuf; + +use clap::Parser; +use log::info; +use tss_esapi::structures::PcrSlot; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::output; +use crate::parse; +use crate::pcr; + +/// Read PCR values. +/// +/// Specify PCR banks + indices like `sha256:0,1,2+sha1:0,1`. +/// Use `all` for all indices in a bank: `sha256:all`. +/// Without arguments reads all PCR banks. +#[derive(Parser)] +pub struct PcrReadCmd { + /// PCR selection list (e.g. sha256:0,1,2+sha1:all) + pub pcr_list: Option, + + /// Output binary PCR values to a file + #[arg(short = 'o', long)] + pub output: Option, +} + +impl PcrReadCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let selection = match &self.pcr_list { + Some(spec) => parse::parse_pcr_selection(spec)?, + None => parse::default_pcr_selection()?, + }; + + // TPM2_PCR_Read returns at most 8 digests per call; loop until all + // requested PCRs have been read. + let chunks = pcr::pcr_read_all(&mut ctx, selection)?; + + // Print results and optionally accumulate raw bytes for file output. + let mut raw: Vec = Vec::new(); + for (read_sel, digests) in &chunks { + let mut idx = 0; + for sel in read_sel.get_selections() { + let alg = sel.hashing_algorithm(); + let selected: Vec = sel.selected().into_iter().collect(); + for slot in &selected { + if idx < digests.value().len() { + let digest = digests.value()[idx].value(); + let pcr_num = parse::pcr_slot_to_index(*slot); + println!(" {alg:?}:"); + println!(" {pcr_num} : 0x{}", hex::encode(digest)); + if self.output.is_some() { + raw.extend_from_slice(digest); + } + idx += 1; + } + } + } + } + + // Optionally write raw binary of all digests to file + if let Some(ref path) = self.output { + output::write_to_file(path, &raw)?; + info!("wrote PCR binary data to {}", path.display()); + } + + Ok(()) + } +} diff --git a/src/cmd/pcrreset.rs b/src/cmd/pcrreset.rs new file mode 100644 index 0000000..1d57fed --- /dev/null +++ b/src/cmd/pcrreset.rs @@ -0,0 +1,62 @@ +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::handles::PcrHandle; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Reset a PCR to its default value. +/// +/// Wraps TPM2_PCR_Reset. Only resettable PCRs (e.g. PCR 16, debug PCR) +/// can be reset. +#[derive(Parser)] +pub struct PcrResetCmd { + /// PCR index to reset (e.g. 16) + #[arg()] + pub pcr_index: u8, +} + +impl PcrResetCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let pcr_handle = pcr_index_to_handle(self.pcr_index)?; + + ctx.execute_with_nullauth_session(|ctx| ctx.pcr_reset(pcr_handle)) + .context("TPM2_PCR_Reset failed")?; + + info!("PCR {} reset", self.pcr_index); + Ok(()) + } +} + +fn pcr_index_to_handle(idx: u8) -> anyhow::Result { + match idx { + 0 => Ok(PcrHandle::Pcr0), + 1 => Ok(PcrHandle::Pcr1), + 2 => Ok(PcrHandle::Pcr2), + 3 => Ok(PcrHandle::Pcr3), + 4 => Ok(PcrHandle::Pcr4), + 5 => Ok(PcrHandle::Pcr5), + 6 => Ok(PcrHandle::Pcr6), + 7 => Ok(PcrHandle::Pcr7), + 8 => Ok(PcrHandle::Pcr8), + 9 => Ok(PcrHandle::Pcr9), + 10 => Ok(PcrHandle::Pcr10), + 11 => Ok(PcrHandle::Pcr11), + 12 => Ok(PcrHandle::Pcr12), + 13 => Ok(PcrHandle::Pcr13), + 14 => Ok(PcrHandle::Pcr14), + 15 => Ok(PcrHandle::Pcr15), + 16 => Ok(PcrHandle::Pcr16), + 17 => Ok(PcrHandle::Pcr17), + 18 => Ok(PcrHandle::Pcr18), + 19 => Ok(PcrHandle::Pcr19), + 20 => Ok(PcrHandle::Pcr20), + 21 => Ok(PcrHandle::Pcr21), + 22 => Ok(PcrHandle::Pcr22), + 23 => Ok(PcrHandle::Pcr23), + _ => anyhow::bail!("PCR index {idx} out of range (0-23)"), + } +} diff --git a/src/cmd/policyauthorize.rs b/src/cmd/policyauthorize.rs new file mode 100644 index 0000000..5cf26a2 --- /dev/null +++ b/src/cmd/policyauthorize.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::{Digest, Name, Nonce, VerifiedTicket}; +use tss_esapi::tss2_esys::TPMT_TK_VERIFIED; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Approve a policy with an authorized signing key. +/// +/// Wraps TPM2_PolicyAuthorize. +#[derive(Parser)] +pub struct PolicyAuthorizeCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Approved policy digest file + #[arg(short = 'i', long = "input")] + pub input: PathBuf, + + /// Policy reference (nonce) file (optional) + #[arg(short = 'q', long = "qualification")] + pub qualification: Option, + + /// Signing key name file + #[arg(short = 'n', long = "name")] + pub name: PathBuf, + + /// Verification ticket file + #[arg(short = 't', long = "ticket")] + pub ticket: PathBuf, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyAuthorizeCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let approved_data = std::fs::read(&self.input) + .with_context(|| format!("reading approved policy from {}", self.input.display()))?; + let approved_policy = Digest::try_from(approved_data) + .map_err(|e| anyhow::anyhow!("invalid approved policy: {e}"))?; + + let policy_ref = match &self.qualification { + Some(path) => { + let data = std::fs::read(path) + .with_context(|| format!("reading policy ref from {}", path.display()))?; + Nonce::try_from(data).map_err(|e| anyhow::anyhow!("invalid policy ref: {e}"))? + } + None => Nonce::default(), + }; + + let name_data = std::fs::read(&self.name) + .with_context(|| format!("reading name from {}", self.name.display()))?; + let key_sign = + Name::try_from(name_data).map_err(|e| anyhow::anyhow!("invalid name: {e}"))?; + + let ticket_data = std::fs::read(&self.ticket) + .with_context(|| format!("reading ticket from {}", self.ticket.display()))?; + let check_ticket = if ticket_data.len() >= std::mem::size_of::() { + let tss_ticket: TPMT_TK_VERIFIED = + unsafe { std::ptr::read(ticket_data.as_ptr() as *const TPMT_TK_VERIFIED) }; + VerifiedTicket::try_from(tss_ticket) + .map_err(|e| anyhow::anyhow!("invalid ticket: {e}"))? + } else { + anyhow::bail!("ticket file too small"); + }; + + ctx.policy_authorize( + policy_session, + approved_policy, + policy_ref, + &key_sign, + check_ticket, + ) + .context("TPM2_PolicyAuthorize failed")?; + + info!("policy authorize succeeded"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policyauthorizenv.rs b/src/cmd/policyauthorizenv.rs new file mode 100644 index 0000000..819a55d --- /dev/null +++ b/src/cmd/policyauthorizenv.rs @@ -0,0 +1,74 @@ +use std::path::PathBuf; + +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Assert policy using a policy stored in an NV index. +/// +/// Wraps TPM2_PolicyAuthorizeNV (raw FFI). +#[derive(Parser)] +pub struct PolicyAuthorizeNvCmd { + /// Policy session context file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// NV index containing the policy (hex, e.g. 0x01000001) + #[arg(short = 'i', long = "nv-index")] + pub nv_index: String, + + /// Auth hierarchy for the NV index (o/p/e or nv) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Auth value for the hierarchy + #[arg(short = 'P', long = "auth")] + pub auth: Option, +} + +impl PolicyAuthorizeNvCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let session_handle = raw.context_load( + self.session + .to_str() + .ok_or_else(|| anyhow::anyhow!("invalid session path"))?, + )?; + + let nv_handle = raw.resolve_nv_index(&self.nv_index)?; + + let auth_handle = if self.hierarchy == "nv" { + nv_handle + } else { + RawEsysContext::resolve_hierarchy(&self.hierarchy)? + }; + + if let Some(ref auth_str) = self.auth { + let a = parse::parse_auth(auth_str)?; + raw.set_auth(auth_handle, a.value())?; + } + + unsafe { + let rc = Esys_PolicyAuthorizeNV( + raw.ptr(), + auth_handle, + nv_handle, + session_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + ); + if rc != 0 { + anyhow::bail!("Esys_PolicyAuthorizeNV failed: 0x{rc:08x}"); + } + } + + raw.context_save_to_file(session_handle, &self.session)?; + info!("policy authorize NV asserted"); + Ok(()) + } +} diff --git a/src/cmd/policyauthvalue.rs b/src/cmd/policyauthvalue.rs new file mode 100644 index 0000000..9189816 --- /dev/null +++ b/src/cmd/policyauthvalue.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Enable binding a policy to the authorization value of the authorized object. +/// +/// Wraps TPM2_PolicyAuthValue. +#[derive(Parser)] +pub struct PolicyAuthValueCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyAuthValueCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + ctx.policy_auth_value(policy_session) + .context("TPM2_PolicyAuthValue failed")?; + + info!("policy auth value set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + info!("policy digest saved to {}", path.display()); + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policycommandcode.rs b/src/cmd/policycommandcode.rs new file mode 100644 index 0000000..6c31af0 --- /dev/null +++ b/src/cmd/policycommandcode.rs @@ -0,0 +1,89 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::CommandCode; +use tss_esapi::constants::SessionType; +use tss_esapi::constants::tss::*; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Restrict policy to a specific TPM command. +/// +/// Wraps TPM2_PolicyCommandCode. +#[derive(Parser)] +pub struct PolicyCommandCodeCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Command code (hex value, e.g. 0x153 for TPM2_CC_Unseal) + #[arg()] + pub command_code: String, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyCommandCodeCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let code = parse_command_code(&self.command_code)?; + + ctx.policy_command_code(policy_session, code) + .context("TPM2_PolicyCommandCode failed")?; + + info!("policy command code set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + info!("policy digest saved to {}", path.display()); + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} + +fn parse_command_code(s: &str) -> anyhow::Result { + // Try hex value first. + let stripped = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s); + if let Ok(raw) = u32::from_str_radix(stripped, 16) { + return CommandCode::try_from(raw) + .map_err(|e| anyhow::anyhow!("invalid command code 0x{raw:08x}: {e}")); + } + + // Try known names. + match s.to_lowercase().as_str() { + "unseal" => CommandCode::try_from(TPM2_CC_Unseal), + "sign" => CommandCode::try_from(TPM2_CC_Sign), + "nv_read" | "nvread" => CommandCode::try_from(TPM2_CC_NV_Read), + "nv_write" | "nvwrite" => CommandCode::try_from(TPM2_CC_NV_Write), + "duplicate" => CommandCode::try_from(TPM2_CC_Duplicate), + "certify" => CommandCode::try_from(TPM2_CC_Certify), + "quote" => CommandCode::try_from(TPM2_CC_Quote), + "create" => CommandCode::try_from(TPM2_CC_Create), + _ => anyhow::bail!("unknown command code: {s}"), + } + .map_err(|e| anyhow::anyhow!("invalid command code: {e}")) +} diff --git a/src/cmd/policycountertimer.rs b/src/cmd/policycountertimer.rs new file mode 100644 index 0000000..598cee7 --- /dev/null +++ b/src/cmd/policycountertimer.rs @@ -0,0 +1,71 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Assert policy bound to the TPM clock/counter. +/// +/// Wraps TPM2_PolicyCounterTimer (raw FFI). +#[derive(Parser)] +pub struct PolicyCounterTimerCmd { + /// Policy session context file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Operand B (hex bytes for comparison) + #[arg(long = "operand-b")] + pub operand_b: String, + + /// Offset in the TPMS_TIME_INFO structure + #[arg(long = "offset", default_value = "0")] + pub offset: u16, + + /// Operation (eq, neq, sgt, ugt, slt, ult, sge, uge, sle, ule, bs, bc) + #[arg(long = "operation", default_value = "eq")] + pub operation: String, +} + +impl PolicyCounterTimerCmd { + #[allow(clippy::field_reassign_with_default)] + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let session_handle = raw.context_load( + self.session + .to_str() + .ok_or_else(|| anyhow::anyhow!("invalid session path"))?, + )?; + + let operand_bytes = hex::decode(&self.operand_b).context("invalid operand-b hex")?; + let mut operand = TPM2B_OPERAND::default(); + operand.size = operand_bytes.len() as u16; + operand.buffer[..operand_bytes.len()].copy_from_slice(&operand_bytes); + + let operation = parse::parse_tpm2_operation(&self.operation)?; + + unsafe { + let rc = Esys_PolicyCounterTimer( + raw.ptr(), + session_handle, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &operand, + self.offset, + operation, + ); + if rc != 0 { + bail!("Esys_PolicyCounterTimer failed: 0x{rc:08x}"); + } + } + + raw.context_save_to_file(session_handle, &self.session)?; + info!("policy counter/timer asserted"); + Ok(()) + } +} diff --git a/src/cmd/policycphash.rs b/src/cmd/policycphash.rs new file mode 100644 index 0000000..3e66138 --- /dev/null +++ b/src/cmd/policycphash.rs @@ -0,0 +1,63 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::Digest; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Bind a policy to specific command parameters. +/// +/// Wraps TPM2_PolicyCpHash. +#[derive(Parser)] +pub struct PolicyCpHashCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// cpHash file (binary digest of command parameters) + #[arg(long = "cphash")] + pub cphash: PathBuf, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyCpHashCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let data = std::fs::read(&self.cphash) + .with_context(|| format!("reading cpHash from {}", self.cphash.display()))?; + let cp_hash = Digest::try_from(data).map_err(|e| anyhow::anyhow!("invalid cpHash: {e}"))?; + + ctx.policy_cp_hash(policy_session, cp_hash) + .context("TPM2_PolicyCpHash failed")?; + + info!("policy cpHash set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policyduplicationselect.rs b/src/cmd/policyduplicationselect.rs new file mode 100644 index 0000000..cce1d0b --- /dev/null +++ b/src/cmd/policyduplicationselect.rs @@ -0,0 +1,82 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::Name; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Gate a policy on a specific duplication target parent. +/// +/// Wraps TPM2_PolicyDuplicationSelect. +#[derive(Parser)] +pub struct PolicyDuplicationSelectCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Object name file (name of object to be duplicated) + #[arg(short = 'n', long = "object-name")] + pub object_name: PathBuf, + + /// New parent name file + #[arg(short = 'N', long = "parent-name")] + pub parent_name: PathBuf, + + /// Include the object name in the policy hash + #[arg(long = "include-object", default_value = "false")] + pub include_object: bool, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyDuplicationSelectCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let obj_data = std::fs::read(&self.object_name) + .with_context(|| format!("reading object name from {}", self.object_name.display()))?; + let object_name = + Name::try_from(obj_data).map_err(|e| anyhow::anyhow!("invalid object name: {e}"))?; + + let parent_data = std::fs::read(&self.parent_name) + .with_context(|| format!("reading parent name from {}", self.parent_name.display()))?; + let new_parent_name = + Name::try_from(parent_data).map_err(|e| anyhow::anyhow!("invalid parent name: {e}"))?; + + ctx.policy_duplication_select( + policy_session, + object_name, + new_parent_name, + self.include_object, + ) + .context("TPM2_PolicyDuplicationSelect failed")?; + + info!("policy duplication select set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policylocality.rs b/src/cmd/policylocality.rs new file mode 100644 index 0000000..4756426 --- /dev/null +++ b/src/cmd/policylocality.rs @@ -0,0 +1,75 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::attributes::LocalityAttributes; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Gate a policy on the TPM locality. +/// +/// Wraps TPM2_PolicyLocality. +#[derive(Parser)] +pub struct PolicyLocalityCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Locality value (0-4, or bitmask as hex) + #[arg()] + pub locality: String, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyLocalityCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let locality = parse_locality(&self.locality)?; + + ctx.policy_locality(policy_session, locality) + .context("TPM2_PolicyLocality failed")?; + + info!("policy locality set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} + +fn parse_locality(s: &str) -> anyhow::Result { + let stripped = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s); + let val: u8 = if let Ok(v) = u8::from_str_radix(stripped, 16) { + v + } else { + s.parse() + .map_err(|_| anyhow::anyhow!("invalid locality: {s}"))? + }; + Ok(LocalityAttributes(val)) +} diff --git a/src/cmd/policynamehash.rs b/src/cmd/policynamehash.rs new file mode 100644 index 0000000..5cf9f5a --- /dev/null +++ b/src/cmd/policynamehash.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::Digest; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Bind a policy to specific object names. +/// +/// Wraps TPM2_PolicyNameHash. +#[derive(Parser)] +pub struct PolicyNameHashCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Name hash file (binary digest) + #[arg(long = "namehash")] + pub namehash: PathBuf, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyNameHashCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let data = std::fs::read(&self.namehash) + .with_context(|| format!("reading name hash from {}", self.namehash.display()))?; + let name_hash = + Digest::try_from(data).map_err(|e| anyhow::anyhow!("invalid name hash: {e}"))?; + + ctx.policy_name_hash(policy_session, name_hash) + .context("TPM2_PolicyNameHash failed")?; + + info!("policy name hash set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policynv.rs b/src/cmd/policynv.rs new file mode 100644 index 0000000..2d91c3a --- /dev/null +++ b/src/cmd/policynv.rs @@ -0,0 +1,98 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Assert policy bound to NV index contents. +/// +/// Wraps TPM2_PolicyNV (raw FFI). +#[derive(Parser)] +pub struct PolicyNvCmd { + /// Policy session context file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// NV index (hex, e.g. 0x01000001) + #[arg(short = 'i', long = "nv-index")] + pub nv_index: String, + + /// Auth hierarchy for NV (o/p/e or nv) + #[arg(short = 'C', long = "hierarchy", default_value = "o")] + pub hierarchy: String, + + /// Auth value for the hierarchy + #[arg(short = 'P', long = "auth")] + pub auth: Option, + + /// Operand B (hex bytes for comparison) + #[arg(long = "operand-b")] + pub operand_b: String, + + /// Offset within the NV data + #[arg(long = "offset", default_value = "0")] + pub offset: u16, + + /// Operation (eq, neq, sgt, ugt, slt, ult, sge, uge, sle, ule, bs, bc) + #[arg(long = "operation", default_value = "eq")] + pub operation: String, +} + +impl PolicyNvCmd { + #[allow(clippy::field_reassign_with_default)] + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let session_handle = raw.context_load( + self.session + .to_str() + .ok_or_else(|| anyhow::anyhow!("invalid session path"))?, + )?; + + let nv_handle = raw.resolve_nv_index(&self.nv_index)?; + + let auth_handle = if self.hierarchy == "nv" { + nv_handle + } else { + RawEsysContext::resolve_hierarchy(&self.hierarchy)? + }; + + if let Some(ref auth_str) = self.auth { + let a = parse::parse_auth(auth_str)?; + raw.set_auth(auth_handle, a.value())?; + } + + let operand_bytes = hex::decode(&self.operand_b).context("invalid operand-b hex")?; + let mut operand = TPM2B_OPERAND::default(); + operand.size = operand_bytes.len() as u16; + operand.buffer[..operand_bytes.len()].copy_from_slice(&operand_bytes); + + let operation = parse::parse_tpm2_operation(&self.operation)?; + + unsafe { + let rc = Esys_PolicyNV( + raw.ptr(), + auth_handle, + nv_handle, + session_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &operand, + self.offset, + operation, + ); + if rc != 0 { + bail!("Esys_PolicyNV failed: 0x{rc:08x}"); + } + } + + raw.context_save_to_file(session_handle, &self.session)?; + info!("policy NV asserted"); + Ok(()) + } +} diff --git a/src/cmd/policynvwritten.rs b/src/cmd/policynvwritten.rs new file mode 100644 index 0000000..c0ac550 --- /dev/null +++ b/src/cmd/policynvwritten.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Gate a policy on the NV written state. +/// +/// Wraps TPM2_PolicyNvWritten. +#[derive(Parser)] +pub struct PolicyNvWrittenCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Written set (true = NV must have been written, false = must not) + #[arg(short = 's', long = "written-set", default_value = "true")] + pub written_set: bool, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyNvWrittenCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + ctx.policy_nv_written(policy_session, self.written_set) + .context("TPM2_PolicyNvWritten failed")?; + + info!("policy NV written set (written={})", self.written_set); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policyor.rs b/src/cmd/policyor.rs new file mode 100644 index 0000000..0ebd893 --- /dev/null +++ b/src/cmd/policyor.rs @@ -0,0 +1,75 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::{Digest, DigestList}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Compound multiple policies with logical OR. +/// +/// Wraps TPM2_PolicyOR. +#[derive(Parser)] +pub struct PolicyOrCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Policy digest files to OR together (at least 2) + #[arg(short = 'l', long = "policy-list", num_args = 2..)] + pub policy_list: Vec, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyOrCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + if self.policy_list.len() < 2 { + bail!("at least 2 policy digests required for OR"); + } + + let mut digest_list = DigestList::new(); + for path in &self.policy_list { + let data = std::fs::read(path) + .with_context(|| format!("reading policy digest from {}", path.display()))?; + let digest = Digest::try_from(data) + .map_err(|e| anyhow::anyhow!("invalid digest from {}: {e}", path.display()))?; + digest_list + .add(digest) + .map_err(|e| anyhow::anyhow!("failed to add digest: {e}"))?; + } + + ctx.policy_or(policy_session, digest_list) + .context("TPM2_PolicyOR failed")?; + + info!("policy OR set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + info!("policy digest saved to {}", path.display()); + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policypassword.rs b/src/cmd/policypassword.rs new file mode 100644 index 0000000..1ea621e --- /dev/null +++ b/src/cmd/policypassword.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Enable binding a policy to the plaintext password of the authorized entity. +/// +/// Wraps TPM2_PolicyPassword. +#[derive(Parser)] +pub struct PolicyPasswordCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyPasswordCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + ctx.policy_password(policy_session) + .context("TPM2_PolicyPassword failed")?; + + info!("policy password set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + info!("policy digest saved to {}", path.display()); + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policypcr.rs b/src/cmd/policypcr.rs new file mode 100644 index 0000000..6aa75ba --- /dev/null +++ b/src/cmd/policypcr.rs @@ -0,0 +1,76 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::Digest; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse; +use crate::session::load_session_from_file; + +/// Gate a policy on the current PCR values. +/// +/// Wraps TPM2_PolicyPCR. +#[derive(Parser)] +pub struct PolicyPcrCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// PCR selection (e.g. sha256:0,1,2) + #[arg(short = 'l', long = "pcr-list")] + pub pcr_list: String, + + /// Expected PCR digest (hex). If empty, uses current PCR values. + #[arg(short = 'f', long = "pcr-digest")] + pub pcr_digest: Option, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyPcrCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let pcr_selection = parse::parse_pcr_selection(&self.pcr_list)?; + + let pcr_digest = match &self.pcr_digest { + Some(hex_str) => { + let bytes = hex::decode(hex_str) + .map_err(|e| anyhow::anyhow!("invalid PCR digest hex: {e}"))?; + Digest::try_from(bytes).map_err(|e| anyhow::anyhow!("invalid PCR digest: {e}"))? + } + None => Digest::default(), + }; + + ctx.policy_pcr(policy_session, pcr_digest, pcr_selection) + .context("TPM2_PolicyPCR failed")?; + + info!("policy PCR set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + info!("policy digest saved to {}", path.display()); + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policyrestart.rs b/src/cmd/policyrestart.rs new file mode 100644 index 0000000..cacfe2e --- /dev/null +++ b/src/cmd/policyrestart.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Restart a policy session, clearing its policy digest. +/// +/// Wraps TPM2_PolicyRestart. +#[derive(Parser)] +pub struct PolicyRestartCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, +} + +impl PolicyRestartCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + ctx.policy_restart(policy_session) + .context("TPM2_PolicyRestart failed")?; + + info!("policy session restarted"); + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policysecret.rs b/src/cmd/policysecret.rs new file mode 100644 index 0000000..e94cead --- /dev/null +++ b/src/cmd/policysecret.rs @@ -0,0 +1,136 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{AuthHandle, ObjectHandle, SessionHandle}; +use tss_esapi::interface_types::session_handles::AuthSession; + +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::load_session_from_file; + +/// Couple a policy to the authorization of another object. +/// +/// Extends the policy session with TPM2_PolicySecret, binding it to +/// the authorization of the object specified by `-c`. +#[derive(Parser)] +pub struct PolicySecretCmd { + /// 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, e/endorsement, p/platform, l/lockout) + #[arg(long = "object-hierarchy", conflicts_with_all = ["object_context", "object_context_handle"])] + pub object_context_hierarchy: Option, + + /// Policy session file (from tpm2 startauthsession) + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, + + /// Authorization value for the object + #[arg(short = 'p', long = "auth")] + pub auth: Option, +} + +impl PolicySecretCmd { + 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" + ), + } + } + + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + // Load the policy session. + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + // Resolve the auth entity. Accept hierarchy shorthands first, + // then fall back to loading a generic object handle. + let auth_handle = match &self.object_context_hierarchy { + Some(h) => match h.to_lowercase().as_str() { + "o" | "owner" => AuthHandle::Owner, + "e" | "endorsement" => AuthHandle::Endorsement, + "p" | "platform" => AuthHandle::Platform, + "l" | "lockout" => AuthHandle::Lockout, + _ => anyhow::bail!("unknown hierarchy shorthand: {h}"), + }, + None => { + let obj = load_object_from_source(&mut ctx, &self.object_context_source()?)?; + // Set the auth value on the object if provided. + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(obj, auth).context("tr_set_auth failed")?; + } + AuthHandle::from(obj) + } + }; + + // For hierarchy handles, set auth directly if supplied. + if let Some(ref auth_str) = self.auth { + match auth_handle { + AuthHandle::Owner + | AuthHandle::Endorsement + | AuthHandle::Platform + | AuthHandle::Lockout => { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(auth_handle.into(), auth) + .context("tr_set_auth failed")?; + } + _ => {} // already handled above + } + } + + // Execute PolicySecret with a password session for the auth entity. + ctx.set_sessions((Some(AuthSession::Password), None, None)); + let (_timeout, _ticket) = ctx + .policy_secret( + policy_session, + auth_handle, + Default::default(), // nonce_tpm + Default::default(), // cp_hash_a + Default::default(), // policy_ref + None, // expiration + ) + .context("TPM2_PolicySecret failed")?; + ctx.clear_sessions(); + + info!("policy secret satisfied"); + + // Optionally save the policy digest. + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + info!("policy digest saved to {}", path.display()); + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + info!("session saved to {}", self.session.display()); + Ok(()) + } +} diff --git a/src/cmd/policysigned.rs b/src/cmd/policysigned.rs new file mode 100644 index 0000000..41795cb --- /dev/null +++ b/src/cmd/policysigned.rs @@ -0,0 +1,162 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::{Digest, Nonce, Signature}; +use tss_esapi::traits::UnMarshall; +use tss_esapi::tss2_esys::TPMT_TK_AUTH; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_object_from_source}; +use crate::parse::parse_hex_u32; +use crate::session::load_session_from_file; + +/// Authorize a policy with a signed authorization. +/// +/// Wraps TPM2_PolicySigned. +#[derive(Parser)] +pub struct PolicySignedCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Signing key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// Signing key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Signature file (marshaled TPMT_SIGNATURE) + #[arg(short = 's', long = "signature")] + pub signature: PathBuf, + + /// Expiration time in seconds (0 = no expiration) + #[arg(short = 'x', long = "expiration", default_value = "0")] + pub expiration: i32, + + /// cpHash file (optional) + #[arg(long = "cphash-input")] + pub cphash_input: Option, + + /// Policy reference / nonce file (optional) + #[arg(short = 'q', long = "qualification")] + pub qualification: Option, + + /// Output file for the timeout + #[arg(short = 't', long = "timeout")] + pub timeout_out: Option, + + /// Output file for the policy ticket + #[arg(long = "ticket")] + pub ticket_out: Option, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicySignedCmd { + fn key_context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-handle must be provided" + ), + } + } + + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let auth_object = load_object_from_source(&mut ctx, &self.key_context_source()?)?; + + let sig_data = std::fs::read(&self.signature) + .with_context(|| format!("reading signature from {}", self.signature.display()))?; + let signature = Signature::unmarshall(&sig_data) + .map_err(|e| anyhow::anyhow!("invalid signature: {e}"))?; + + let cp_hash = match &self.cphash_input { + Some(path) => { + let data = std::fs::read(path)?; + Digest::try_from(data).map_err(|e| anyhow::anyhow!("invalid cpHash: {e}"))? + } + None => Digest::default(), + }; + + let policy_ref = match &self.qualification { + Some(path) => { + let data = std::fs::read(path)?; + Nonce::try_from(data).map_err(|e| anyhow::anyhow!("invalid policy ref: {e}"))? + } + None => Nonce::default(), + }; + + let expiration = if self.expiration == 0 { + None + } else { + Some(std::time::Duration::from_secs(self.expiration as u64)) + }; + + let (timeout, ticket) = ctx + .policy_signed( + policy_session, + auth_object, + Nonce::default(), // nonce_tpm + cp_hash, + policy_ref, + expiration, + signature, + ) + .context("TPM2_PolicySigned failed")?; + + info!("policy signed succeeded"); + + if let Some(ref path) = self.timeout_out { + std::fs::write(path, timeout.value()) + .with_context(|| format!("writing timeout to {}", path.display()))?; + } + + if let Some(ref path) = self.ticket_out { + let tss_ticket: TPMT_TK_AUTH = ticket + .try_into() + .map_err(|e| anyhow::anyhow!("failed to convert ticket: {e:?}"))?; + let bytes = unsafe { + std::slice::from_raw_parts( + &tss_ticket as *const TPMT_TK_AUTH as *const u8, + std::mem::size_of::(), + ) + }; + std::fs::write(path, bytes) + .with_context(|| format!("writing ticket to {}", path.display()))?; + } + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policytemplate.rs b/src/cmd/policytemplate.rs new file mode 100644 index 0000000..1ba893c --- /dev/null +++ b/src/cmd/policytemplate.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::structures::Digest; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +/// Bind a policy to a specific object creation template. +/// +/// Wraps TPM2_PolicyTemplate. +#[derive(Parser)] +pub struct PolicyTemplateCmd { + /// Policy session file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Template hash file (binary digest) + #[arg(long = "template-hash")] + pub template_hash: PathBuf, + + /// Output file for the policy digest + #[arg(short = 'L', long = "policy")] + pub policy: Option, +} + +impl PolicyTemplateCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Policy)?; + let policy_session = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected a policy session"))?; + + let data = std::fs::read(&self.template_hash).with_context(|| { + format!( + "reading template hash from {}", + self.template_hash.display() + ) + })?; + let template_hash = + Digest::try_from(data).map_err(|e| anyhow::anyhow!("invalid template hash: {e}"))?; + + ctx.policy_template(policy_session, template_hash) + .context("TPM2_PolicyTemplate failed")?; + + info!("policy template set"); + + if let Some(ref path) = self.policy { + let digest = ctx + .policy_get_digest(policy_session) + .context("TPM2_PolicyGetDigest failed")?; + std::fs::write(path, digest.value()) + .with_context(|| format!("writing policy digest to {}", path.display()))?; + } + + let handle: ObjectHandle = SessionHandle::from(policy_session).into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/policyticket.rs b/src/cmd/policyticket.rs new file mode 100644 index 0000000..6026f41 --- /dev/null +++ b/src/cmd/policyticket.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::raw_esys::RawEsysContext; + +/// Include a policy ticket to satisfy a prior policy assertion. +/// +/// Wraps TPM2_PolicyTicket (raw FFI). +#[derive(Parser)] +pub struct PolicyTicketCmd { + /// Policy session context file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Timeout value (hex bytes) + #[arg(long = "timeout")] + pub timeout: Option, + + /// cpHash for the command being authorized (hex) + #[arg(long = "cphash")] + pub cphash: Option, + + /// Policy reference (hex) + #[arg(long = "policy-ref")] + pub policy_ref: Option, + + /// Key name (hex) + #[arg(short = 'n', long = "name")] + pub name: String, + + /// Ticket file (binary) + #[arg(short = 't', long = "ticket")] + pub ticket: PathBuf, +} + +impl PolicyTicketCmd { + #[allow(clippy::field_reassign_with_default)] + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let session_handle = raw.context_load( + self.session + .to_str() + .ok_or_else(|| anyhow::anyhow!("invalid session path"))?, + )?; + + let mut timeout = TPM2B_TIMEOUT::default(); + if let Some(ref t) = self.timeout { + let bytes = hex::decode(t).context("invalid timeout hex")?; + timeout.size = bytes.len() as u16; + timeout.buffer[..bytes.len()].copy_from_slice(&bytes); + } + + let mut cp_hash_a = TPM2B_DIGEST::default(); + if let Some(ref h) = self.cphash { + let bytes = hex::decode(h).context("invalid cphash hex")?; + cp_hash_a.size = bytes.len() as u16; + cp_hash_a.buffer[..bytes.len()].copy_from_slice(&bytes); + } + + let mut policy_ref = TPM2B_NONCE::default(); + if let Some(ref r) = self.policy_ref { + let bytes = hex::decode(r).context("invalid policy-ref hex")?; + policy_ref.size = bytes.len() as u16; + policy_ref.buffer[..bytes.len()].copy_from_slice(&bytes); + } + + let name_bytes = hex::decode(&self.name).context("invalid name hex")?; + let mut auth_name = TPM2B_NAME::default(); + auth_name.size = name_bytes.len() as u16; + auth_name.name[..name_bytes.len()].copy_from_slice(&name_bytes); + + // Read ticket from file + let ticket_data = std::fs::read(&self.ticket) + .with_context(|| format!("reading ticket from {}", self.ticket.display()))?; + if ticket_data.len() < std::mem::size_of::() { + anyhow::bail!("ticket file too small"); + } + let ticket: TPMT_TK_AUTH = + unsafe { std::ptr::read(ticket_data.as_ptr() as *const TPMT_TK_AUTH) }; + + unsafe { + let rc = Esys_PolicyTicket( + raw.ptr(), + session_handle, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &timeout, + &cp_hash_a, + &policy_ref, + &auth_name, + &ticket, + ); + if rc != 0 { + anyhow::bail!("Esys_PolicyTicket failed: 0x{rc:08x}"); + } + } + + raw.context_save_to_file(session_handle, &self.session)?; + info!("policy ticket asserted"); + Ok(()) + } +} diff --git a/src/cmd/print.rs b/src/cmd/print.rs new file mode 100644 index 0000000..a9a61fd --- /dev/null +++ b/src/cmd/print.rs @@ -0,0 +1,201 @@ +use std::io::Read; +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use clap::Parser; +use tss_esapi::structures::{Attest, AttestInfo, Public, PublicBuffer}; +use tss_esapi::traits::UnMarshall; +use tss_esapi::utils::TpmsContext; + +/// Decode and display a TPM data structure. +/// +/// Reads a binary TPM structure from a file (or stdin) and prints a +/// human-readable representation to stdout. +#[derive(Parser)] +pub struct PrintCmd { + /// Structure type to decode + #[arg(short = 't', long = "type")] + pub structure_type: String, + + /// Input file (default: stdin) + #[arg()] + pub input: Option, +} + +impl PrintCmd { + pub fn execute(&self, _global: &crate::cli::GlobalOpts) -> anyhow::Result<()> { + let data = read_input(&self.input)?; + + match self.structure_type.to_lowercase().as_str() { + "tpms_attest" => print_attest(&data)?, + "tpms_context" => print_context(&data)?, + "tpm2b_public" => print_tpm2b_public(&data)?, + "tpmt_public" => print_tpmt_public(&data)?, + other => bail!( + "unsupported type: {other}\n\ + supported types: TPMS_ATTEST, TPMS_CONTEXT, TPM2B_PUBLIC, TPMT_PUBLIC" + ), + } + + Ok(()) + } +} + +fn read_input(path: &Option) -> anyhow::Result> { + match path { + Some(p) => std::fs::read(p).with_context(|| format!("reading input: {}", p.display())), + None => { + let mut buf = Vec::new(); + std::io::stdin() + .read_to_end(&mut buf) + .context("reading stdin")?; + Ok(buf) + } + } +} + +fn print_attest(data: &[u8]) -> anyhow::Result<()> { + let attest = Attest::unmarshall(data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal TPMS_ATTEST: {e}"))?; + + println!("type: {:?}", attest.attestation_type()); + println!( + "qualified_signer: {}", + hex::encode(attest.qualified_signer().value()) + ); + println!("extra_data: {}", hex::encode(attest.extra_data().value())); + println!("clock_info:"); + let ci = attest.clock_info(); + println!(" clock: {}", ci.clock()); + println!(" reset_count: {}", ci.reset_count()); + println!(" restart_count: {}", ci.restart_count()); + println!(" safe: {}", if ci.safe() { "yes" } else { "no" }); + println!("firmware_version: 0x{:016x}", attest.firmware_version()); + + match attest.attested() { + AttestInfo::Quote { info } => { + println!("attested:"); + println!(" type: quote"); + println!(" pcr_digest: {}", hex::encode(info.pcr_digest().value())); + println!(" pcr_selection: {:?}", info.pcr_selection()); + } + AttestInfo::Certify { info } => { + println!("attested:"); + println!(" type: certify"); + println!(" name: {}", hex::encode(info.name().value())); + println!( + " qualified_name: {}", + hex::encode(info.qualified_name().value()) + ); + } + AttestInfo::Creation { info } => { + println!("attested:"); + println!(" type: creation"); + println!(" object_name: {}", hex::encode(info.object_name().value())); + println!( + " creation_hash: {}", + hex::encode(info.creation_hash().value()) + ); + } + AttestInfo::Time { info } => { + println!("attested:"); + println!(" type: time"); + let ti = info.time_info(); + println!(" time: {}", ti.time()); + println!(" clock: {}", ti.clock_info().clock()); + println!(" firmware_version: 0x{:016x}", info.firmware_version()); + } + other => { + println!("attested:"); + println!(" type: {:?}", std::mem::discriminant(other)); + } + } + + Ok(()) +} + +fn print_context(data: &[u8]) -> anyhow::Result<()> { + let ctx: TpmsContext = + serde_json::from_slice(data).context("failed to deserialize TPMS_CONTEXT")?; + println!("{ctx:#?}"); + Ok(()) +} + +fn print_tpm2b_public(data: &[u8]) -> anyhow::Result<()> { + let buf = PublicBuffer::unmarshall(data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal TPM2B_PUBLIC: {e}"))?; + let public: Public = buf + .try_into() + .map_err(|e: tss_esapi::Error| anyhow::anyhow!("failed to decode Public: {e}"))?; + print_public(&public); + Ok(()) +} + +fn print_tpmt_public(data: &[u8]) -> anyhow::Result<()> { + let public = Public::unmarshall(data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal TPMT_PUBLIC: {e}"))?; + print_public(&public); + Ok(()) +} + +fn print_public(public: &Public) { + match public { + Public::Rsa { + object_attributes, + name_hashing_algorithm, + auth_policy, + parameters, + unique, + } => { + println!("type: rsa"); + println!("name_hash_algorithm: {name_hashing_algorithm:?}"); + println!("object_attributes: {object_attributes:?}"); + println!("auth_policy: {}", hex::encode(auth_policy.value())); + println!("parameters: {parameters:?}"); + println!("modulus: {}", hex::encode(unique.value())); + } + Public::Ecc { + object_attributes, + name_hashing_algorithm, + auth_policy, + parameters, + unique, + } => { + println!("type: ecc"); + println!("name_hash_algorithm: {name_hashing_algorithm:?}"); + println!("object_attributes: {object_attributes:?}"); + println!("auth_policy: {}", hex::encode(auth_policy.value())); + println!("parameters: {parameters:?}"); + println!("x: {}", hex::encode(unique.x().value())); + println!("y: {}", hex::encode(unique.y().value())); + } + Public::KeyedHash { + object_attributes, + name_hashing_algorithm, + auth_policy, + parameters, + unique, + } => { + println!("type: keyedhash"); + println!("name_hash_algorithm: {name_hashing_algorithm:?}"); + println!("object_attributes: {object_attributes:?}"); + println!("auth_policy: {}", hex::encode(auth_policy.value())); + println!("parameters: {parameters:?}"); + println!("unique: {}", hex::encode(unique.value())); + } + Public::SymCipher { + object_attributes, + name_hashing_algorithm, + auth_policy, + parameters, + unique, + } => { + println!("type: symcipher"); + println!("name_hash_algorithm: {name_hashing_algorithm:?}"); + println!("object_attributes: {object_attributes:?}"); + println!("auth_policy: {}", hex::encode(auth_policy.value())); + println!("parameters: {parameters:?}"); + println!("unique: {}", hex::encode(unique.value())); + } + } +} diff --git a/src/cmd/quote.rs b/src/cmd/quote.rs new file mode 100644 index 0000000..547a7de --- /dev/null +++ b/src/cmd/quote.rs @@ -0,0 +1,139 @@ +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}; +use crate::parse::{self, parse_hex_u32}; +use crate::pcr; +use crate::session::execute_with_optional_session; + +/// Generate a TPM quote over selected PCRs. +#[derive(Parser)] +pub struct QuoteCmd { + /// 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, + + /// PCR selection list (e.g. sha256:0,1,2+sha1:all) + #[arg(short = 'l', long = "pcr-list")] + pub pcr_list: String, + + /// 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 quote message (TPMS_ATTEST, marshaled binary) + #[arg(short = 'm', long = "message")] + pub message: Option, + + /// Output file for the signature (TPMT_SIGNATURE, marshaled binary) + #[arg(short = 's', long = "signature")] + pub signature: Option, + + /// Output file for PCR digest values (raw binary) + #[arg(short = 'o', long = "pcr")] + pub pcr_output: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl QuoteCmd { + 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 mut ctx = create_context(global.tcti.as_deref())?; + + let key_handle = load_key_from_source(&mut ctx, &self.context_source()?)?; + let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?; + let scheme = parse::parse_signature_scheme(&self.scheme, hash_alg)?; + let pcr_selection = parse::parse_pcr_selection(&self.pcr_list)?; + + let qualifying_data = if let Some(ref q) = self.qualification { + let bytes = + parse::parse_qualification_hex(q).context("failed to parse qualification data")?; + Data::try_from(bytes).map_err(|e| anyhow::anyhow!("qualifying data: {e}"))? + } else if let Some(ref path) = self.qualification_file { + let bytes = parse::parse_qualification_file(path) + .context("failed to read qualification file")?; + Data::try_from(bytes).map_err(|e| anyhow::anyhow!("qualifying data: {e}"))? + } else { + Data::default() + }; + + let session_path = self.session.as_deref(); + let (attest, signature) = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.quote( + key_handle, + qualifying_data.clone(), + scheme, + pcr_selection.clone(), + ) + }) + .context("TPM2_Quote failed")?; + + if let Some(ref path) = self.message { + let msg_bytes = attest.marshall().context("failed to marshal TPMS_ATTEST")?; + std::fs::write(path, &msg_bytes) + .with_context(|| format!("writing message to {}", path.display()))?; + info!("message saved to {}", path.display()); + } + + if let Some(ref path) = self.signature { + let sig_bytes = signature + .marshall() + .context("failed to marshal TPMT_SIGNATURE")?; + std::fs::write(path, &sig_bytes) + .with_context(|| format!("writing signature to {}", path.display()))?; + info!("signature saved to {}", path.display()); + } + + if let Some(ref path) = self.pcr_output { + let chunks = pcr::pcr_read_all(&mut ctx, pcr_selection)?; + let mut raw = Vec::new(); + for (_, digests) in &chunks { + for digest in digests.value() { + raw.extend_from_slice(digest.value()); + } + } + std::fs::write(path, &raw) + .with_context(|| format!("writing PCR data to {}", path.display()))?; + info!("PCR data saved to {}", path.display()); + } + + Ok(()) + } +} diff --git a/src/cmd/rcdecode.rs b/src/cmd/rcdecode.rs new file mode 100644 index 0000000..dd1a841 --- /dev/null +++ b/src/cmd/rcdecode.rs @@ -0,0 +1,142 @@ +use clap::Parser; + +use crate::cli::GlobalOpts; + +/// Decode a TPM2 response code into human-readable text. +/// +/// This is a client-side utility that does not contact the TPM. +#[derive(Parser)] +pub struct RcDecodeCmd { + /// Response code (hex, e.g. 0x100) + #[arg()] + pub rc: String, +} + +impl RcDecodeCmd { + pub fn execute(&self, _global: &GlobalOpts) -> anyhow::Result<()> { + let stripped = self + .rc + .strip_prefix("0x") + .or_else(|| self.rc.strip_prefix("0X")) + .unwrap_or(&self.rc); + let code: u32 = u32::from_str_radix(stripped, 16) + .map_err(|_| anyhow::anyhow!("invalid response code: {}", self.rc))?; + + println!("0x{code:08X}:"); + + // Decode the error format + let fmt1 = (code & 0x80) != 0; + + if code == 0 { + println!(" TPM_RC_SUCCESS"); + return Ok(()); + } + + // Check for format 1 (parameter/session/handle errors) + if fmt1 { + let error_number = code & 0x3F; + let parameter = (code >> 8) & 0xF; + let session_handle = (code >> 8) & 0x7; + let is_parameter = (code & 0x40) != 0; + + let error_name = decode_fmt1_error(error_number); + + if is_parameter { + println!(" format 1 error"); + println!(" error: {error_name} (0x{error_number:03x})"); + println!(" parameter: {parameter}"); + } else { + println!(" format 1 error"); + println!(" error: {error_name} (0x{error_number:03x})"); + println!(" session/handle: {session_handle}"); + } + } else { + let error_number = code & 0x7F; + let error_name = decode_fmt0_error(error_number); + println!(" format 0 error"); + println!(" error: {error_name} (0x{error_number:03x})"); + } + + Ok(()) + } +} + +fn decode_fmt1_error(n: u32) -> &'static str { + match n { + 0x01 => "TPM_RC_ASYMMETRIC", + 0x02 => "TPM_RC_ATTRIBUTES", + 0x03 => "TPM_RC_HASH", + 0x04 => "TPM_RC_VALUE", + 0x05 => "TPM_RC_HIERARCHY", + 0x07 => "TPM_RC_KEY_SIZE", + 0x08 => "TPM_RC_MGF", + 0x09 => "TPM_RC_MODE", + 0x0A => "TPM_RC_TYPE", + 0x0B => "TPM_RC_HANDLE", + 0x0C => "TPM_RC_KDF", + 0x0D => "TPM_RC_RANGE", + 0x0E => "TPM_RC_AUTH_FAIL", + 0x0F => "TPM_RC_NONCE", + 0x10 => "TPM_RC_PP", + 0x12 => "TPM_RC_SCHEME", + 0x15 => "TPM_RC_SIZE", + 0x16 => "TPM_RC_SYMMETRIC", + 0x17 => "TPM_RC_TAG", + 0x18 => "TPM_RC_SELECTOR", + 0x1A => "TPM_RC_INSUFFICIENT", + 0x1B => "TPM_RC_SIGNATURE", + 0x1C => "TPM_RC_KEY", + 0x1D => "TPM_RC_POLICY_FAIL", + 0x1F => "TPM_RC_INTEGRITY", + 0x20 => "TPM_RC_TICKET", + 0x21 => "TPM_RC_RESERVED_BITS", + 0x22 => "TPM_RC_BAD_AUTH", + 0x23 => "TPM_RC_EXPIRED", + 0x24 => "TPM_RC_POLICY_CC", + 0x25 => "TPM_RC_BINDING", + 0x26 => "TPM_RC_CURVE", + 0x27 => "TPM_RC_ECC_POINT", + _ => "UNKNOWN", + } +} + +fn decode_fmt0_error(n: u32) -> &'static str { + match n { + 0x00 => "TPM_RC_SUCCESS", + 0x01 => "TPM_RC_INITIALIZE", + 0x03 => "TPM_RC_FAILURE", + 0x0B => "TPM_RC_SEQUENCE", + 0x19 => "TPM_RC_PRIVATE", + 0x20 => "TPM_RC_HMAC", + 0x23 => "TPM_RC_DISABLED", + 0x24 => "TPM_RC_EXCLUSIVE", + 0x25 => "TPM_RC_AUTH_TYPE", + 0x26 => "TPM_RC_AUTH_MISSING", + 0x27 => "TPM_RC_POLICY", + 0x28 => "TPM_RC_PCR", + 0x29 => "TPM_RC_PCR_CHANGED", + 0x2D => "TPM_RC_UPGRADE", + 0x2E => "TPM_RC_TOO_MANY_CONTEXTS", + 0x2F => "TPM_RC_AUTH_UNAVAILABLE", + 0x30 => "TPM_RC_REBOOT", + 0x31 => "TPM_RC_UNBALANCED", + 0x42 => "TPM_RC_COMMAND_SIZE", + 0x43 => "TPM_RC_COMMAND_CODE", + 0x44 => "TPM_RC_AUTHSIZE", + 0x45 => "TPM_RC_AUTH_CONTEXT", + 0x46 => "TPM_RC_NV_RANGE", + 0x47 => "TPM_RC_NV_SIZE", + 0x48 => "TPM_RC_NV_LOCKED", + 0x49 => "TPM_RC_NV_AUTHORIZATION", + 0x4A => "TPM_RC_NV_UNINITIALIZED", + 0x4B => "TPM_RC_NV_SPACE", + 0x4C => "TPM_RC_NV_DEFINED", + 0x50 => "TPM_RC_BAD_CONTEXT", + 0x51 => "TPM_RC_CPHASH", + 0x52 => "TPM_RC_PARENT", + 0x53 => "TPM_RC_NEEDS_TEST", + 0x54 => "TPM_RC_NO_RESULT", + 0x55 => "TPM_RC_SENSITIVE", + _ => "UNKNOWN", + } +} diff --git a/src/cmd/readclock.rs b/src/cmd/readclock.rs new file mode 100644 index 0000000..a156d2a --- /dev/null +++ b/src/cmd/readclock.rs @@ -0,0 +1,50 @@ +use clap::Parser; +use serde_json::json; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::raw_esys::RawEsysContext; + +/// Read the current TPM clock and time values. +/// +/// Wraps TPM2_ReadClock (raw FFI). +#[derive(Parser)] +pub struct ReadClockCmd {} + +impl ReadClockCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + + unsafe { + let mut current_time: *mut TPMS_TIME_INFO = std::ptr::null_mut(); + + let rc = Esys_ReadClock( + raw.ptr(), + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &mut current_time, + ); + if rc != 0 { + anyhow::bail!("Esys_ReadClock failed: 0x{rc:08x}"); + } + + let t = &*current_time; + let output = json!({ + "time": t.time, + "clock_info": { + "clock": t.clockInfo.clock, + "reset_count": t.clockInfo.resetCount, + "restart_count": t.clockInfo.restartCount, + "safe": t.clockInfo.safe == 1, + } + }); + + Esys_Free(current_time as *mut _); + + println!("{}", serde_json::to_string_pretty(&output)?); + } + + Ok(()) + } +} diff --git a/src/cmd/readpublic.rs b/src/cmd/readpublic.rs new file mode 100644 index 0000000..4b3693d --- /dev/null +++ b/src/cmd/readpublic.rs @@ -0,0 +1,58 @@ +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_key_from_source}; +use crate::output; +use crate::parse::parse_hex_u32; + +/// Read the public area of a loaded object. +#[derive(Parser)] +pub struct ReadPublicCmd { + /// Object context file path + #[arg(short = 'c', long = "context", conflicts_with = "context_handle")] + pub context: Option, + + /// Object handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "context-handle", value_parser = parse_hex_u32, conflicts_with = "context")] + pub context_handle: Option, + + /// Output file for the public area (binary) + #[arg(short = 'o', long)] + pub output: Option, +} + +impl ReadPublicCmd { + 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 mut ctx = create_context(global.tcti.as_deref())?; + + let key_handle = load_key_from_source(&mut ctx, &self.context_source()?)?; + + let (public, name, qualified_name) = ctx + .execute_without_session(|ctx| ctx.read_public(key_handle)) + .context("TPM2_ReadPublic failed")?; + + println!("name: 0x{}", hex::encode(name.value())); + println!("qualified name: 0x{}", hex::encode(qualified_name.value())); + println!("{public:#?}"); + + if let Some(ref path) = self.output { + output::write_to_file(path, format!("{public:?}").as_bytes())?; + info!("public area saved to {}", path.display()); + } + + Ok(()) + } +} diff --git a/src/cmd/rsadecrypt.rs b/src/cmd/rsadecrypt.rs new file mode 100644 index 0000000..a673b85 --- /dev/null +++ b/src/cmd/rsadecrypt.rs @@ -0,0 +1,123 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{Data, PublicKeyRsa, RsaDecryptionScheme}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_key_from_source}; +use crate::output; +use crate::parse; +use crate::parse::parse_hex_u32; +use crate::session::execute_with_optional_session; + +/// Perform RSA decryption using a TPM-loaded key. +/// +/// Wraps TPM2_RSA_Decrypt. +#[derive(Parser)] +pub struct RsaDecryptCmd { + /// RSA key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// RSA key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Auth value for the key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Decryption scheme (rsaes, oaep, null) + #[arg(short = 's', long = "scheme", default_value = "rsaes")] + pub scheme: String, + + /// Hash algorithm for OAEP (default: sha256) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Label for OAEP (optional) + #[arg(short = 'l', long = "label")] + pub label: Option, + + /// Input file (ciphertext) + #[arg(short = 'i', long = "input")] + pub input: PathBuf, + + /// Output file (plaintext) + #[arg(short = 'o', long = "output")] + pub output: PathBuf, + + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl RsaDecryptCmd { + fn key_context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-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.key_context_source()?)?; + + let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?; + let scheme = parse_rsa_scheme(&self.scheme, hash_alg)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(key_handle.into(), auth) + .context("tr_set_auth failed")?; + } + + let ciphertext_data = std::fs::read(&self.input) + .with_context(|| format!("reading input from {}", self.input.display()))?; + let cipher_text = PublicKeyRsa::try_from(ciphertext_data) + .map_err(|e| anyhow::anyhow!("invalid ciphertext: {e}"))?; + + let label_data = match &self.label { + Some(l) => { + let bytes = l.as_bytes().to_vec(); + Data::try_from(bytes).map_err(|e| anyhow::anyhow!("label too large: {e}"))? + } + None => Data::default(), + }; + + let session_path = self.session.as_deref(); + let plaintext = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.rsa_decrypt(key_handle, cipher_text.clone(), scheme, label_data.clone()) + }) + .context("TPM2_RSA_Decrypt failed")?; + + output::write_to_file(&self.output, plaintext.value())?; + info!("plaintext saved to {}", self.output.display()); + Ok(()) + } +} + +fn parse_rsa_scheme( + s: &str, + hash_alg: tss_esapi::interface_types::algorithm::HashingAlgorithm, +) -> anyhow::Result { + match s.to_lowercase().as_str() { + "rsaes" => Ok(RsaDecryptionScheme::RsaEs), + "oaep" => Ok(RsaDecryptionScheme::Oaep( + tss_esapi::structures::HashScheme::new(hash_alg), + )), + "null" => Ok(RsaDecryptionScheme::Null), + _ => anyhow::bail!("unsupported RSA scheme: {s}"), + } +} diff --git a/src/cmd/rsaencrypt.rs b/src/cmd/rsaencrypt.rs new file mode 100644 index 0000000..be18449 --- /dev/null +++ b/src/cmd/rsaencrypt.rs @@ -0,0 +1,105 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::{Data, PublicKeyRsa, RsaDecryptionScheme}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_key_from_source}; +use crate::output; +use crate::parse::parse_hex_u32; + +/// Perform RSA encryption using a TPM-loaded key. +/// +/// Wraps TPM2_RSA_Encrypt. +#[derive(Parser)] +pub struct RsaEncryptCmd { + /// RSA key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// RSA key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Encryption scheme (rsaes, oaep, null) + #[arg(short = 's', long = "scheme", default_value = "rsaes")] + pub scheme: String, + + /// Hash algorithm for OAEP (default: sha256) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Label for OAEP (optional) + #[arg(short = 'l', long = "label")] + pub label: Option, + + /// Input file (plaintext) + #[arg(short = 'i', long = "input")] + pub input: PathBuf, + + /// Output file (ciphertext) + #[arg(short = 'o', long = "output")] + pub output: PathBuf, +} + +impl RsaEncryptCmd { + fn key_context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-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.key_context_source()?)?; + + let hash_alg = crate::parse::parse_hashing_algorithm(&self.hash_algorithm)?; + let scheme = parse_rsa_scheme(&self.scheme, hash_alg)?; + + let plaintext = std::fs::read(&self.input) + .with_context(|| format!("reading input from {}", self.input.display()))?; + let message = PublicKeyRsa::try_from(plaintext) + .map_err(|e| anyhow::anyhow!("invalid plaintext: {e}"))?; + + let label_data = match &self.label { + Some(l) => { + let bytes = l.as_bytes().to_vec(); + Data::try_from(bytes).map_err(|e| anyhow::anyhow!("label too large: {e}"))? + } + None => Data::default(), + }; + + let ciphertext = ctx + .execute_without_session(|ctx| ctx.rsa_encrypt(key_handle, message, scheme, label_data)) + .context("TPM2_RSA_Encrypt failed")?; + + output::write_to_file(&self.output, ciphertext.value())?; + info!("ciphertext saved to {}", self.output.display()); + Ok(()) + } +} + +fn parse_rsa_scheme( + s: &str, + hash_alg: tss_esapi::interface_types::algorithm::HashingAlgorithm, +) -> anyhow::Result { + match s.to_lowercase().as_str() { + "rsaes" => Ok(RsaDecryptionScheme::RsaEs), + "oaep" => Ok(RsaDecryptionScheme::Oaep( + tss_esapi::structures::HashScheme::new(hash_alg), + )), + "null" => Ok(RsaDecryptionScheme::Null), + _ => anyhow::bail!("unsupported RSA scheme: {s}"), + } +} diff --git a/src/cmd/selftest.rs b/src/cmd/selftest.rs new file mode 100644 index 0000000..c25018d --- /dev/null +++ b/src/cmd/selftest.rs @@ -0,0 +1,28 @@ +use anyhow::Context; +use clap::Parser; +use log::info; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Run the TPM self test. +/// +/// Wraps TPM2_SelfTest. +#[derive(Parser)] +pub struct SelfTestCmd { + /// Run full self test (default: true) + #[arg(long = "full-test", default_value = "true")] + pub full_test: bool, +} + +impl SelfTestCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + ctx.execute_without_session(|ctx| ctx.self_test(self.full_test)) + .context("TPM2_SelfTest failed")?; + + info!("self test completed (full={})", self.full_test); + Ok(()) + } +} diff --git a/src/cmd/send.rs b/src/cmd/send.rs new file mode 100644 index 0000000..c175257 --- /dev/null +++ b/src/cmd/send.rs @@ -0,0 +1,77 @@ +use std::io::{Read, Write}; +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; + +use crate::cli::GlobalOpts; +use crate::output; + +/// Send a raw TPM command buffer and receive the response. +/// +/// This is a low-level tool for sending pre-built TPM command bytes +/// directly to the TPM device. Works with device TCTIs (e.g. /dev/tpm0). +#[derive(Parser)] +pub struct SendCmd { + /// Input file containing the raw TPM command bytes + #[arg(short = 'i', long = "input")] + pub input: PathBuf, + + /// Output file for the raw TPM response + #[arg(short = 'o', long = "output")] + pub output: Option, + + /// TPM device path (default: extracted from TCTI or /dev/tpm0) + #[arg(short = 'd', long = "device")] + pub device: Option, +} + +impl SendCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let cmd_bytes = std::fs::read(&self.input) + .with_context(|| format!("reading command from {}", self.input.display()))?; + + let device_path = match &self.device { + Some(d) => d.clone(), + None => crate::tcti::extract_device_path(global.tcti.as_deref()), + }; + + // Open the device, write the command, read the response + let mut dev = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&device_path) + .with_context(|| format!("opening TPM device {device_path}"))?; + + dev.write_all(&cmd_bytes) + .context("writing command to TPM device")?; + + // Read response: TPM responses have the size in bytes 2-5 (big-endian u32) + let mut header = [0u8; 10]; + dev.read_exact(&mut header) + .context("reading TPM response header")?; + + let response_size = + u32::from_be_bytes([header[2], header[3], header[4], header[5]]) as usize; + let mut response = vec![0u8; response_size]; + response[..10].copy_from_slice(&header); + if response_size > 10 { + dev.read_exact(&mut response[10..]) + .context("reading TPM response body")?; + } + + if let Some(ref path) = self.output { + output::write_to_file(path, &response)?; + info!( + "response ({} bytes) saved to {}", + response.len(), + path.display() + ); + } else { + output::print_hex(&response); + } + + Ok(()) + } +} diff --git a/src/cmd/sessionconfig.rs b/src/cmd/sessionconfig.rs new file mode 100644 index 0000000..478fccc --- /dev/null +++ b/src/cmd/sessionconfig.rs @@ -0,0 +1,105 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::attributes::SessionAttributesBuilder; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{ObjectHandle, SessionHandle}; +use tss_esapi::tss2_esys::TPMA_SESSION; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::session::load_session_from_file; + +const DECRYPT_BIT: TPMA_SESSION = 1 << 5; +const ENCRYPT_BIT: TPMA_SESSION = 1 << 6; +const AUDIT_BIT: TPMA_SESSION = 1 << 7; + +/// Configure session attributes (encrypt, decrypt, audit, etc.). +/// +/// Modifies the session attributes and saves the session back. +#[derive(Parser)] +pub struct SessionConfigCmd { + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Enable command encryption + #[arg(long = "enable-encrypt")] + pub enable_encrypt: bool, + + /// Enable command decryption + #[arg(long = "enable-decrypt")] + pub enable_decrypt: bool, + + /// Enable audit + #[arg(long = "enable-audit")] + pub enable_audit: bool, + + /// Disable command encryption + #[arg(long = "disable-encrypt")] + pub disable_encrypt: bool, + + /// Disable command decryption + #[arg(long = "disable-decrypt")] + pub disable_decrypt: bool, + + /// Disable audit + #[arg(long = "disable-audit")] + pub disable_audit: bool, +} + +impl SessionConfigCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + // Load the session. + let session = load_session_from_file(&mut ctx, &self.session, SessionType::Hmac)?; + + let session_handle: SessionHandle = session.into(); + + // Get current attributes as raw TPMA_SESSION. + let current_attrs = ctx + .tr_sess_get_attributes(session) + .context("failed to get session attributes")?; + let mut raw: TPMA_SESSION = current_attrs.into(); + + // Modify based on flags using named bit constants. + if self.enable_encrypt { + raw |= ENCRYPT_BIT; + } + if self.enable_decrypt { + raw |= DECRYPT_BIT; + } + if self.enable_audit { + raw |= AUDIT_BIT; + } + if self.disable_encrypt { + raw &= !ENCRYPT_BIT; + } + if self.disable_decrypt { + raw &= !DECRYPT_BIT; + } + if self.disable_audit { + raw &= !AUDIT_BIT; + } + + let (attrs, mask) = SessionAttributesBuilder::new() + .with_decrypt(raw & DECRYPT_BIT != 0) + .with_encrypt(raw & ENCRYPT_BIT != 0) + .with_audit(raw & AUDIT_BIT != 0) + .build(); + + ctx.tr_sess_set_attributes(session, attrs, mask) + .context("failed to set session attributes")?; + + info!("session attributes updated"); + + // Save back. + let obj_handle: ObjectHandle = session_handle.into(); + crate::session::save_session_and_forget(ctx, obj_handle, &self.session)?; + + Ok(()) + } +} diff --git a/src/cmd/setclock.rs b/src/cmd/setclock.rs new file mode 100644 index 0000000..a102022 --- /dev/null +++ b/src/cmd/setclock.rs @@ -0,0 +1,54 @@ +use clap::Parser; +use log::info; +use tss_esapi::tss2_esys::*; + +use crate::cli::GlobalOpts; +use crate::parse; +use crate::raw_esys::RawEsysContext; + +/// Set the TPM clock to a new value. +/// +/// Wraps TPM2_ClockSet (raw FFI). +#[derive(Parser)] +pub struct SetClockCmd { + /// 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, + + /// New clock value (milliseconds) + #[arg()] + pub new_time: u64, +} + +impl SetClockCmd { + 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())?; + } + + unsafe { + let rc = Esys_ClockSet( + raw.ptr(), + auth_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + self.new_time, + ); + if rc != 0 { + anyhow::bail!("Esys_ClockSet failed: 0x{rc:08x}"); + } + } + + info!("clock set to {}", self.new_time); + Ok(()) + } +} diff --git a/src/cmd/setcommandauditstatus.rs b/src/cmd/setcommandauditstatus.rs new file mode 100644 index 0000000..afd8699 --- /dev/null +++ b/src/cmd/setcommandauditstatus.rs @@ -0,0 +1,93 @@ +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; + +/// Set or clear the audit status for a command. +/// +/// Wraps TPM2_SetCommandCodeAuditStatus (raw FFI). +#[derive(Parser)] +pub struct SetCommandAuditStatusCmd { + /// 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, + + /// Hash algorithm for the audit digest + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Command codes to set for audit (comma-separated hex) + #[arg(long = "set-list")] + pub set_list: Option, + + /// Command codes to clear from audit (comma-separated hex) + #[arg(long = "clear-list")] + pub clear_list: Option, +} + +impl SetCommandAuditStatusCmd { + 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 audit_alg: u16 = match self.hash_algorithm.to_lowercase().as_str() { + "sha1" => TPM2_ALG_SHA1, + "sha256" => TPM2_ALG_SHA256, + "sha384" => TPM2_ALG_SHA384, + "sha512" => TPM2_ALG_SHA512, + _ => anyhow::bail!("unknown hash algorithm: {}", self.hash_algorithm), + }; + + let set_list = parse_command_list(self.set_list.as_deref())?; + let clear_list = parse_command_list(self.clear_list.as_deref())?; + + unsafe { + let rc = Esys_SetCommandCodeAuditStatus( + raw.ptr(), + auth_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + audit_alg, + &set_list, + &clear_list, + ); + if rc != 0 { + anyhow::bail!("Esys_SetCommandCodeAuditStatus failed: 0x{rc:08x}"); + } + } + + info!("command audit status updated"); + Ok(()) + } +} + +fn parse_command_list(s: Option<&str>) -> anyhow::Result { + let mut list = TPML_CC::default(); + if let Some(codes) = s { + for code_str in codes.split(',') { + let stripped = code_str + .trim() + .strip_prefix("0x") + .unwrap_or(code_str.trim()); + let code: u32 = u32::from_str_radix(stripped, 16) + .map_err(|_| anyhow::anyhow!("invalid command code: {code_str}"))?; + list.commandCodes[list.count as usize] = code; + list.count += 1; + } + } + Ok(list) +} diff --git a/src/cmd/setprimarypolicy.rs b/src/cmd/setprimarypolicy.rs new file mode 100644 index 0000000..040a410 --- /dev/null +++ b/src/cmd/setprimarypolicy.rs @@ -0,0 +1,79 @@ +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::parse; +use crate::raw_esys::RawEsysContext; + +/// Set the authorization policy for a hierarchy. +/// +/// Wraps TPM2_SetPrimaryPolicy (raw FFI). +#[derive(Parser)] +pub struct SetPrimaryPolicyCmd { + /// Hierarchy (o/owner, e/endorsement, p/platform, l/lockout) + #[arg(short = 'C', long = "hierarchy")] + pub hierarchy: String, + + /// Auth value + #[arg(short = 'P', long = "auth")] + pub auth: Option, + + /// Policy digest file + #[arg(short = 'L', long = "policy")] + pub policy: PathBuf, + + /// Hash algorithm used for the policy (default: sha256) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, +} + +impl SetPrimaryPolicyCmd { + 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 policy_data = std::fs::read(&self.policy) + .with_context(|| format!("reading policy from {}", self.policy.display()))?; + + let mut auth_policy = TPM2B_DIGEST::default(); + let len = policy_data.len().min(auth_policy.buffer.len()); + auth_policy.size = len as u16; + auth_policy.buffer[..len].copy_from_slice(&policy_data[..len]); + + let hash_alg: u16 = match self.hash_algorithm.to_lowercase().as_str() { + "sha1" => TPM2_ALG_SHA1, + "sha256" => TPM2_ALG_SHA256, + "sha384" => TPM2_ALG_SHA384, + "sha512" => TPM2_ALG_SHA512, + _ => anyhow::bail!("unknown hash algorithm: {}", self.hash_algorithm), + }; + + unsafe { + let rc = Esys_SetPrimaryPolicy( + raw.ptr(), + auth_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &auth_policy, + hash_alg, + ); + if rc != 0 { + anyhow::bail!("Esys_SetPrimaryPolicy failed: 0x{rc:08x}"); + } + } + + info!("primary policy set for hierarchy {}", self.hierarchy); + Ok(()) + } +} diff --git a/src/cmd/shutdown.rs b/src/cmd/shutdown.rs new file mode 100644 index 0000000..cfa02f0 --- /dev/null +++ b/src/cmd/shutdown.rs @@ -0,0 +1,31 @@ +use anyhow::Context; +use clap::Parser; +use log::info; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Send TPM2_Shutdown command. +#[derive(Parser)] +pub struct ShutdownCmd { + /// Send Shutdown(CLEAR) instead of Shutdown(STATE) + #[arg(short = 'c', long = "clear")] + pub clear: bool, +} + +impl ShutdownCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let shutdown_type = if self.clear { + tss_esapi::constants::StartupType::Clear + } else { + tss_esapi::constants::StartupType::State + }; + + ctx.shutdown(shutdown_type) + .context("TPM2_Shutdown failed")?; + info!("TPM2_Shutdown successful"); + Ok(()) + } +} diff --git a/src/cmd/sign.rs b/src/cmd/sign.rs new file mode 100644 index 0000000..dd07a0c --- /dev/null +++ b/src/cmd/sign.rs @@ -0,0 +1,114 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::tss::TPM2_RH_NULL; +use tss_esapi::structures::{Digest, HashcheckTicket}; +use tss_esapi::traits::Marshall; +use tss_esapi::tss2_esys::TPMT_TK_HASHCHECK; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_key_from_source}; +use crate::output; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Sign a digest with a TPM key. +#[derive(Parser)] +pub struct SignCmd { + /// 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, + + /// Hash algorithm (sha1, sha256, sha384, sha512) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Signature scheme (rsassa, rsapss, ecdsa) + #[arg(short = 's', long = "scheme", default_value = "rsassa")] + pub scheme: String, + + /// File containing the digest to sign + #[arg(short = 'd', long = "digest")] + pub digest: PathBuf, + + /// Output file for the signature + #[arg(short = 'o', long)] + pub output: Option, + + /// Hashcheck ticket file from tpm2 hash (required for restricted keys) + #[arg(short = 't', long = "ticket")] + pub ticket: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl SignCmd { + 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 mut ctx = create_context(global.tcti.as_deref())?; + + let key_handle = load_key_from_source(&mut ctx, &self.context_source()?)?; + let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?; + let scheme = parse::parse_signature_scheme(&self.scheme, hash_alg)?; + + let digest_bytes = std::fs::read(&self.digest) + .with_context(|| format!("reading digest: {}", self.digest.display()))?; + let digest = + Digest::try_from(digest_bytes).map_err(|e| anyhow::anyhow!("invalid digest: {e}"))?; + + let validation = if let Some(ref ticket_path) = self.ticket { + let ticket_data = std::fs::read(ticket_path) + .with_context(|| format!("reading ticket from {}", ticket_path.display()))?; + if ticket_data.len() < std::mem::size_of::() { + anyhow::bail!("ticket file too small"); + } + let tss_ticket: TPMT_TK_HASHCHECK = + unsafe { std::ptr::read(ticket_data.as_ptr() as *const TPMT_TK_HASHCHECK) }; + HashcheckTicket::try_from(tss_ticket) + .map_err(|e| anyhow::anyhow!("invalid ticket: {e}"))? + } else { + // Null ticket for externally-provided digests (unrestricted keys only) + HashcheckTicket::try_from(TPMT_TK_HASHCHECK { + tag: tss_esapi::constants::StructureTag::Hashcheck.into(), + hierarchy: TPM2_RH_NULL, + digest: Default::default(), + }) + .map_err(|e| anyhow::anyhow!("failed to create hashcheck ticket: {e}"))? + }; + + let session_path = self.session.as_deref(); + let signature = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.sign(key_handle, digest.clone(), scheme, validation.clone()) + }) + .context("TPM2_Sign failed")?; + + let sig_bytes = signature + .marshall() + .context("failed to marshal TPMT_SIGNATURE")?; + + if let Some(ref path) = self.output { + std::fs::write(path, &sig_bytes)?; + info!("signature saved to {}", path.display()); + } else { + output::print_hex(&sig_bytes); + } + + Ok(()) + } +} diff --git a/src/cmd/startauthsession.rs b/src/cmd/startauthsession.rs new file mode 100644 index 0000000..4ac43e6 --- /dev/null +++ b/src/cmd/startauthsession.rs @@ -0,0 +1,81 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::SessionHandle; +use tss_esapi::structures::SymmetricDefinition; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::parse; + +/// Start a TPM authorization session and save the session context to a file. +/// +/// The session can later be used for policy evaluation or HMAC-based +/// authorization in other commands. +#[derive(Parser)] +pub struct StartAuthSessionCmd { + /// Output file for the session context + #[arg(short = 'S', long = "session")] + pub session: PathBuf, + + /// Hash algorithm for the session (sha1, sha256, sha384, sha512) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Start a policy session (instead of the default trial session) + #[arg(long = "policy-session", conflicts_with_all = ["hmac-session", "audit-session"])] + pub policy_session: bool, + + /// Start an HMAC session + #[arg(long = "hmac-session", conflicts_with_all = ["policy-session", "audit-session"])] + pub hmac_session: bool, + + /// Start an audit session (HMAC with audit flag) + #[arg(long = "audit-session", conflicts_with_all = ["policy-session", "hmac-session"])] + pub audit_session: bool, +} + +impl StartAuthSessionCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?; + let session_type = self.resolve_session_type(); + + let session = ctx + .start_auth_session( + None, + None, + None, + session_type, + SymmetricDefinition::AES_128_CFB, + hash_alg, + ) + .context("TPM2_StartAuthSession failed")? + .ok_or_else(|| anyhow::anyhow!("no session returned"))?; + + // Save the session context to file. + let session_handle: SessionHandle = session.into(); + let handle: tss_esapi::handles::ObjectHandle = session_handle.into(); + crate::session::save_session_and_forget(ctx, handle, &self.session)?; + + info!( + "session ({session_type:?}) saved to {}", + self.session.display() + ); + Ok(()) + } + + fn resolve_session_type(&self) -> SessionType { + if self.policy_session { + SessionType::Policy + } else if self.hmac_session || self.audit_session { + SessionType::Hmac + } else { + SessionType::Trial + } + } +} diff --git a/src/cmd/startup.rs b/src/cmd/startup.rs new file mode 100644 index 0000000..df2b247 --- /dev/null +++ b/src/cmd/startup.rs @@ -0,0 +1,30 @@ +use anyhow::Context; +use clap::Parser; +use log::info; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Send TPM2_Startup command. +#[derive(Parser)] +pub struct StartupCmd { + /// Send Startup(CLEAR) — reset TPM state + #[arg(short = 'c', long = "clear")] + pub clear: bool, +} + +impl StartupCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let startup_type = if self.clear { + tss_esapi::constants::StartupType::Clear + } else { + tss_esapi::constants::StartupType::State + }; + + ctx.startup(startup_type).context("TPM2_Startup failed")?; + info!("TPM2_Startup successful"); + Ok(()) + } +} diff --git a/src/cmd/stirrandom.rs b/src/cmd/stirrandom.rs new file mode 100644 index 0000000..fd04123 --- /dev/null +++ b/src/cmd/stirrandom.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::SensitiveData; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Add external entropy to the TPM RNG state. +/// +/// Wraps TPM2_StirRandom. +#[derive(Parser)] +pub struct StirRandomCmd { + /// Input file containing entropy data + #[arg(short = 'i', long = "input")] + pub input: PathBuf, +} + +impl StirRandomCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let data = std::fs::read(&self.input) + .with_context(|| format!("reading entropy from {}", self.input.display()))?; + let sensitive = + SensitiveData::try_from(data).map_err(|e| anyhow::anyhow!("input too large: {e}"))?; + + ctx.execute_without_session(|ctx| ctx.stir_random(sensitive)) + .context("TPM2_StirRandom failed")?; + + info!("entropy added to TPM RNG"); + Ok(()) + } +} diff --git a/src/cmd/testparms.rs b/src/cmd/testparms.rs new file mode 100644 index 0000000..e6313ac --- /dev/null +++ b/src/cmd/testparms.rs @@ -0,0 +1,82 @@ +use clap::Parser; +use log::info; +use tss_esapi::interface_types::key_bits::RsaKeyBits; +use tss_esapi::structures::{ + PublicKeyedHashParameters, PublicParameters, PublicRsaParameters, RsaExponent, RsaScheme, + SymmetricCipherParameters, SymmetricDefinitionObject, +}; + +use crate::cli::GlobalOpts; +use crate::context::create_context; + +/// Check if the TPM supports a given algorithm combination. +/// +/// Wraps TPM2_TestParms. +#[derive(Parser)] +pub struct TestParmsCmd { + /// Algorithm parameters to test. Format: + /// Supported: rsa, rsa2048, rsa3072, rsa4096, + /// aes, aes128, aes192, aes256, + /// keyedhash, hmac, xor + #[arg()] + pub parameters: String, +} + +impl TestParmsCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let params = parse_public_params(&self.parameters)?; + + match ctx.execute_without_session(|ctx| ctx.test_parms(params)) { + Ok(()) => { + info!("parameters '{}' are supported", self.parameters); + println!("supported"); + } + Err(e) => { + println!("not supported: {e}"); + } + } + + Ok(()) + } +} + +fn parse_public_params(s: &str) -> anyhow::Result { + match s.to_lowercase().as_str() { + "rsa" | "rsa2048" => Ok(PublicParameters::Rsa(PublicRsaParameters::new( + SymmetricDefinitionObject::Null, + RsaScheme::Null, + RsaKeyBits::Rsa2048, + RsaExponent::default(), + ))), + "rsa3072" => Ok(PublicParameters::Rsa(PublicRsaParameters::new( + SymmetricDefinitionObject::Null, + RsaScheme::Null, + RsaKeyBits::Rsa3072, + RsaExponent::default(), + ))), + "rsa4096" => Ok(PublicParameters::Rsa(PublicRsaParameters::new( + SymmetricDefinitionObject::Null, + RsaScheme::Null, + RsaKeyBits::Rsa4096, + RsaExponent::default(), + ))), + "keyedhash" | "hmac" | "xor" => Ok(PublicParameters::KeyedHash( + PublicKeyedHashParameters::new(tss_esapi::structures::KeyedHashScheme::HMAC_SHA_256), + )), + "aes" | "aes128" => Ok(PublicParameters::SymCipher(SymmetricCipherParameters::new( + SymmetricDefinitionObject::Aes { + key_bits: tss_esapi::interface_types::key_bits::AesKeyBits::Aes128, + mode: tss_esapi::interface_types::algorithm::SymmetricMode::Cfb, + }, + ))), + "aes256" => Ok(PublicParameters::SymCipher(SymmetricCipherParameters::new( + SymmetricDefinitionObject::Aes { + key_bits: tss_esapi::interface_types::key_bits::AesKeyBits::Aes256, + mode: tss_esapi::interface_types::algorithm::SymmetricMode::Cfb, + }, + ))), + _ => anyhow::bail!("unsupported parameter set: {s}"), + } +} diff --git a/src/cmd/tpmhmac.rs b/src/cmd/tpmhmac.rs new file mode 100644 index 0000000..f65bc1c --- /dev/null +++ b/src/cmd/tpmhmac.rs @@ -0,0 +1,97 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::structures::MaxBuffer; + +use crate::cli::GlobalOpts; +use crate::context::create_context; +use crate::handle::{ContextSource, load_object_from_source}; +use crate::output; +use crate::parse::{self, parse_hex_u32}; +use crate::session::execute_with_optional_session; + +/// Compute an HMAC using the TPM. +/// +/// Wraps TPM2_HMAC: computes an HMAC over the input data using the +/// specified loaded HMAC key and hash algorithm. +#[derive(Parser)] +pub struct HmacCmd { + /// HMAC key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// HMAC key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Auth value for the key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Hash algorithm (default: sha256) + #[arg(short = 'g', long = "hash-algorithm", default_value = "sha256")] + pub hash_algorithm: String, + + /// Input data file (reads from stdin if not provided) + #[arg(short = 'i', long = "input")] + pub input: PathBuf, + + /// Output file for the HMAC digest + #[arg(short = 'o', long = "output")] + pub output: Option, + + /// Session context file + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl HmacCmd { + fn key_context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-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_object_from_source(&mut ctx, &self.key_context_source()?)?; + let hash_alg = parse::parse_hashing_algorithm(&self.hash_algorithm)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + ctx.tr_set_auth(key_handle, auth) + .context("tr_set_auth failed")?; + } + + let data = std::fs::read(&self.input) + .with_context(|| format!("reading input from {}", self.input.display()))?; + let buffer = MaxBuffer::try_from(data) + .map_err(|e| anyhow::anyhow!("input too large for TPM buffer: {e}"))?; + + let session_path = self.session.as_deref(); + let digest = execute_with_optional_session(&mut ctx, session_path, |ctx| { + ctx.hmac(key_handle, buffer.clone(), hash_alg) + }) + .context("TPM2_HMAC failed")?; + + if let Some(ref path) = self.output { + output::write_to_file(path, digest.value())?; + info!("HMAC digest saved to {}", path.display()); + } else { + output::print_hex(digest.value()); + } + + Ok(()) + } +} diff --git a/src/cmd/unseal.rs b/src/cmd/unseal.rs new file mode 100644 index 0000000..b381066 --- /dev/null +++ b/src/cmd/unseal.rs @@ -0,0 +1,64 @@ +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::output; +use crate::parse::parse_hex_u32; +use crate::session::execute_with_optional_session; + +/// Unseal data previously sealed to a TPM object. +#[derive(Parser)] +pub struct UnsealCmd { + /// Sealed object context file path + #[arg(short = 'c', long = "context", conflicts_with = "context_handle")] + pub context: Option, + + /// Sealed object handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "context-handle", value_parser = parse_hex_u32, conflicts_with = "context")] + pub context_handle: Option, + + /// Output file for the unsealed data + #[arg(short = 'o', long)] + pub output: Option, + + /// Session context file for authorization + #[arg(short = 'S', long = "session")] + pub session: Option, +} + +impl UnsealCmd { + 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 mut ctx = create_context(global.tcti.as_deref())?; + + let obj_handle = load_object_from_source(&mut ctx, &self.context_source()?)?; + + let session_path = self.session.as_deref(); + let sensitive = + execute_with_optional_session(&mut ctx, session_path, |ctx| ctx.unseal(obj_handle)) + .context("TPM2_Unseal failed")?; + + let bytes = sensitive.value(); + + if let Some(ref path) = self.output { + output::write_to_file(path, bytes)?; + info!("unsealed {} bytes to {}", bytes.len(), path.display()); + } else { + output::write_binary_stdout(bytes)?; + } + + Ok(()) + } +} diff --git a/src/cmd/verifysignature.rs b/src/cmd/verifysignature.rs new file mode 100644 index 0000000..3c99c47 --- /dev/null +++ b/src/cmd/verifysignature.rs @@ -0,0 +1,160 @@ +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use log::info; +use tss_esapi::interface_types::resource_handles::Hierarchy; +use tss_esapi::structures::{Digest, MaxBuffer, Public, 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::{self, parse_hex_u32}; + +/// Verify a signature using a TPM-loaded key or an external public key file. +/// +/// The signature file should contain a raw TPM marshaled TPMT_SIGNATURE. +/// The verification key can be specified as a context file (`-c`), a +/// persistent handle (`-H`), or an external public key file (`-k`) in +/// marshaled TPM2B_PUBLIC format. +#[derive(Parser)] +pub struct VerifySignatureCmd { + /// Key context file path + #[arg(short = 'c', long = "context", conflicts_with_all = ["context_handle", "key_file"])] + pub context: Option, + + /// Key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "context-handle", value_parser = parse_hex_u32, conflicts_with_all = ["context", "key_file"])] + pub context_handle: Option, + + /// External public key file (marshaled TPM2B_PUBLIC binary) + #[arg(short = 'k', long = "key-file", conflicts_with_all = ["context", "context_handle"])] + pub key_file: Option, + + /// Hierarchy for the ticket (owner, endorsement, platform, null) + #[arg(short = 'C', long = "hierarchy", default_value = "owner")] + pub hierarchy: String, + + /// Hash algorithm (sha1, sha256, sha384, sha512) + #[arg( + short = 'g', + long = "hash-algorithm", + default_value = "sha256", + requires = "message", + conflicts_with = "digest" + )] + pub hash_algorithm: Option, + + /// File containing the message that was signed + #[arg( + short = 'm', + long = "message", + conflicts_with = "digest", + requires = "hash-algorithm", + conflicts_with = "digest" + )] + pub message: Option, + + /// File containing the digest that was signed + #[arg(short = 'd', long = "digest", conflicts_with_all = ["message", "hash-algorithm"])] + pub digest: Option, + + /// File containing the signature to verify (raw TPM marshaled binary) + #[arg(short = 's', long = "signature")] + pub signature: PathBuf, + + /// Output file for the verification ticket + #[arg(short = 't', long = "ticket")] + pub ticket: Option, +} + +impl VerifySignatureCmd { + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut ctx = create_context(global.tcti.as_deref())?; + + let hierarchy = + parse::parse_hierarchy(&self.hierarchy).with_context(|| "failed to parse hierarchy")?; + + // Resolve the verification key: context file, hex handle, or external key file. + let (key_handle, flush_after) = if let Some(ref key_path) = self.key_file { + let handle = load_external_public_key(&mut ctx, key_path, hierarchy)?; + (handle, true) + } else { + let src = match (&self.context, self.context_handle) { + (Some(path), None) => ContextSource::File(path.clone()), + (None, Some(handle)) => ContextSource::Handle(handle), + _ => anyhow::bail!( + "exactly one of --context, --context-handle, or --key-file must be provided" + ), + }; + let handle = load_key_from_source(&mut ctx, &src)?; + (handle, false) + }; + + let digest_bytes = if let Some(digest_path) = &self.digest { + std::fs::read(digest_path) + .with_context(|| format!("reading digest: {}", digest_path.display()))? + } else { + let message_path = self.message.as_ref().unwrap(); + let message_bytes = std::fs::read(message_path) + .with_context(|| format!("reading message: {}", message_path.display()))?; + let hash_alg_str = self.hash_algorithm.as_ref().unwrap(); + let alg = parse::parse_hashing_algorithm(hash_alg_str) + .with_context(|| "failed to parse hash algorithm")?; + let buffer = MaxBuffer::try_from(message_bytes) + .map_err(|e| anyhow::anyhow!("input too large: {e}"))?; + let (digest, _ticket) = ctx + .execute_without_session(|ctx| ctx.hash(buffer.clone(), alg, hierarchy)) + .context("TPM2_Hash failed")?; + digest.value().to_vec() + }; + + let digest = + Digest::try_from(digest_bytes).map_err(|e| anyhow::anyhow!("invalid digest: {e}"))?; + + 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}"))?; + + let _ticket = ctx + .execute_without_session(|ctx| { + ctx.verify_signature(key_handle, digest.clone(), signature.clone()) + }) + .context("TPM2_VerifySignature failed")?; + + info!("signature is valid"); + + if let Some(ref path) = self.ticket { + let out = format!("{_ticket:?}"); + std::fs::write(path, &out)?; + info!("ticket saved to {}", path.display()); + } + + // Flush the transient handle if we loaded an external key. + if flush_after { + ctx.flush_context(key_handle.into()) + .context("failed to flush external key handle")?; + } + + Ok(()) + } +} + +/// Load an external public key from a marshaled TPM2B_PUBLIC file into the TPM. +fn load_external_public_key( + ctx: &mut tss_esapi::Context, + path: &PathBuf, + hierarchy: Hierarchy, +) -> anyhow::Result { + let pub_data = std::fs::read(path) + .with_context(|| format!("reading public key file: {}", path.display()))?; + let public = Public::unmarshall(&pub_data) + .map_err(|e| anyhow::anyhow!("failed to unmarshal public key: {e}"))?; + let key_handle = ctx + .execute_without_session(|ctx| ctx.load_external_public(public, hierarchy)) + .context("TPM2_LoadExternal (public only) failed")?; + info!("loaded external public key from {}", path.display()); + Ok(key_handle) +} diff --git a/src/cmd/zgen2phase.rs b/src/cmd/zgen2phase.rs new file mode 100644 index 0000000..4db43b8 --- /dev/null +++ b/src/cmd/zgen2phase.rs @@ -0,0 +1,150 @@ +use std::path::PathBuf; + +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::RawEsysContext; + +/// Execute the second phase of a two-phase key exchange. +/// +/// Wraps TPM2_ZGen_2Phase (raw FFI). +#[derive(Parser)] +pub struct Zgen2PhaseCmd { + /// Key context file path + #[arg( + short = 'c', + long = "key-context", + conflicts_with = "key_context_handle" + )] + pub key_context: Option, + + /// Key handle (hex, e.g. 0x81000001) + #[arg(short = 'H', long = "key-context-handle", value_parser = parse_hex_u32, conflicts_with = "key_context")] + pub key_context_handle: Option, + + /// Auth value for the key + #[arg(short = 'p', long = "auth")] + pub auth: Option, + + /// Other party's static public point file (raw x||y bytes) + #[arg(long = "static-public")] + pub static_public: PathBuf, + + /// Other party's ephemeral public point file (raw x||y bytes) + #[arg(long = "ephemeral-public")] + pub ephemeral_public: PathBuf, + + /// Key exchange scheme (ecdh, sm2) + #[arg(short = 's', long = "scheme", default_value = "ecdh")] + pub scheme: String, + + /// Counter from the commit + #[arg(short = 't', long = "counter")] + pub counter: u16, + + /// Output file for Z1 point + #[arg(long = "output-Z1")] + pub output_z1: PathBuf, + + /// Output file for Z2 point + #[arg(long = "output-Z2")] + pub output_z2: PathBuf, +} + +impl Zgen2PhaseCmd { + fn key_context_source(&self) -> anyhow::Result { + match (&self.key_context, self.key_context_handle) { + (Some(path), None) => Ok(ContextSource::File(path.clone())), + (None, Some(handle)) => Ok(ContextSource::Handle(handle)), + _ => anyhow::bail!( + "exactly one of --key-context or --key-context-handle must be provided" + ), + } + } + + pub fn execute(&self, global: &GlobalOpts) -> anyhow::Result<()> { + let mut raw = RawEsysContext::new(global.tcti.as_deref())?; + let key_handle = raw.resolve_handle_from_source(&self.key_context_source()?)?; + + if let Some(ref auth_str) = self.auth { + let auth = parse::parse_auth(auth_str)?; + raw.set_auth(key_handle, auth.value())?; + } + + let static_data = std::fs::read(&self.static_public)?; + let ephemeral_data = std::fs::read(&self.ephemeral_public)?; + + let in_qs = bytes_to_ecc_point(&static_data); + let in_qe = bytes_to_ecc_point(&ephemeral_data); + + let in_scheme: u16 = match self.scheme.to_lowercase().as_str() { + "ecdh" => TPM2_ALG_ECDH, + "sm2" => TPM2_ALG_SM2, + _ => anyhow::bail!("unsupported scheme: {}", self.scheme), + }; + + unsafe { + let mut z1_ptr: *mut TPM2B_ECC_POINT = std::ptr::null_mut(); + let mut z2_ptr: *mut TPM2B_ECC_POINT = std::ptr::null_mut(); + + let rc = Esys_ZGen_2Phase( + raw.ptr(), + key_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &in_qs, + &in_qe, + in_scheme, + self.counter, + &mut z1_ptr, + &mut z2_ptr, + ); + if rc != 0 { + anyhow::bail!("Esys_ZGen_2Phase failed: 0x{rc:08x}"); + } + + if !z1_ptr.is_null() { + let z1 = ecc_point_to_bytes(&*z1_ptr); + std::fs::write(&self.output_z1, &z1)?; + info!("Z1 saved to {}", self.output_z1.display()); + Esys_Free(z1_ptr as *mut _); + } + + if !z2_ptr.is_null() { + let z2 = ecc_point_to_bytes(&*z2_ptr); + std::fs::write(&self.output_z2, &z2)?; + info!("Z2 saved to {}", self.output_z2.display()); + Esys_Free(z2_ptr as *mut _); + } + } + + info!("ZGen_2Phase succeeded"); + Ok(()) + } +} + +fn bytes_to_ecc_point(data: &[u8]) -> TPM2B_ECC_POINT { + let mut point = TPM2B_ECC_POINT::default(); + let half = data.len() / 2; + let x = &data[..half]; + let y = &data[half..]; + point.point.x.size = x.len() as u16; + point.point.x.buffer[..x.len()].copy_from_slice(x); + point.point.y.size = y.len() as u16; + point.point.y.buffer[..y.len()].copy_from_slice(y); + point.size = std::mem::size_of::() as u16; + point +} + +fn ecc_point_to_bytes(p: &TPM2B_ECC_POINT) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&p.point.x.buffer[..p.point.x.size as usize]); + out.extend_from_slice(&p.point.y.buffer[..p.point.y.size as usize]); + out +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..1816533 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,13 @@ +use tss_esapi::Context; + +use crate::error::Tpm2Error; +use crate::tcti::parse_tcti; + +/// Create a TPM [`Context`] from an optional TCTI configuration string. +/// +/// If `tcti` is `None` the default resolution order applies (env var, then +/// `device:/dev/tpm0`). +pub fn create_context(tcti: Option<&str>) -> Result { + let tcti_conf = parse_tcti(tcti)?; + Context::new(tcti_conf).map_err(Tpm2Error::Tss) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..774b866 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,19 @@ +use std::io; + +#[derive(Debug, thiserror::Error)] +pub enum Tpm2Error { + #[error("TPM error: {0}")] + Tss(#[from] tss_esapi::Error), + + #[error("invalid TCTI configuration: {0}")] + InvalidTcti(String), + + #[error("invalid auth value: {0}")] + InvalidAuth(String), + + #[error("invalid handle: {0}")] + InvalidHandle(String), + + #[error("I/O error: {0}")] + Io(#[from] io::Error), +} diff --git a/src/handle.rs b/src/handle.rs new file mode 100644 index 0000000..bd96c5d --- /dev/null +++ b/src/handle.rs @@ -0,0 +1,114 @@ +//! TPM handle/object loading utilities. +//! +//! Every function here requires a [`tss_esapi::Context`] to resolve a CLI +//! string (hex handle or file path) into a live TPM handle. Pure argument +//! parsers that do **not** need a context live in [`crate::parse`]. + +use std::path::{Path, PathBuf}; + +use anyhow::Context; +use tss_esapi::handles::{KeyHandle, NvIndexTpmHandle, ObjectHandle, TpmHandle}; +use tss_esapi::interface_types::resource_handles::NvAuth; +use tss_esapi::utils::TpmsContext; + +// --------------------------------------------------------------------------- +// ContextSource — type-safe split of "hex handle vs. file path" +// --------------------------------------------------------------------------- + +/// A resolved context source — either a file path or a raw hex handle. +/// +/// Using this enum instead of a bare `String` prevents the ambiguity where a +/// value like `deadbeef` could be interpreted as either a hex handle or a +/// filename. The CLI layer decides which variant applies at parse time. +#[derive(Debug, Clone)] +pub enum ContextSource { + /// A JSON context file path (from `--context` / `-c`). + File(PathBuf), + /// A raw persistent TPM handle (from `--context-handle` / `-H`). + Handle(u32), +} + +/// Load a [`KeyHandle`] from a [`ContextSource`]. +pub fn load_key_from_source( + ctx: &mut tss_esapi::Context, + src: &ContextSource, +) -> anyhow::Result { + match src { + ContextSource::Handle(raw) => { + let tpm_handle = TpmHandle::try_from(*raw) + .map_err(|e| anyhow::anyhow!("invalid TPM handle 0x{raw:08x}: {e}"))?; + let obj = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .with_context(|| format!("failed to load handle 0x{raw:08x}"))?; + Ok(obj.into()) + } + ContextSource::File(path) => load_key_context_file(ctx, path), + } +} + +/// Load an [`ObjectHandle`] from a [`ContextSource`]. +pub fn load_object_from_source( + ctx: &mut tss_esapi::Context, + src: &ContextSource, +) -> anyhow::Result { + match src { + ContextSource::Handle(raw) => { + let tpm_handle = TpmHandle::try_from(*raw) + .map_err(|e| anyhow::anyhow!("invalid TPM handle 0x{raw:08x}: {e}"))?; + let obj = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .with_context(|| format!("failed to load handle 0x{raw:08x}"))?; + Ok(obj) + } + ContextSource::File(path) => load_object_context_file(ctx, path), + } +} + +/// Load a key handle from a JSON context file. +pub fn load_key_context_file( + ctx: &mut tss_esapi::Context, + path: &Path, +) -> anyhow::Result { + let data = + std::fs::read(path).with_context(|| format!("reading context file: {}", path.display()))?; + let saved: TpmsContext = + serde_json::from_slice(&data).context("failed to deserialize context")?; + let handle = ctx.context_load(saved).context("context_load failed")?; + Ok(handle.into()) +} + +/// Load a generic object handle from a JSON context file. +pub fn load_object_context_file( + ctx: &mut tss_esapi::Context, + path: &Path, +) -> anyhow::Result { + let data = + std::fs::read(path).with_context(|| format!("reading context file: {}", path.display()))?; + let saved: TpmsContext = + serde_json::from_slice(&data).context("failed to deserialize context")?; + let handle = ctx.context_load(saved).context("context_load failed")?; + Ok(handle) +} + +/// Resolve the NV authorization entity for `nvread` / `nvwrite`. +/// +/// - `"o"` / `"owner"` → [`NvAuth::Owner`] +/// - `"p"` / `"platform"` → [`NvAuth::Platform`] +/// - anything else → load the NV index itself as the auth entity +pub fn resolve_nv_auth( + ctx: &mut tss_esapi::Context, + hierarchy: &str, + nv_handle: NvIndexTpmHandle, +) -> anyhow::Result { + match hierarchy.to_lowercase().as_str() { + "o" | "owner" => Ok(NvAuth::Owner), + "p" | "platform" => Ok(NvAuth::Platform), + _ => { + let tpm_handle: TpmHandle = nv_handle.into(); + let obj = ctx + .execute_without_session(|ctx| ctx.tr_from_tpm_public(tpm_handle)) + .context("failed to load NV index for auth")?; + Ok(NvAuth::NvIndex(obj.into())) + } + } +} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..19dfd3c --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,19 @@ +use anyhow::{Context, Result}; +use flexi_logger::{Duplicate, FileSpec, LevelFilter, Logger}; +use std::path::PathBuf; + +pub(crate) fn init_logger(verbosity: LevelFilter, log_file: Option) -> Result<()> { + let logger = if let Some(p) = &log_file { + Logger::with(verbosity) + .log_to_file(FileSpec::try_from(p)?) + .duplicate_to_stderr(Duplicate::from(verbosity)) + } else { + Logger::with(verbosity).log_to_stdout() + }; + + if let Err(e) = logger.start() { + return Err(e).context("failed to start flexi_logger"); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..774bf43 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,29 @@ +mod cli; +mod cmd; +mod context; +mod error; +mod handle; +mod logger; +mod output; +mod parse; +mod pcr; +mod raw_esys; +mod session; +mod tcti; + +use clap::Parser; +use log::error; + +fn main() { + let cli = cli::Cli::parse(); + + if let Err(e) = logger::init_logger(cli.global.verbosity, cli.global.log_file.clone()) { + eprintln!("error: failed to initialise logger: {e}"); + std::process::exit(1); + } + + if let Err(e) = cli.command.execute(&cli.global) { + error!("{e:#}"); + std::process::exit(1); + } +} diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..9884c26 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,19 @@ +use std::fs; +use std::io::{self, Write}; +use std::path::Path; + +/// Write raw bytes to a file, creating it if necessary. +pub fn write_to_file(path: &Path, data: &[u8]) -> io::Result<()> { + fs::write(path, data) +} + +/// Print bytes as a lowercase hex string to stdout (no `0x` prefix), followed +/// by a newline. +pub fn print_hex(data: &[u8]) { + println!("{}", hex::encode(data)); +} + +/// Write bytes to stdout in raw binary form. +pub fn write_binary_stdout(data: &[u8]) -> io::Result<()> { + io::stdout().write_all(data) +} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..afd20af --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,301 @@ +//! Pure CLI argument parsers. +//! +//! Every function in this module converts a CLI string into a typed value +//! without touching a TPM context. Functions that need a [`tss_esapi::Context`] +//! live in [`crate::handle`] or [`crate::session`]. + +use anyhow::{Context, bail}; +use tss_esapi::attributes::NvIndexAttributesBuilder; +use tss_esapi::handles::AuthHandle; +use tss_esapi::interface_types::algorithm::{HashingAlgorithm, SymmetricMode}; +use tss_esapi::interface_types::resource_handles::{Hierarchy, HierarchyAuth, Provision}; +use tss_esapi::structures::{ + Auth, HashScheme, PcrSelectionList, PcrSelectionListBuilder, PcrSlot, SignatureScheme, +}; + +use crate::error::Tpm2Error; + +// --------------------------------------------------------------------------- +// Hex +// --------------------------------------------------------------------------- + +/// Parse a hex `u32` value, accepting an optional `0x` prefix. +/// +/// Intended for use as a clap `value_parser`: +/// ```ignore +/// #[arg(value_parser = crate::parse::parse_hex_u32)] +/// pub handle: u32, +/// ``` +pub fn parse_hex_u32(s: &str) -> Result { + let digits = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s); + u32::from_str_radix(digits, 16) + .map_err(|_| format!("expected a hex value (e.g. 0x01400001), got: '{s}'")) +} + +// --------------------------------------------------------------------------- +// Hashing algorithm +// --------------------------------------------------------------------------- + +/// Parse a hashing algorithm name. +pub fn parse_hashing_algorithm(s: &str) -> anyhow::Result { + match s.to_lowercase().as_str() { + "sha1" | "sha" => Ok(HashingAlgorithm::Sha1), + "sha256" => Ok(HashingAlgorithm::Sha256), + "sha384" => Ok(HashingAlgorithm::Sha384), + "sha512" => Ok(HashingAlgorithm::Sha512), + "sm3_256" | "sm3" => Ok(HashingAlgorithm::Sm3_256), + "sha3_256" => Ok(HashingAlgorithm::Sha3_256), + "sha3_384" => Ok(HashingAlgorithm::Sha3_384), + "sha3_512" => Ok(HashingAlgorithm::Sha3_512), + _ => bail!("unknown hashing algorithm: {s}"), + } +} + +// --------------------------------------------------------------------------- +// Signature scheme +// --------------------------------------------------------------------------- + +/// Parse a signature scheme name together with the hashing algorithm it uses. +pub fn parse_signature_scheme( + s: &str, + hash_alg: HashingAlgorithm, +) -> anyhow::Result { + let hs = HashScheme::new(hash_alg); + match s.to_lowercase().as_str() { + "rsassa" => Ok(SignatureScheme::RsaSsa { hash_scheme: hs }), + "rsapss" => Ok(SignatureScheme::RsaPss { hash_scheme: hs }), + "ecdsa" => Ok(SignatureScheme::EcDsa { hash_scheme: hs }), + "null" => Ok(SignatureScheme::Null), + _ => bail!("unsupported signature scheme: {s}"), + } +} + +// --------------------------------------------------------------------------- +// Hierarchy / Provision / AuthHandle +// --------------------------------------------------------------------------- + +/// Parse a hierarchy/auth-handle specification. +/// +/// Accepted values: +/// - `o` / `owner` → [`Hierarchy::Owner`] +/// - `p` / `platform` → [`Hierarchy::Platform`] +/// - `e` / `endorsement` → [`Hierarchy::Endorsement`] +/// - `n` / `null` → [`Hierarchy::Null`] +pub fn parse_hierarchy(value: &str) -> Result { + match value.to_lowercase().as_str() { + "o" | "owner" => Ok(Hierarchy::Owner), + "p" | "platform" => Ok(Hierarchy::Platform), + "e" | "endorsement" => Ok(Hierarchy::Endorsement), + "n" | "null" => Ok(Hierarchy::Null), + _ => Err(Tpm2Error::InvalidHandle(format!( + "unknown hierarchy: {value}" + ))), + } +} + +/// Parse a provision handle (owner or platform) for administrative commands. +pub fn parse_provision(value: &str) -> Result { + match value.to_lowercase().as_str() { + "o" | "owner" => Ok(Provision::Owner), + "p" | "platform" => Ok(Provision::Platform), + _ => Err(Tpm2Error::InvalidHandle(format!( + "provision must be 'o'/'owner' or 'p'/'platform', got: {value}" + ))), + } +} + +/// Parse an auth handle from a string (for commands like `clear`). +pub fn parse_auth_handle(value: &str) -> Result { + match value.to_lowercase().as_str() { + "o" | "owner" => Ok(AuthHandle::Owner), + "p" | "platform" => Ok(AuthHandle::Platform), + "l" | "lockout" => Ok(AuthHandle::Lockout), + _ => Err(Tpm2Error::InvalidHandle(format!( + "unknown auth handle: {value}" + ))), + } +} + +/// Map a [`Provision`] to the corresponding [`HierarchyAuth`]. +pub fn provision_to_hierarchy_auth(provision: Provision) -> HierarchyAuth { + match provision { + Provision::Owner => HierarchyAuth::Owner, + Provision::Platform => HierarchyAuth::Platform, + } +} + +// --------------------------------------------------------------------------- +// Authorization value +// --------------------------------------------------------------------------- + +/// Parse an authorization value from a CLI string. +/// +/// Supported formats: +/// - `hex:` — hex-encoded byte string +/// - `file:` — read raw bytes from file +/// - `` — plain UTF-8 password (fallback) +pub fn parse_auth(value: &str) -> Result { + let bytes = if let Some(hex_str) = value.strip_prefix("hex:") { + hex::decode(hex_str).map_err(|e| Tpm2Error::InvalidAuth(e.to_string()))? + } else if let Some(path) = value.strip_prefix("file:") { + std::fs::read(std::path::Path::new(path))? + } else { + value.as_bytes().to_vec() + }; + Auth::try_from(bytes).map_err(|e| Tpm2Error::InvalidAuth(e.to_string())) +} + +// --------------------------------------------------------------------------- +// NV attributes +// --------------------------------------------------------------------------- + +/// Parse symbolic NV index attributes separated by `|`. +pub fn parse_nv_attributes(s: &str) -> anyhow::Result { + let mut builder = NvIndexAttributesBuilder::new(); + + for attr in s.split('|') { + builder = match attr.trim().to_lowercase().as_str() { + "ownerwrite" => builder.with_owner_write(true), + "ownerread" => builder.with_owner_read(true), + "authwrite" => builder.with_auth_write(true), + "authread" => builder.with_auth_read(true), + "policywrite" => builder.with_policy_write(true), + "policyread" => builder.with_policy_read(true), + "ppwrite" => builder.with_pp_write(true), + "ppread" => builder.with_pp_read(true), + "writedefine" => builder.with_write_define(true), + "written" => builder.with_written(true), + "writeall" => builder.with_write_all(true), + "read_stclear" => builder.with_read_stclear(true), + "write_stclear" => builder.with_write_stclear(true), + "platformcreate" => builder.with_platform_create(true), + _ => anyhow::bail!("unknown NV attribute: {attr}"), + }; + } + + builder.build().context("failed to build NV attributes") +} + +// --------------------------------------------------------------------------- +// PCR selection +// --------------------------------------------------------------------------- + +/// Parse a PCR selection string like `sha256:0,1,2+sha1:all`. +pub fn parse_pcr_selection(spec: &str) -> anyhow::Result { + let mut builder = PcrSelectionListBuilder::new(); + + for bank_spec in spec.split('+') { + let (alg_str, indices_str) = bank_spec + .split_once(':') + .ok_or_else(|| anyhow::anyhow!("invalid PCR spec: missing ':' in '{bank_spec}'"))?; + + let alg = parse_hashing_algorithm(alg_str)?; + let slots = parse_pcr_indices(indices_str)?; + builder = builder.with_selection(alg, &slots); + } + + builder + .build() + .context("failed to build PCR selection list") +} + +/// Build a default selection covering sha256 and sha1, all 24 PCRs. +pub fn default_pcr_selection() -> anyhow::Result { + let all_slots = all_pcr_slots(); + PcrSelectionListBuilder::new() + .with_selection(HashingAlgorithm::Sha256, &all_slots) + .with_selection(HashingAlgorithm::Sha1, &all_slots) + .build() + .context("failed to build default PCR selection list") +} + +/// Convert a PCR index (0..31) to the corresponding [`PcrSlot`] enum variant. +pub fn index_to_pcr_slot(idx: u8) -> Option { + let bit: u32 = 1u32.checked_shl(idx as u32)?; + PcrSlot::try_from(bit).ok() +} + +/// Convert a [`PcrSlot`] back to its index (0..31). +pub fn pcr_slot_to_index(slot: PcrSlot) -> u8 { + let val: u32 = slot.into(); + val.trailing_zeros() as u8 +} + +// --------------------------------------------------------------------------- +// Symmetric mode +// --------------------------------------------------------------------------- + +/// Parse a symmetric cipher mode name. +pub fn parse_symmetric_mode(s: &str) -> anyhow::Result { + match s.to_lowercase().as_str() { + "cfb" => Ok(SymmetricMode::Cfb), + "cbc" => Ok(SymmetricMode::Cbc), + "ecb" => Ok(SymmetricMode::Ecb), + "ofb" => Ok(SymmetricMode::Ofb), + "ctr" => Ok(SymmetricMode::Ctr), + "null" => Ok(SymmetricMode::Null), + _ => bail!("unsupported symmetric mode: {s}"), + } +} + +// --------------------------------------------------------------------------- +// Qualification data +// --------------------------------------------------------------------------- + +/// Parse qualification data from an explicit hex string. +pub fn parse_qualification_hex(s: &str) -> anyhow::Result> { + let stripped = s.strip_prefix("0x").unwrap_or(s); + hex::decode(stripped).map_err(|e| anyhow::anyhow!("invalid hex qualification data '{s}': {e}")) +} + +/// Read qualification data from a file path. +pub fn parse_qualification_file(path: &std::path::Path) -> anyhow::Result> { + std::fs::read(path).with_context(|| format!("reading qualification file: {}", path.display())) +} + +// --------------------------------------------------------------------------- +// TPM2 comparison operation +// --------------------------------------------------------------------------- + +/// Parse a TPM2_EO_* comparison operation name to its `u16` constant. +pub fn parse_tpm2_operation(s: &str) -> anyhow::Result { + use tss_esapi::constants::tss::*; + match s.to_lowercase().as_str() { + "eq" => Ok(TPM2_EO_EQ), + "neq" => Ok(TPM2_EO_NEQ), + "sgt" => Ok(TPM2_EO_SIGNED_GT), + "ugt" => Ok(TPM2_EO_UNSIGNED_GT), + "slt" => Ok(TPM2_EO_SIGNED_LT), + "ult" => Ok(TPM2_EO_UNSIGNED_LT), + "sge" => Ok(TPM2_EO_SIGNED_GE), + "uge" => Ok(TPM2_EO_UNSIGNED_GE), + "sle" => Ok(TPM2_EO_SIGNED_LE), + "ule" => Ok(TPM2_EO_UNSIGNED_LE), + "bs" => Ok(TPM2_EO_BITSET), + "bc" => Ok(TPM2_EO_BITCLEAR), + _ => bail!("unknown operation: {s}; expected eq/neq/sgt/ugt/slt/ult/sge/uge/sle/ule/bs/bc"), + } +} + +fn parse_pcr_indices(s: &str) -> anyhow::Result> { + if s.eq_ignore_ascii_case("all") { + return Ok(all_pcr_slots()); + } + + s.split(',') + .map(|tok| { + let idx: u8 = tok + .trim() + .parse() + .with_context(|| format!("invalid PCR index: {tok}"))?; + index_to_pcr_slot(idx).ok_or_else(|| anyhow::anyhow!("PCR index out of range: {idx}")) + }) + .collect() +} + +fn all_pcr_slots() -> Vec { + (0u8..24).filter_map(index_to_pcr_slot).collect() +} diff --git a/src/pcr.rs b/src/pcr.rs new file mode 100644 index 0000000..322387c --- /dev/null +++ b/src/pcr.rs @@ -0,0 +1,43 @@ +//! PCR-specific TPM operations. +//! +//! Pure PCR argument parsers (selection strings, slot conversion, etc.) have +//! moved to [`crate::parse`]. This module keeps only operations that require +//! a live [`tss_esapi::Context`]. + +use anyhow::Context; +use tss_esapi::structures::{DigestList, PcrSelectionList}; + +/// Read all PCRs in `selection`, issuing multiple `TPM2_PCR_Read` calls as +/// needed because the TPM returns at most 8 digests per call. +/// +/// Returns a vec of `(pcrSelectionOut, digests)` pairs — one per TPM call — +/// preserving the ordering needed to correlate each digest with its slot. +pub fn pcr_read_all( + ctx: &mut tss_esapi::Context, + selection: PcrSelectionList, +) -> anyhow::Result> { + let mut remaining = selection; + let mut chunks = Vec::new(); + + loop { + if remaining.is_empty() { + break; + } + + let (_, read_sel, digests) = ctx + .execute_without_session(|ctx| ctx.pcr_read(remaining.clone())) + .context("TPM2_PCR_Read failed")?; + + if read_sel.is_empty() { + break; + } + + remaining + .subtract(&read_sel) + .context("failed to subtract returned PCR selection")?; + + chunks.push((read_sel, digests)); + } + + Ok(chunks) +} diff --git a/src/raw_esys.rs b/src/raw_esys.rs new file mode 100644 index 0000000..e165a65 --- /dev/null +++ b/src/raw_esys.rs @@ -0,0 +1,375 @@ +//! Raw ESYS FFI wrappers for TPM2 commands not yet in tss-esapi. +//! +//! tss-esapi 7.6.0 does not wrap `TPM2_Commit` or `TPM2_EC_Ephemeral`. +//! This module calls the C ESAPI functions directly through tss-esapi-sys, +//! managing its own raw `ESYS_CONTEXT`. + +use std::ffi::CString; +use std::path::Path; +use std::ptr::{null, null_mut}; + +use anyhow::{Context, bail}; +use tss_esapi::tss2_esys::*; + +// ----------------------------------------------------------------------- +// Raw context helpers +// ----------------------------------------------------------------------- + +/// A thin RAII wrapper around a raw `ESYS_CONTEXT*`. +pub(crate) struct RawEsysContext { + ctx: *mut ESYS_CONTEXT, +} + +impl RawEsysContext { + /// Create a new raw ESYS context from a TCTI config string. + pub(crate) fn new(tcti: Option<&str>) -> anyhow::Result { + let tcti_str = match tcti { + Some(s) => s.to_owned(), + None => { + std::env::var("TPM2TOOLS_TCTI").unwrap_or_else(|_| "device:/dev/tpm0".to_owned()) + } + }; + let c_str = CString::new(tcti_str.as_str()).context("TCTI string contains NUL")?; + + unsafe { + let mut tcti_ctx: *mut TSS2_TCTI_CONTEXT = null_mut(); + let rc = Tss2_TctiLdr_Initialize(c_str.as_ptr(), &mut tcti_ctx); + if rc != 0 { + bail!("Tss2_TctiLdr_Initialize failed: 0x{rc:08x}"); + } + + let mut esys_ctx: *mut ESYS_CONTEXT = null_mut(); + let rc = Esys_Initialize(&mut esys_ctx, tcti_ctx, null_mut()); + if rc != 0 { + Tss2_TctiLdr_Finalize(&mut tcti_ctx); + bail!("Esys_Initialize failed: 0x{rc:08x}"); + } + + Ok(Self { ctx: esys_ctx }) + } + } + + pub(crate) fn ptr(&mut self) -> *mut ESYS_CONTEXT { + self.ctx + } + + /// Set auth on an ESYS_TR handle. + pub(crate) fn set_auth(&mut self, handle: ESYS_TR, auth_bytes: &[u8]) -> anyhow::Result<()> { + unsafe { + let mut tpm2b_auth = TPM2B_AUTH { + size: auth_bytes.len() as u16, + ..Default::default() + }; + tpm2b_auth.buffer[..auth_bytes.len()].copy_from_slice(auth_bytes); + let rc = Esys_TR_SetAuth(self.ctx, handle, &tpm2b_auth); + if rc != 0 { + bail!("Esys_TR_SetAuth failed: 0x{rc:08x}"); + } + } + Ok(()) + } + + /// Resolve a hierarchy string to the well-known ESYS_TR constant. + pub(crate) fn resolve_hierarchy(s: &str) -> anyhow::Result { + match s.to_lowercase().as_str() { + "o" | "owner" => Ok(ESYS_TR_RH_OWNER), + "p" | "platform" => Ok(ESYS_TR_RH_PLATFORM), + "e" | "endorsement" => Ok(ESYS_TR_RH_ENDORSEMENT), + "n" | "null" => Ok(ESYS_TR_RH_NULL), + "l" | "lockout" => Ok(ESYS_TR_RH_LOCKOUT), + _ => bail!("unknown hierarchy: {s}"), + } + } + + /// Resolve a persistent TPM handle to an ESYS_TR. + pub(crate) fn tr_from_tpm_public(&mut self, tpm_handle: u32) -> anyhow::Result { + unsafe { + let mut esys_handle: ESYS_TR = ESYS_TR_NONE; + let rc = Esys_TR_FromTPMPublic( + self.ctx, + tpm_handle, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &mut esys_handle, + ); + if rc != 0 { + bail!("Esys_TR_FromTPMPublic failed: 0x{rc:08x}"); + } + Ok(esys_handle) + } + } + + /// Load a saved context (from JSON file) and return the ESYS_TR. + pub(crate) fn context_load(&mut self, path: &str) -> anyhow::Result { + let data = std::fs::read(path).with_context(|| format!("reading context file: {path}"))?; + let saved: tss_esapi::utils::TpmsContext = + serde_json::from_slice(&data).context("failed to deserialize context")?; + let tpms: TPMS_CONTEXT = saved + .try_into() + .map_err(|e| anyhow::anyhow!("TpmsContext conversion failed: {e:?}"))?; + + unsafe { + let mut handle: ESYS_TR = ESYS_TR_NONE; + let rc = Esys_ContextLoad(self.ctx, &tpms, &mut handle); + if rc != 0 { + bail!("Esys_ContextLoad failed: 0x{rc:08x}"); + } + Ok(handle) + } + } + + /// Resolve a [`ContextSource`] to an ESYS_TR. + pub(crate) fn resolve_handle_from_source( + &mut self, + src: &crate::handle::ContextSource, + ) -> anyhow::Result { + match src { + crate::handle::ContextSource::Handle(raw) => self.tr_from_tpm_public(*raw), + crate::handle::ContextSource::File(path) => { + self.context_load(path.to_str().unwrap_or_default()) + } + } + } + + /// Parse a hex NV index string and resolve it to an ESYS_TR. + pub(crate) fn resolve_nv_index(&mut self, s: &str) -> anyhow::Result { + let stripped = s.strip_prefix("0x").unwrap_or(s); + let raw: u32 = u32::from_str_radix(stripped, 16) + .map_err(|_| anyhow::anyhow!("invalid NV index: {s}"))?; + self.tr_from_tpm_public(raw) + } + + /// Save a session/object handle to a JSON context file via Esys_ContextSave. + pub(crate) fn context_save_to_file( + &mut self, + handle: ESYS_TR, + path: &Path, + ) -> anyhow::Result<()> { + unsafe { + let mut saved_ptr: *mut TPMS_CONTEXT = null_mut(); + let rc = Esys_ContextSave(self.ctx, handle, &mut saved_ptr); + if rc != 0 { + bail!("Esys_ContextSave failed: 0x{rc:08x}"); + } + let saved = *saved_ptr; + Esys_Free(saved_ptr as *mut _); + let tpms: tss_esapi::utils::TpmsContext = saved + .try_into() + .map_err(|e| anyhow::anyhow!("TpmsContext conversion failed: {e:?}"))?; + let json = serde_json::to_string(&tpms)?; + std::fs::write(path, json) + .with_context(|| format!("writing context to {}", path.display()))?; + } + Ok(()) + } +} + +impl Drop for RawEsysContext { + fn drop(&mut self) { + unsafe { + Esys_Finalize(&mut self.ctx); + } + } +} + +// ----------------------------------------------------------------------- +// TPM2_Commit +// ----------------------------------------------------------------------- + +/// Result of a TPM2_Commit operation. +pub struct CommitResult { + pub k: Vec, + pub l: Vec, + pub e: Vec, + pub counter: u16, +} + +/// Execute TPM2_Commit via raw ESYS FFI. +pub fn commit( + tcti: Option<&str>, + key_context: &crate::handle::ContextSource, + auth: Option<&str>, + p1: Option<&[u8]>, + s2: Option<&[u8]>, + y2: Option<&[u8]>, +) -> anyhow::Result { + let mut raw = RawEsysContext::new(tcti)?; + let sign_handle = raw.resolve_handle_from_source(key_context)?; + + // Set auth on the key if provided. + if let Some(auth_str) = auth { + let auth_val = crate::parse::parse_auth(auth_str)?; + unsafe { + let mut tpm2b_auth = TPM2B_AUTH { + size: auth_val.value().len() as u16, + ..Default::default() + }; + tpm2b_auth.buffer[..auth_val.value().len()].copy_from_slice(auth_val.value()); + let rc = Esys_TR_SetAuth(raw.ptr(), sign_handle, &tpm2b_auth); + if rc != 0 { + bail!("Esys_TR_SetAuth failed: 0x{rc:08x}"); + } + } + } + + // Build input structures. + let p1_struct = p1.map(bytes_to_ecc_point); + let s2_struct = s2.map(|data| { + let mut sd = TPM2B_SENSITIVE_DATA::default(); + let len = data.len().min(sd.buffer.len()); + sd.size = len as u16; + sd.buffer[..len].copy_from_slice(&data[..len]); + sd + }); + let y2_struct = y2.map(|data| { + let mut ep = TPM2B_ECC_PARAMETER::default(); + let len = data.len().min(ep.buffer.len()); + ep.size = len as u16; + ep.buffer[..len].copy_from_slice(&data[..len]); + ep + }); + + let p1_ptr = p1_struct.as_ref().map_or(null(), |p| p as *const _); + let s2_ptr = s2_struct.as_ref().map_or(null(), |p| p as *const _); + let y2_ptr = y2_struct.as_ref().map_or(null(), |p| p as *const _); + + unsafe { + let mut k_ptr: *mut TPM2B_ECC_POINT = null_mut(); + let mut l_ptr: *mut TPM2B_ECC_POINT = null_mut(); + let mut e_ptr: *mut TPM2B_ECC_POINT = null_mut(); + let mut counter: u16 = 0; + + let rc = Esys_Commit( + raw.ptr(), + sign_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + p1_ptr, + s2_ptr, + y2_ptr, + &mut k_ptr, + &mut l_ptr, + &mut e_ptr, + &mut counter, + ); + if rc != 0 { + bail!("Esys_Commit failed: 0x{rc:08x}"); + } + + let k = ecc_point_ptr_to_bytes(k_ptr); + let l = ecc_point_ptr_to_bytes(l_ptr); + let e = ecc_point_ptr_to_bytes(e_ptr); + + Esys_Free(k_ptr as *mut _); + Esys_Free(l_ptr as *mut _); + Esys_Free(e_ptr as *mut _); + + Ok(CommitResult { k, l, e, counter }) + } +} + +// ----------------------------------------------------------------------- +// TPM2_EC_Ephemeral +// ----------------------------------------------------------------------- + +/// Execute TPM2_EC_Ephemeral via raw ESYS FFI. +/// +/// Returns `(q_point_bytes, counter)`. +pub fn ec_ephemeral(tcti: Option<&str>, curve_id: u16) -> anyhow::Result<(Vec, u16)> { + let mut raw = RawEsysContext::new(tcti)?; + + unsafe { + let mut q_ptr: *mut TPM2B_ECC_POINT = null_mut(); + let mut counter: u16 = 0; + + let rc = Esys_EC_Ephemeral( + raw.ptr(), + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + curve_id, + &mut q_ptr, + &mut counter, + ); + if rc != 0 { + bail!("Esys_EC_Ephemeral failed: 0x{rc:08x}"); + } + + let q = ecc_point_ptr_to_bytes(q_ptr); + Esys_Free(q_ptr as *mut _); + + Ok((q, counter)) + } +} + +// ----------------------------------------------------------------------- +// Attestation / signature output helpers +// ----------------------------------------------------------------------- + +/// Write a raw `TPM2B_ATTEST` to a file (attestation data only, no header). +/// +/// # Safety +/// `info` must be a valid pointer returned by an ESYS function, or null. +pub(crate) unsafe fn write_raw_attestation( + info: *const TPM2B_ATTEST, + path: &Path, +) -> anyhow::Result<()> { + if info.is_null() { + bail!("attestation pointer is null"); + } + let attest = unsafe { &*info }; + let data = &attest.attestationData[..attest.size as usize]; + crate::output::write_to_file(path, data) + .with_context(|| format!("writing attestation to {}", path.display())) +} + +/// Write a raw `TPMT_SIGNATURE` to a file as its full struct bytes. +/// +/// # Safety +/// `sig` must be a valid pointer returned by an ESYS function, or null. +pub(crate) unsafe fn write_raw_signature( + sig: *const TPMT_SIGNATURE, + path: &Path, +) -> anyhow::Result<()> { + if sig.is_null() { + bail!("signature pointer is null"); + } + let sig_bytes = unsafe { + std::slice::from_raw_parts(sig as *const u8, std::mem::size_of::()) + }; + crate::output::write_to_file(path, sig_bytes) + .with_context(|| format!("writing signature to {}", path.display())) +} + +// ----------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------- + +fn bytes_to_ecc_point(data: &[u8]) -> TPM2B_ECC_POINT { + let mut point = TPM2B_ECC_POINT::default(); + // Split data in half: first half is x, second half is y. + let half = data.len() / 2; + let x = &data[..half]; + let y = &data[half..]; + point.point.x.size = x.len() as u16; + point.point.x.buffer[..x.len()].copy_from_slice(x); + point.point.y.size = y.len() as u16; + point.point.y.buffer[..y.len()].copy_from_slice(y); + point.size = std::mem::size_of::() as u16; + point +} + +unsafe fn ecc_point_ptr_to_bytes(ptr: *mut TPM2B_ECC_POINT) -> Vec { + if ptr.is_null() { + return Vec::new(); + } + let p = unsafe { &*ptr }; + let x_len = p.point.x.size as usize; + let y_len = p.point.y.size as usize; + let mut out = Vec::with_capacity(x_len + y_len); + out.extend_from_slice(&p.point.x.buffer[..x_len]); + out.extend_from_slice(&p.point.y.buffer[..y_len]); + out +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..421e827 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,135 @@ +//! Session loading and management utilities. +//! +//! Functions in this module deal with TPM authorization sessions: loading +//! a previously saved session context from a file, starting EK policy +//! sessions, and running closures with a user-supplied or default session. + +use std::path::Path; + +use anyhow::Context; +use tss_esapi::constants::SessionType; +use tss_esapi::handles::{AuthHandle, ObjectHandle, SessionHandle}; +use tss_esapi::interface_types::algorithm::HashingAlgorithm; +use tss_esapi::interface_types::session_handles::{AuthSession, PolicySession}; +use tss_esapi::structures::SymmetricDefinition; +use tss_esapi::utils::TpmsContext; + +/// Load a session context from a JSON file and return it as an [`AuthSession`]. +/// +/// The file must contain a serialized [`TpmsContext`] (as produced by +/// [`tss_esapi::Context::context_save`]). The `session_type` determines +/// whether the returned [`AuthSession`] is an HMAC session or a policy +/// session variant; the TPM itself tracks the real session type, but the +/// Rust wrapper needs to know in order to produce the right enum variant. +pub fn load_session_from_file( + ctx: &mut tss_esapi::Context, + path: &Path, + session_type: SessionType, +) -> anyhow::Result { + let data = + std::fs::read(path).with_context(|| format!("reading session file: {}", path.display()))?; + let saved: TpmsContext = + serde_json::from_slice(&data).context("failed to deserialize session context")?; + let obj_handle: ObjectHandle = ctx + .context_load(saved) + .context("context_load (session) failed")?; + let session_handle: SessionHandle = obj_handle.into(); + + // AuthSession::create produces Some(_) for any non-None handle. + AuthSession::create(session_type, session_handle, HashingAlgorithm::Sha256) + .ok_or_else(|| anyhow::anyhow!("loaded session handle is not a valid auth session")) +} + +/// Execute a closure with either a loaded session or a default null-auth session. +/// +/// When `session_path` is `Some`, the session context file is loaded and set +/// as the sole authorization session. When `None`, the standard +/// [`execute_with_nullauth_session`](tss_esapi::Context::execute_with_nullauth_session) +/// convenience method is used. +pub fn execute_with_optional_session( + ctx: &mut tss_esapi::Context, + session_path: Option<&Path>, + f: F, +) -> anyhow::Result +where + F: FnOnce(&mut tss_esapi::Context) -> tss_esapi::Result, +{ + match session_path { + Some(path) => { + let session = load_session_from_file(ctx, path, SessionType::Hmac)?; + ctx.set_sessions((Some(session), None, None)); + let result = f(ctx).map_err(|e| anyhow::anyhow!(e))?; + ctx.clear_sessions(); + Ok(result) + } + None => ctx + .execute_with_nullauth_session(f) + .map_err(|e| anyhow::anyhow!(e)), + } +} + +/// Start a policy session and satisfy `PolicySecret(TPM_RH_ENDORSEMENT)`. +/// +/// This is required for any command that uses the EK as a parent, since the +/// TCG default EK template has `adminWithPolicy`. +pub fn start_ek_policy_session(ctx: &mut tss_esapi::Context) -> anyhow::Result { + let session = ctx + .start_auth_session( + None, + None, + None, + SessionType::Policy, + SymmetricDefinition::AES_128_CFB, + HashingAlgorithm::Sha256, + ) + .context("TPM2_StartAuthSession failed")? + .ok_or_else(|| anyhow::anyhow!("no session returned"))?; + + let policy_session: PolicySession = session + .try_into() + .map_err(|_| anyhow::anyhow!("expected policy session"))?; + + // Satisfy the EK's policy: PolicySecret(endorsement hierarchy). + ctx.set_sessions((Some(AuthSession::Password), None, None)); + ctx.policy_secret( + policy_session, + AuthHandle::Endorsement, + Default::default(), // nonce_tpm + Default::default(), // cp_hash_a + Default::default(), // policy_ref + None, // expiration + ) + .context("TPM2_PolicySecret failed")?; + ctx.clear_sessions(); + + Ok(policy_session) +} + +/// Save a session handle to a JSON file and leak the context. +/// +/// After `context_save` the C ESAPI layer invalidates the ESYS_TR, but the +/// Rust `handle_manager` retains a stale entry. Consuming `ctx` by value +/// and calling `mem::forget` avoids spurious flush errors in `Context::drop`. +pub fn save_session_and_forget( + mut ctx: tss_esapi::Context, + handle: impl Into, + path: &Path, +) -> anyhow::Result<()> { + let saved = ctx + .context_save(handle.into()) + .context("context_save (session) failed")?; + let json = serde_json::to_string(&saved)?; + std::fs::write(path, json).with_context(|| format!("saving session to {}", path.display()))?; + std::mem::forget(ctx); + Ok(()) +} + +/// Flush a policy session handle. +pub fn flush_policy_session( + ctx: &mut tss_esapi::Context, + policy_session: PolicySession, +) -> anyhow::Result<()> { + let ps_handle: ObjectHandle = SessionHandle::from(policy_session).into(); + ctx.flush_context(ps_handle) + .context("failed to flush policy session") +} diff --git a/src/tcti.rs b/src/tcti.rs new file mode 100644 index 0000000..3df3ef6 --- /dev/null +++ b/src/tcti.rs @@ -0,0 +1,38 @@ +use std::str::FromStr; + +use tss_esapi::tcti_ldr::TctiNameConf; + +use crate::error::Tpm2Error; + +/// Default TCTI configuration string. +pub(crate) const DEFAULT_TCTI: &str = "device:/dev/tpm0"; + +/// Default raw device path (used by `send`). +pub(crate) const DEFAULT_DEVICE_PATH: &str = "/dev/tpm0"; + +/// Parse a TCTI configuration string into a [`TctiNameConf`]. +/// +/// If `tcti` is `None`, falls back to the `TPM2TOOLS_TCTI` environment +/// variable, then to `device:/dev/tpm0`. +pub fn parse_tcti(tcti: Option<&str>) -> Result { + let tcti_str = match tcti { + Some(s) => s.to_owned(), + None => std::env::var("TPM2TOOLS_TCTI").unwrap_or_else(|_| DEFAULT_TCTI.to_owned()), + }; + TctiNameConf::from_str(&tcti_str).map_err(|e| Tpm2Error::InvalidTcti(e.to_string())) +} + +/// Extract the raw device path from a TCTI string. +/// +/// Used by `send` to open the TPM device directly. +pub(crate) fn extract_device_path(tcti: Option<&str>) -> String { + let tcti_str = match tcti { + Some(s) => s.to_owned(), + None => std::env::var("TPM2TOOLS_TCTI").unwrap_or_else(|_| DEFAULT_TCTI.to_owned()), + }; + if let Some(rest) = tcti_str.strip_prefix("device:") { + rest.to_owned() + } else { + DEFAULT_DEVICE_PATH.to_owned() + } +}