diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e25f244..d92f6ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,15 +2,11 @@ name: build on: push: - tags: - - 'v*' + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] workflow_dispatch: - inputs: - release-docker: - description: 'Build and push Docker image' - type: boolean - required: false - default: false jobs: build: @@ -33,7 +29,6 @@ jobs: path: | ${{ matrix.arch }}/boot.disk ${{ matrix.arch }}/busybox - ${{ matrix.arch }}/bubblewrap ${{ matrix.arch }}/stage1 ${{ matrix.arch }}/os-release ${{ matrix.arch }}/efi-vars.* @@ -41,7 +36,7 @@ jobs: release-docker: runs-on: ubuntu-latest needs: build - if: inputs.release-docker + if: startsWith(github.ref, 'refs/tags/v') permissions: contents: write packages: write diff --git a/Cargo.lock b/Cargo.lock index 3f32832..37b5e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "atomic-waker" @@ -69,9 +69,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" @@ -90,21 +90,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.44" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -184,16 +184,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "coset" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8cc80f631f8307b887faca24dcc3abc427cd0367f6eb6188f6e8f5b7ad8fb" -dependencies = [ - "ciborium", - "ciborium-io", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -223,9 +213,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -398,9 +388,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flagset" @@ -408,12 +398,6 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -475,9 +459,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -486,9 +470,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -553,12 +537,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -593,9 +576,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -631,14 +614,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -701,9 +683,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -715,9 +697,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -772,9 +754,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -782,15 +764,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -807,15 +789,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "litemap" @@ -825,21 +807,21 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -848,9 +830,9 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ "lazy_static", "libm", @@ -1045,18 +1027,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -1090,47 +1072,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rawdogtpm2" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64", - "ciborium", - "hex", - "hmac", - "serde", - "serde_bytes", - "serde_json", - "sha1", - "sha2", - "thiserror", -] - -[[package]] -name = "rawdogtpm2-verify" -version = "0.1.0" -dependencies = [ - "base64", - "ciborium", - "coset", - "der", - "ecdsa", - "hex", - "p256", - "p384", - "rawdogtpm2", - "rustls-pki-types", - "rustls-rustcrypto", - "rustls-webpki", - "serde", - "serde_json", - "sha2", - "spki", - "thiserror", - "x509-cert", -] - [[package]] name = "reqwest" version = "0.12.24" @@ -1196,9 +1137,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -1239,9 +1180,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] @@ -1295,9 +1236,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "sec1" @@ -1361,15 +1302,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -1424,9 +1365,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1436,9 +1377,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -1474,13 +1415,13 @@ dependencies = [ "base64", "hex", "libc", - "rawdogtpm2", "reqwest", "rustls", "rustls-rustcrypto", "serde", "serde_json", "sha2", + "vaportpm-attest", ] [[package]] @@ -1491,9 +1432,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1573,9 +1514,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -1597,9 +1538,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -1612,9 +1553,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -1642,9 +1583,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -1652,9 +1593,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -1695,9 +1636,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -1711,6 +1652,26 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "vaportpm-attest" +version = "0.1.0" +source = "git+https://github.com/lockboot/vaportpm#66fd00340fd853695d9adf1d89ae5625a00a4939" +dependencies = [ + "anyhow", + "base64", + "ciborium", + "der", + "hex", + "hmac", + "serde", + "serde_bytes", + "serde_json", + "sha1", + "sha2", + "thiserror", + "x509-cert", +] + [[package]] name = "version_check" version = "0.9.5" @@ -1734,9 +1695,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -1747,11 +1708,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -1760,9 +1722,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1770,9 +1732,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -1783,18 +1745,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -1802,9 +1764,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -2025,18 +1987,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -2075,9 +2037,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", @@ -2116,3 +2078,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml index 42f9cb5..b53cea5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/rawdogtpm2", "crates/rawdogtpm2-verify", "crates/stage1", "crates/example-stage2"] +members = ["crates/stage1", "crates/example-stage2"] resolver = "2" [workspace.package] @@ -8,25 +8,13 @@ license = "MIT OR Apache-2.0" [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde_bytes = "0.11" base64 = "0.22" -sha1 = "0.10" sha2 = "0.10" -hmac = "0.12" anyhow = "1.0" -thiserror = "1.0" hex = "0.4" libc = "0.2" -# X.509 and crypto for verification -der = { version = "0.7", features = ["alloc", "pem", "oid"] } -spki = { version = "0.7", features = ["alloc"] } -x509-cert = { version = "0.2", features = ["pem"] } -p256 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] } -p384 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] } -ecdsa = { version = "0.16", features = ["verifying", "der"] } -coset = "0.3" -ciborium = "0.2" +vaportpm-attest = { git = "https://github.com/lockboot/vaportpm" } reqwest = { version = "=0.12.24", default-features = false, features = ["blocking", "rustls-tls-webpki-roots-no-provider"] } rustls = { version = "=0.23.23", default-features = false, features = ["std", "tls12"] } diff --git a/Dockerfile.build b/Dockerfile.build index c824995..5e4a561 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -5,32 +5,28 @@ RUN rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl # Install tools needed for UKI building RUN apt-get -qq update && \ - apt-get install -y \ - binutils \ + apt-get install -y --no-install-recommends \ binutils-aarch64-linux-gnu \ binutils-x86-64-linux-gnu \ - gcc \ - gcc-aarch64-linux-gnu \ - qemu-efi-aarch64 \ - kmod \ cpio \ - gzip \ - findutils \ + curl \ dosfstools \ + efitools \ fdisk \ + findutils \ gdisk \ - util-linux \ + gzip \ + kmod \ + make \ + ovmf \ python3 \ python3-pip \ + qemu-efi-aarch64 \ rpm2cpio \ sbsigntool \ - efitools \ + util-linux \ uuid-runtime \ - musl-tools \ - ovmf \ - wget \ - curl \ - make && \ + wget && \ pip3 install --break-system-packages virt-firmware # Reproducible builds environment diff --git a/Makefile b/Makefile index 4766a38..1ffafb3 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ define ARTIFACT_RULE @cp downloads/$$@ $$@ endef -$(foreach artifact,busybox bubblewrap stub.efi kernel.rpm,$(eval $(call ARTIFACT_RULE,$(artifact)))) +$(foreach artifact,busybox stub.efi kernel.rpm,$(eval $(call ARTIFACT_RULE,$(artifact)))) downloads/%: $(MAKE) -C downloads $* @@ -72,7 +72,7 @@ docker-buildx-setup: docker buildx inspect --bootstrap # Build runtime image for current platform only and load into Docker -docker-runtime: x86_64/busybox x86_64/bubblewrap x86_64/stage1 aarch64/busybox aarch64/bubblewrap aarch64/stage1 +docker-runtime: x86_64/busybox x86_64/stage1 aarch64/busybox aarch64/stage1 docker buildx build \ -f Dockerfile.runtime \ -t $(RUNTIME_IMAGE) \ @@ -80,7 +80,7 @@ docker-runtime: x86_64/busybox x86_64/bubblewrap x86_64/stage1 aarch64/busybox a . # Build multi-arch and export to OCI tar (for local multi-arch without registry) -docker-runtime-oci: x86_64/busybox x86_64/bubblewrap x86_64/stage1 aarch64/busybox aarch64/bubblewrap aarch64/stage1 +docker-runtime-oci: x86_64/busybox x86_64/stage1 aarch64/busybox aarch64/stage1 docker buildx build \ --platform linux/amd64,linux/arm64 \ -f Dockerfile.runtime \ @@ -129,7 +129,7 @@ docker-shell-dev: docker-build-dev # Build the UKI and boot disk for a specific architecture # This creates: UKI, disk image with EFI boot structure -%/boot.disk: %/busybox %/bubblewrap %/stage1 %/stub.efi %/kernel.rpm keys/db.crt +%/boot.disk: %/busybox %/stage1 %/stub.efi %/kernel.rpm keys/db.crt $(DOCKER_RUN) $(DOCKER_OPT_DOCKER) -e ARCH=$* \ $(BUILD_IMAGE) ./scripts/build.sh diff --git a/README.md b/README.md index 226e21e..af56059 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,7 @@ You can run any statically linked Linux ELF, but the minimal filesystem only has - **[stage1](crates/stage1/README.md)**: Secure bootloader (fetches config, verifies binaries, extends PCRs) - **[example-stage2](crates/example-stage2/README.md)**: Example user application -- **[rawdogtpm2](crates/rawdogtpm2/README.md)**: Low-level TPM 2.0 operations library -- **[rawdogtpm2-verify](crates/rawdogtpm2-verify/README.md)**: Verify attestations made by rawdogtpm2 +- **[vaportpm](https://github.com/lockboot/vaportpm)**: TPM 2.0 attestation library (external dependency) ## Cloud Deployment diff --git a/crates/rawdogtpm2-verify/Cargo.toml b/crates/rawdogtpm2-verify/Cargo.toml deleted file mode 100644 index 9c26908..0000000 --- a/crates/rawdogtpm2-verify/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "rawdogtpm2-verify" -version = "0.1.0" -edition = "2021" -description = "Attestation verification for TPM and Nitro attestations" -license.workspace = true - -[dependencies] -rawdogtpm2 = { path = "../rawdogtpm2" } - -# X.509 parsing -der = { workspace = true } -spki = { workspace = true } -x509-cert = { workspace = true } - -# Certificate chain validation (rustls ecosystem) -webpki = { workspace = true } -pki-types = { workspace = true } -rustls-rustcrypto = { workspace = true } - -# Signature verification (still needed for TPM AK signature) -p256 = { workspace = true } -p384 = { workspace = true } -ecdsa = { workspace = true } - -# COSE/CBOR for Nitro -coset = { workspace = true } -ciborium = { workspace = true } - -# Common -sha2 = { workspace = true } -hex = { workspace = true } -base64 = { workspace = true } -thiserror = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } - -[lib] -name = "rawdogtpm2_verify" -path = "src/lib.rs" - -[[bin]] -name = "selftest-verify" -path = "src/bin/selftest-verify.rs" - -[[bin]] -name = "test-verify" -path = "src/bin/test-verify.rs" diff --git a/crates/rawdogtpm2-verify/README.md b/crates/rawdogtpm2-verify/README.md deleted file mode 100644 index 5ea8248..0000000 --- a/crates/rawdogtpm2-verify/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# rawdogtpm2-verify - -Verification library for TPM and Nitro attestations produced by `rawdogtpm2`. - -## Overview - -This crate verifies attestation documents without requiring TPM access. It can run anywhere - on a server, in a browser (via WASM), or in any environment that needs to verify attestations. - -## Features - -- **Nitro Attestation Verification** - COSE Sign1 signature and certificate chain validation -- **TPM Signature Verification** - ECDSA P-256 signature verification -- **PCR Policy Verification** - Compute and verify PCR policy digests -- **X.509 Chain Validation** - Certificate chain validation using rustls-webpki -- **Zero TPM Dependencies** - Pure cryptographic verification - -## Usage - -```rust -use rawdogtpm2_verify::{verify_attestation_output, AttestationOutput}; - -fn verify(json: &str) -> Result<(), Box> { - // Parse the attestation output - let output: AttestationOutput = serde_json::from_str(json)?; - - // Verify the entire attestation - let result = verify_attestation_output(&output)?; - - println!("Verified via: {:?}", result.method); - println!("Nonce: {}", result.nonce); - println!("Root CA hash: {}", result.root_pubkey_hash); - - Ok(()) -} -``` - -## What Gets Verified - -### Nitro Path (AWS) - -| Check | Description | -|-------|-------------| -| COSE Signature | ECDSA P-384 signature over Nitro document | -| Certificate Chain | Validates chain, returns root pubkey hash | -| Public Key Binding | AK public key matches signed `public_key` field | -| TPM Signature | AK's ECDSA P-256 signature over TPM2B_ATTEST | -| PCR Policy | Certified name matches computed policy from SHA-384 PCRs | -| Nonce Binding | TPM nonce matches Nitro nonce (freshness) | -| PCR Values | Claimed PCRs match signed values in Nitro document | - -### Verification Result - -```rust -pub struct VerificationResult { - /// The verified nonce (hex-encoded) - pub nonce: String, - /// SHA-256 hash of the root CA's public key - pub root_pubkey_hash: String, - /// How verification was performed - pub method: VerificationMethod, -} -``` - -The `root_pubkey_hash` identifies the trust anchor. For AWS Nitro, this is the hash of the Nitro Root CA's public key. - -## API - -```rust -/// Verify a complete AttestationOutput, returns the root of trust hash -pub fn verify_attestation_output( - output: &AttestationOutput -) -> Result; -``` - -Returns `VerificationResult` containing: -- `nonce` - The verified challenge (hex) -- `root_pubkey_hash` - SHA-256 of the trust anchor's public key (hex) -- `method` - How verification was performed (currently only `Nitro`) - -## Security Considerations - -### Trust Model - -This library is **trust-agnostic**. It verifies cryptographic signatures and returns the `root_pubkey_hash` - the SHA-256 hash of the root CA's public key. **You** decide whether to trust that root. - -For AWS Nitro, you would check that `root_pubkey_hash` matches the known AWS Nitro Root CA public key hash. - -### What You Must Verify Separately - -1. **PCR Semantics** - This library verifies PCR *values*, not their *meaning*. You need to know what software produces which measurements. - -2. **Nonce Freshness** - You must generate and track nonces to prevent replay attacks. - -3. **Application Logic** - The attestation proves system state at a point in time. Your application must decide if that state is acceptable. - -### Verification Flow - -```mermaid -flowchart TD - A[AttestationOutput JSON] --> B{Nitro Present?} - B -->|Yes| C[Verify Nitro Document] - B -->|No| X[Error: Unsupported] - - C --> D[Verify COSE Signature] - D --> E[Validate Cert Chain] - E --> F[Extract signed public_key] - - F --> G[Verify TPM Signature] - G --> H[Parse TPM2B_ATTEST] - H --> I[Compute PCR Policy] - I --> J{Name Match?} - - J -->|Yes| K[Verify Nonce Binding] - J -->|No| Y[Error: PCR Mismatch] - - K --> L{Nonces Match?} - L -->|Yes| M[Success: Return VerificationResult] - L -->|No| Z[Error: Freshness Failed] -``` \ No newline at end of file diff --git a/crates/rawdogtpm2-verify/src/bin/selftest-verify.rs b/crates/rawdogtpm2-verify/src/bin/selftest-verify.rs deleted file mode 100644 index 1c8804d..0000000 --- a/crates/rawdogtpm2-verify/src/bin/selftest-verify.rs +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! Verification selftest binary -//! -//! Tests verification functionality against a real TPM. -//! Uses rawdogtpm2 for TPM operations and rawdogtpm2-verify for verification. - -use hex; -use sha2::{Sha256, Digest}; -use std::collections::BTreeMap; - -use rawdogtpm2::{ - Tpm, TpmAlg, EkOps, PcrOps, NvOps, - TPM_RH_OWNER, TPM_RH_ENDORSEMENT, - NV_INDEX_ECC_P256_EK_CERT, - der_to_pem, -}; -use rawdogtpm2_verify::{ - verify_ecdsa_p256, compute_ecc_p256_name, calculate_pcr_policy, - extract_public_key, parse_cert_chain_pem, hash_public_key, -}; - -fn main() -> Result<(), Box> { - println!("Verification Selftest (Real TPM)"); - println!("=================================\n"); - - // Open TPM device - println!("Opening TPM device..."); - let mut tpm = Tpm::open()?; - println!("✓ TPM device opened successfully\n"); - - // Test 1: Software verification of TPM signature - println!("Test 1: TPM Signature with Software Verification"); - println!("-------------------------------------------------"); - - // Create a signing key - let key_result = tpm.create_primary_ecc_key(TPM_RH_OWNER)?; - println!("Created signing key: 0x{:08X}", key_result.handle); - - // Sign some data with the TPM - let test_data = b"Test data for signature verification"; - let digest = Sha256::digest(test_data); - println!("Test data: {:?}", std::str::from_utf8(test_data).unwrap()); - println!("SHA256 digest: {}", hex::encode(&digest)); - - let signature = tpm.sign(key_result.handle, &digest)?; - println!("TPM signature: {} bytes (DER)", signature.len()); - - // Verify using rawdogtpm2-verify (p256 crate) - let mut pubkey = vec![0x04]; - pubkey.extend(&key_result.public_key.x); - pubkey.extend(&key_result.public_key.y); - - match verify_ecdsa_p256(test_data, &signature, &pubkey) { - Ok(()) => { - println!("✓ Signature verified successfully using software (p256 crate)"); - } - Err(e) => { - println!("✗ Signature verification FAILED: {}", e); - } - } - - // Test with wrong data (should fail) - let wrong_data = b"Wrong data"; - match verify_ecdsa_p256(wrong_data, &signature, &pubkey) { - Ok(()) => { - println!("✗ FAIL: Verification should have failed with wrong data"); - } - Err(_) => { - println!("✓ Correctly rejected signature for wrong data"); - } - } - - tpm.flush_context(key_result.handle)?; - println!(); - - // Test 2: Standard EK and Certificate Comparison - println!("Test 2: Standard EK vs Certificate Comparison"); - println!("----------------------------------------------"); - - match tpm.create_standard_ek() { - Ok(standard_ek) => { - println!("✓ Standard EK created using TCG template"); - println!(" EK X: {}", hex::encode(&standard_ek.public_key.x)); - println!(" EK Y: {}", hex::encode(&standard_ek.public_key.y)); - - // Try to read EK certificate from NV RAM - match tpm.nv_read(NV_INDEX_ECC_P256_EK_CERT) { - Ok(cert_der) => { - println!("\n Found EK certificate in NV RAM ({} bytes)", cert_der.len()); - - if cert_der.starts_with(&[0x30, 0x82]) { - // Convert DER to PEM for parsing - let pem = der_to_pem(&cert_der, "CERTIFICATE"); - - match parse_cert_chain_pem(&pem) { - Ok(chain) => { - match extract_public_key(&chain[0]) { - Ok(cert_pubkey) => { - println!(" Certificate pubkey: {} bytes", cert_pubkey.len()); - - // Build EK pubkey in same format - let mut ek_pubkey = vec![0x04]; - ek_pubkey.extend(&standard_ek.public_key.x); - ek_pubkey.extend(&standard_ek.public_key.y); - - if ek_pubkey == cert_pubkey { - println!("✓ Standard EK matches certificate!"); - println!(" Deterministic key derivation verified."); - } else { - println!("⚠ Standard EK does NOT match certificate"); - println!(" EK from TPM: {}", hex::encode(&ek_pubkey)); - println!(" EK from cert: {}", hex::encode(&cert_pubkey)); - println!(" This may indicate:"); - println!(" - Certificate was issued with a different template"); - println!(" - TPM was re-provisioned after certificate issuance"); - } - } - Err(e) => { - println!(" Could not extract public key from certificate: {}", e); - } - } - } - Err(e) => { - println!(" Could not parse certificate: {}", e); - } - } - } else { - println!(" Certificate is not in standard DER format"); - } - } - Err(e) => { - println!(" No EK certificate in NV RAM: {}", e); - println!(" (Cannot compare - certificate not available)"); - } - } - - tpm.flush_context(standard_ek.handle)?; - } - Err(e) => { - println!("⚠ Could not create standard EK: {}", e); - println!(" (Endorsement hierarchy may require authentication)"); - } - } - println!(); - - // Test 3: PCR Policy Calculation and Verification - println!("Test 3: PCR Policy Calculation"); - println!("-------------------------------"); - - // Read actual PCR values from TPM - let all_pcrs = tpm.read_all_allocated_pcrs()?; - let sha256_pcrs: Vec<(u8, Vec)> = all_pcrs.iter() - .filter(|(_, alg, _)| *alg == TpmAlg::Sha256) - .map(|(idx, _, val)| (*idx, val.clone())) - .collect(); - - println!("Read {} SHA-256 PCRs from TPM", sha256_pcrs.len()); - - // Convert to BTreeMap for calculate_pcr_policy - let pcr_map: BTreeMap = sha256_pcrs.iter() - .map(|(idx, val)| (*idx, hex::encode(val))) - .collect(); - - // Calculate policy using rawdogtpm2-verify - let policy_hex = calculate_pcr_policy(&pcr_map, TpmAlg::Sha256)?; - println!("Calculated PCR policy: {}...", &policy_hex[..32]); - - // Calculate policy using rawdogtpm2 (should match) - let policy_from_tpm = Tpm::calculate_pcr_policy_digest(&sha256_pcrs, TpmAlg::Sha256)?; - println!("Policy from rawdogtpm2: {}...", hex::encode(&policy_from_tpm[..16])); - - if policy_hex == hex::encode(&policy_from_tpm) { - println!("✓ Policy calculations match between crates"); - } else { - println!("✗ Policy calculations differ!"); - } - println!(); - - // Test 4: ReadPublic and Name Computation - println!("Test 4: ReadPublic and Name Verification"); - println!("-----------------------------------------"); - - // Create a key and verify name computation - let test_key = tpm.create_primary_ecc_key(TPM_RH_OWNER)?; - let read_result = tpm.read_public(test_key.handle)?; - - println!("TPM ReadPublic returned:"); - println!(" Public area: {} bytes", read_result.public_area.len()); - println!(" Name from TPM: {}", hex::encode(&read_result.name)); - - // Compute name using rawdogtpm2-verify - let computed_name = compute_ecc_p256_name( - &test_key.public_key.x, - &test_key.public_key.y, - &[], // empty policy for basic signing key - ); - println!(" Computed name: {}", hex::encode(&computed_name)); - - if read_result.name == computed_name { - println!("✓ TPM's name matches computed name"); - } else { - println!("⚠ Name mismatch - key may have non-empty authPolicy"); - } - - tpm.flush_context(test_key.handle)?; - println!(); - - // Test 5: Public Key Hashing - println!("Test 5: Public Key Hashing"); - println!("--------------------------"); - - match tpm.create_primary_ecc_key(TPM_RH_ENDORSEMENT) { - Ok(ek) => { - let mut ek_pubkey = vec![0x04]; - ek_pubkey.extend(&ek.public_key.x); - ek_pubkey.extend(&ek.public_key.y); - - let hash = hash_public_key(&ek_pubkey); - println!("EK public key hash: {}", hash); - println!(" (This would be the trust anchor identifier)"); - - tpm.flush_context(ek.handle)?; - println!("✓ Public key hash computed"); - } - Err(e) => { - println!("⚠ Could not access EK: {}", e); - } - } - println!(); - - // Test 6: Certify with Signature Verification - println!("Test 6: TPM2_Certify with Software Verification"); - println!("------------------------------------------------"); - - // Create an AK (signing key) and a key to certify - let ak = tpm.create_primary_ecc_key(TPM_RH_OWNER)?; - println!("Created AK: 0x{:08X}", ak.handle); - - // Create PCR-sealed key to certify - let pcr_values: Vec<(u8, Vec)> = sha256_pcrs.clone(); - let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, TpmAlg::Sha256)?; - let sealed_key = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?; - println!("Created PCR-sealed key: 0x{:08X}", sealed_key.handle); - - // Certify the sealed key with AK - let qualifying_data = b"test-certification-nonce"; - let cert_result = tpm.certify(sealed_key.handle, ak.handle, qualifying_data)?; - println!("TPM2_Certify returned:"); - println!(" Attestation data: {} bytes", cert_result.attest_data.len()); - println!(" Signature: {} bytes", cert_result.signature.len()); - - // Verify AK signature over attestation data using rawdogtpm2-verify - let mut ak_pubkey = vec![0x04]; - ak_pubkey.extend(&ak.public_key.x); - ak_pubkey.extend(&ak.public_key.y); - - match verify_ecdsa_p256(&cert_result.attest_data, &cert_result.signature, &ak_pubkey) { - Ok(()) => { - println!("✓ Certification signature verified successfully"); - } - Err(e) => { - println!("✗ Certification signature verification FAILED: {}", e); - } - } - - // Verify the certified name matches our computed name - let expected_name = compute_ecc_p256_name( - &sealed_key.public_key.x, - &sealed_key.public_key.y, - &auth_policy, - ); - println!("Expected certified name: {}", hex::encode(&expected_name)); - - tpm.flush_context(ak.handle)?; - tpm.flush_context(sealed_key.handle)?; - println!(); - - println!("==========================="); - println!("All verification tests completed!"); - println!("==========================="); - - Ok(()) -} - diff --git a/crates/rawdogtpm2-verify/src/bin/test-verify.rs b/crates/rawdogtpm2-verify/src/bin/test-verify.rs deleted file mode 100644 index 1eb9d39..0000000 --- a/crates/rawdogtpm2-verify/src/bin/test-verify.rs +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! Test binary for verifying TPM and Nitro attestations - -use rawdogtpm2::attest; -use rawdogtpm2_verify::{ - verify_tpm_attestation, verify_nitro_attestation, AttestationOutput, - parse_cert_chain_pem, extract_public_key, verify_ecdsa_p256, hash_public_key, -}; - -fn main() -> Result<(), Box> { - println!("Generating attestation..."); - let nonce = b"test-nonce-12345"; - let attestation_json = attest(nonce)?; - - println!("Attestation generated successfully!"); - println!("JSON length: {} bytes", attestation_json.len()); - - // Parse the output - let output: AttestationOutput = serde_json::from_str(&attestation_json)?; - - // Check what we got - println!("\nEK Certificates present:"); - println!(" RSA-2048: {}", output.ek_certificates.rsa_2048.is_some()); - println!(" ECC P-256: {}", output.ek_certificates.ecc_p256.is_some()); - println!(" ECC P-384: {}", output.ek_certificates.ecc_p384.is_some()); - - println!("\nTPM attestations: {:?}", output.attestation.tpm.keys().collect::>()); - println!("Nitro attestation: {}", output.attestation.nitro.is_some()); - - // Verify TPM attestation - for (key_type, attestation) in &output.attestation.tpm { - println!("\nVerifying TPM attestation for {}...", key_type); - - let cert_pem = match key_type.as_str() { - "rsa_2048" => output.ek_certificates.rsa_2048.as_ref(), - "ecc_p256" => output.ek_certificates.ecc_p256.as_ref(), - "ecc_p384" => output.ek_certificates.ecc_p384.as_ref(), - _ => None, - }; - - let ek_pk = output.ek_public_keys.get(key_type); - let ak_pk = output.signing_key_public_keys.get(key_type); - - // Debug info - println!(" Attest data (nonce): {} bytes", attestation.attest_data.len() / 2); - println!(" Signature length: {} bytes", attestation.signature.len() / 2); - - if let Some(ek) = ek_pk { - println!(" EK pubkey X: {}...", &ek.x[..std::cmp::min(16, ek.x.len())]); - println!(" EK pubkey Y: {}...", &ek.y[..std::cmp::min(16, ek.y.len())]); - } - - if let Some(ak) = ak_pk { - println!(" AK pubkey X: {}...", &ak.x[..std::cmp::min(16, ak.x.len())]); - println!(" AK pubkey Y: {}...", &ak.y[..std::cmp::min(16, ak.y.len())]); - } - - // Compare EK pubkey with certificate - if let Some(cert) = cert_pem { - if let Ok(chain) = parse_cert_chain_pem(cert) { - if let Ok(cert_pubkey) = extract_public_key(&chain[0]) { - println!(" Cert pubkey length: {} bytes", cert_pubkey.len()); - println!(" Cert pubkey: {}...", hex::encode(&cert_pubkey[..std::cmp::min(20, cert_pubkey.len())])); - - // Check if EK from attestation matches certificate - if let Some(ek) = ek_pk { - let ek_x = hex::decode(&ek.x)?; - let ek_y = hex::decode(&ek.y)?; - let mut ek_pubkey = vec![0x04]; - ek_pubkey.extend(&ek_x); - ek_pubkey.extend(&ek_y); - - if ek_pubkey == cert_pubkey { - println!(" EK pubkey MATCHES certificate!"); - } else { - println!(" EK pubkey DOES NOT MATCH certificate"); - println!(" EK from output: {}", hex::encode(&ek_pubkey)); - println!(" EK from cert: {}", hex::encode(&cert_pubkey)); - } - } - } - } - } - - // Try verification - if let (Some(cert), Some(ek), Some(ak)) = (cert_pem, ek_pk, ak_pk) { - match verify_tpm_attestation( - &attestation.attest_data, - &attestation.signature, - &ak.x, - &ak.y, - &ek.x, - &ek.y, - cert, - ) { - Ok(result) => { - println!(" Verification SUCCESS!"); - println!(" Root pubkey hash: {}", result.root_pubkey_hash); - - // Decode and show the nonce - let nonce_bytes = hex::decode(&result.nonce)?; - if let Ok(nonce_str) = std::str::from_utf8(&nonce_bytes) { - println!(" Nonce: {}", nonce_str); - } else { - println!(" Nonce: {} (binary)", result.nonce); - } - } - Err(e) => { - println!(" Verification FAILED: {}", e); - - // Try manual verification with AK - println!("\n Trying manual AK signature verification..."); - let ak_x = hex::decode(&ak.x)?; - let ak_y = hex::decode(&ak.y)?; - let mut ak_pubkey = vec![0x04]; - ak_pubkey.extend(&ak_x); - ak_pubkey.extend(&ak_y); - - let nonce_data = hex::decode(&attestation.attest_data)?; - let signature = hex::decode(&attestation.signature)?; - - match verify_ecdsa_p256(&nonce_data, &signature, &ak_pubkey) { - Ok(()) => { - println!(" AK signature verification SUCCESS!"); - let ak_hash = hash_public_key(&ak_pubkey); - println!(" AK pubkey hash: {}", ak_hash); - } - Err(e2) => { - println!(" AK signature verification also failed: {}", e2); - } - } - } - } - } else { - println!(" Missing certificate, EK, or AK public key for {}", key_type); - } - } - - // Try Nitro if present - if let Some(ref nitro) = output.attestation.nitro { - println!("\nVerifying Nitro attestation..."); - match verify_nitro_attestation(&nitro.document, Some(nonce), None) { - Ok(result) => { - println!(" Verification SUCCESS!"); - println!(" Root pubkey hash: {}", result.root_pubkey_hash); - println!(" Module ID: {}", result.document.module_id); - } - Err(e) => { - println!(" Verification FAILED: {}", e); - } - } - } - - Ok(()) -} diff --git a/crates/rawdogtpm2-verify/src/error.rs b/crates/rawdogtpm2-verify/src/error.rs deleted file mode 100644 index 52001f0..0000000 --- a/crates/rawdogtpm2-verify/src/error.rs +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! Verification error types - -use thiserror::Error; - -/// Errors that can occur during verification -#[derive(Debug, Error)] -pub enum VerifyError { - #[error("Invalid hex encoding: {0}")] - HexDecode(#[from] hex::FromHexError), - - #[error("Invalid attestation structure: {0}")] - InvalidAttest(String), - - #[error("Signature verification failed: {0}")] - SignatureInvalid(String), - - #[error("Certificate parsing failed: {0}")] - CertificateParse(String), - - #[error("Certificate chain validation failed: {0}")] - ChainValidation(String), - - #[error("CBOR parsing failed: {0}")] - CborParse(String), - - #[error("COSE signature verification failed: {0}")] - CoseVerify(String), - - #[error("PCR index out of bounds: {0}")] - PcrIndexOutOfBounds(String), - - #[error("No attestations could be verified: {0}")] - NoValidAttestation(String), -} diff --git a/crates/rawdogtpm2-verify/src/lib.rs b/crates/rawdogtpm2-verify/src/lib.rs deleted file mode 100644 index 75f663a..0000000 --- a/crates/rawdogtpm2-verify/src/lib.rs +++ /dev/null @@ -1,391 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! Attestation verification for TPM and Nitro attestations -//! -//! This crate provides verification of attestation documents generated by rawdogtpm2: -//! - TPM attestation verification with EK public key matching -//! - X.509 certificate chain validation -//! - AWS Nitro COSE Sign1 document verification -//! - Root certificate public key hash extraction (SHA-256, hex-encoded) - -mod error; -mod nitro; -mod tpm; -mod x509; - -use serde::Serialize; - -// Re-export error type -pub use error::VerifyError; - -// Re-export TPM types and functions -pub use tpm::{ - calculate_pcr_policy, parse_tpm2b_attest, verify_ecdsa_p256, - verify_pcr_policy, verify_tpm_attestation, - verify_tpm_signature_only, TpmAttestInfo, -}; - -// Re-export from rawdogtpm2 (single source of truth for TPM crypto) -pub use rawdogtpm2::{compute_ecc_p256_name, TpmAlg}; - -// Re-export Nitro types and functions -pub use nitro::{verify_nitro_attestation, NitroDocument, NitroVerifyResult}; - -// Re-export X.509 utility functions -pub use x509::{ - extract_public_key, hash_public_key, parse_and_validate_cert_chain, - parse_and_validate_tpm_cert_chain, parse_cert_chain_pem, validate_cert_chain, - validate_tpm_cert_chain, ChainValidationResult, MAX_CHAIN_DEPTH, -}; - -// Re-export time type for testing -pub use pki_types::UnixTime; - -// Re-export types from rawdogtpm2 for convenience -pub use rawdogtpm2::a9n::{ - AttestationContainer, AttestationData, AttestationOutput, EccPublicKeyCoords, EkCertificates, - NitroAttestationData, -}; - -/// How the attestation was verified -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -pub enum VerificationMethod { - /// Verified via AWS Nitro attestation - Nitro, - /// Verified via cloud provider AK certificate chain (GCP/Azure) - /// Note: Not yet implemented - reserved for future use - #[allow(dead_code)] - CloudAkChain, -} - -/// Result of successful attestation verification -#[derive(Debug, Serialize)] -pub struct VerificationResult { - /// The nonce that was verified (hex-encoded) - pub nonce: String, - /// SHA-256 hash of the root CA's public key - pub root_pubkey_hash: String, - /// How the attestation was verified - pub method: VerificationMethod, -} - -/// Verify an entire AttestationOutput -/// -/// Currently supports one verification path: -/// -/// 1. **Nitro path** (AWS): If Nitro attestation is present, verify it and trust -/// the TPM signing key via the Nitro document's `public_key` binding. The -/// TPM2B_ATTEST structure is verified for PCR policy binding. EK certificates -/// are not required in this path. -/// -/// Future paths (not yet implemented): -/// - **GCP Shielded VM**: AK certificate from Google CA (NV index 0x01c10000) -/// - **Azure Trusted Launch**: AK certificate from Microsoft CA (NV index 0x01C101D0) -/// -/// # Returns -/// A unified `VerificationResult` containing: -/// - `nonce`: The verified challenge (from TPM2B_ATTEST.extraData) -/// - `root_pubkey_hash`: SHA-256 of the trust anchor's public key -/// - `method`: How verification was performed -/// -/// # Errors -/// Returns `NoValidAttestation` if no supported verification path is available. -pub fn verify_attestation_output( - output: &AttestationOutput, -) -> Result { - // Must have at least one TPM attestation - if output.attestation.tpm.is_empty() { - return Err(VerifyError::NoValidAttestation( - "no TPM attestations present".into(), - )); - } - - // Get the first (and typically only) TPM attestation - let (key_type, attestation) = output.attestation.tpm.iter().next().unwrap(); - - // Get the corresponding signing key (AK) public key - let ak_pk = output.signing_key_public_keys.get(key_type).ok_or_else(|| { - VerifyError::NoValidAttestation(format!("{}: missing AK public key", key_type)) - })?; - - // Decode AK public key - let ak_x = hex::decode(&ak_pk.x)?; - let ak_y = hex::decode(&ak_pk.y)?; - - // Determine PCR algorithm based on verification path - // Nitro uses SHA-384 (signed in Nitro document), non-Nitro uses SHA-256 - let (pcr_alg, pcrs_for_policy) = if output.attestation.nitro.is_some() { - (TpmAlg::Sha384, output.pcrs.get("sha384").cloned().unwrap_or_default()) - } else { - (TpmAlg::Sha256, output.pcrs.get("sha256").cloned().unwrap_or_default()) - }; - - // Parse TPM2B_ATTEST structure (needed for both paths) - let attest_data = hex::decode(&attestation.attest_data)?; - let attest_info = parse_tpm2b_attest(&attest_data)?; - - // Verify nonce field matches nonce in attest_data (prevents tampering) - let nonce_from_field = hex::decode(&attestation.nonce)?; - if nonce_from_field != attest_info.nonce { - return Err(VerifyError::InvalidAttest(format!( - "Nonce field does not match nonce in attest_data. \ - Field: {}, Attest: {}", - attestation.nonce, - hex::encode(&attest_info.nonce) - ))); - } - - // Verify AK signature over TPM2B_ATTEST - let signature = hex::decode(&attestation.signature)?; - let mut ak_pubkey = vec![0x04]; - ak_pubkey.extend(&ak_x); - ak_pubkey.extend(&ak_y); - verify_ecdsa_p256(&attest_data, &signature, &ak_pubkey)?; - - // Compute authPolicy from PCRs and verify certified name (proves PCR binding) - if !pcrs_for_policy.is_empty() { - let auth_policy_hex = calculate_pcr_policy(&pcrs_for_policy, pcr_alg)?; - let auth_policy = hex::decode(&auth_policy_hex)?; - - let expected_name = compute_ecc_p256_name(&ak_x, &ak_y, &auth_policy); - if attest_info.certified_name != expected_name { - return Err(VerifyError::InvalidAttest(format!( - "Certified key name does not match expected PCR policy. \ - AK's authPolicy does not match claimed PCR values. \ - Expected name: {}, Got: {}", - hex::encode(&expected_name), - hex::encode(&attest_info.certified_name) - ))); - } - } - - // If Nitro attestation is present, use Nitro path - if let Some(ref nitro) = output.attestation.nitro { - // Verify Nitro attestation (COSE signature, cert chain) - let nitro_result = verify_nitro_attestation( - &nitro.document, - None, // Nonce validation happens via TPM binding - None, // Pubkey validation happens below - )?; - - // Verify the convenience fields match what's signed in the Nitro document. - // These fields are duplicated in the JSON for easy access, but we must ensure - // they match the cryptographically signed values to prevent tampering. - - // Verify public_key field matches signed document - let signed_pubkey = nitro_result.document.public_key.as_ref().ok_or_else(|| { - VerifyError::NoValidAttestation( - "Nitro document missing public_key field - cannot bind TPM signing key".into() - ) - })?; - if nitro.public_key != *signed_pubkey { - return Err(VerifyError::SignatureInvalid(format!( - "attestation.nitro.public_key does not match signed value in document: {} != {}", - nitro.public_key, signed_pubkey - ))); - } - - // Verify nonce field matches signed document - let signed_nonce = nitro_result.document.nonce.as_ref().ok_or_else(|| { - VerifyError::NoValidAttestation( - "Nitro document missing nonce field - cannot verify freshness".into() - ) - })?; - if nitro.nonce != *signed_nonce { - return Err(VerifyError::SignatureInvalid(format!( - "attestation.nitro.nonce does not match signed value in document: {} != {}", - nitro.nonce, signed_nonce - ))); - } - - // Verify the AK public key matches the signed public_key - let ak_secg = format!("04{}{}", ak_pk.x, ak_pk.y); - if ak_secg != *signed_pubkey { - return Err(VerifyError::SignatureInvalid(format!( - "TPM signing key does not match Nitro public_key binding: {} != {}", - ak_secg, signed_pubkey - ))); - } - - // Verify TPM nonce matches Nitro nonce (proves attestations generated together) - let tpm_nonce_hex = hex::encode(&attest_info.nonce); - if tpm_nonce_hex != *signed_nonce { - return Err(VerifyError::SignatureInvalid(format!( - "TPM nonce does not match Nitro nonce - attestations not generated together: {} != {}", - tpm_nonce_hex, signed_nonce - ))); - } - - // Verify SHA-384 PCRs match signed values in Nitro document - // The Nitro document contains nitrotpm_pcrs which are signed by AWS hardware - if let Some(sha384_pcrs) = output.pcrs.get("sha384") { - let signed_pcrs = &nitro_result.document.pcrs; - - // Check all signed PCRs are present and match - for (idx, signed_value) in signed_pcrs.iter() { - match sha384_pcrs.get(idx) { - Some(claimed_value) if claimed_value == signed_value => { - // Match - good - } - Some(claimed_value) => { - return Err(VerifyError::SignatureInvalid(format!( - "PCR {} SHA-384 value does not match signed value: {} != {}", - idx, claimed_value, signed_value - ))); - } - None => { - return Err(VerifyError::SignatureInvalid(format!( - "PCR {} missing from output.pcrs[sha384] but present in signed Nitro document", - idx - ))); - } - } - } - } - - // Nonce is from TPM2B_ATTEST.extraData - return Ok(VerificationResult { - nonce: hex::encode(&attest_info.nonce), - root_pubkey_hash: nitro_result.root_pubkey_hash, - method: VerificationMethod::Nitro, - }); - } - - // No Nitro attestation - currently unsupported - // - // Future: GCP/Azure AK certificate path will be implemented here. - // This requires: - // 1. AK certificate from cloud provider (not just EK certificate) - // 2. Certificate chain validation to cloud provider root CA - // 3. AK certificate proves the signing key belongs to the cloud provider's vTPM - // - // EK certificates alone are NOT sufficient because: - // - EK certificate only proves "this is a genuine TPM" - // - It doesn't prove the AK (signing key) belongs to that TPM - // - Without AK binding, an attacker could use their own signing key - - Err(VerifyError::NoValidAttestation( - "No Nitro attestation present. \ - GCP/Azure AK certificate verification not yet implemented. \ - Currently only AWS Nitro attestation is supported.".into() - )) -} - -/// Convenience function to verify attestation from JSON string -/// -/// Parses JSON and calls `verify_attestation_output`. -pub fn verify_attestation_json(json: &str) -> Result { - let output: AttestationOutput = serde_json::from_str(json) - .map_err(|e| VerifyError::InvalidAttest(format!("JSON parse error: {}", e)))?; - verify_attestation_output(&output) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_verify_nitro_fixture() { - let fixture = include_str!("../test-nitro-fixture.json"); - let output: AttestationOutput = serde_json::from_str(fixture) - .expect("Failed to parse test-nitro-fixture.json"); - - let result = verify_attestation_output(&output) - .expect("Verification should succeed"); - - // Should be verified via Nitro path (no EK certs in fixture) - assert_eq!(result.method, VerificationMethod::Nitro); - - // Nonce is now from TPM2B_ATTEST.extraData, not the raw attest_data field - assert!(!result.nonce.is_empty()); - - // Should have a root pubkey hash (AWS Nitro root) - assert!(!result.root_pubkey_hash.is_empty()); - assert_eq!(result.root_pubkey_hash.len(), 64); // SHA-256 = 32 bytes = 64 hex chars - } - - #[test] - fn test_reject_empty_attestation() { - let output = AttestationOutput { - ek_certificates: EkCertificates { - rsa_2048: None, - ecc_p256: None, - ecc_p384: None, - }, - pcrs: std::collections::HashMap::new(), - ek_public_keys: std::collections::HashMap::new(), - signing_key_public_keys: std::collections::HashMap::new(), - attestation: AttestationContainer { - tpm: std::collections::HashMap::new(), - nitro: None, - }, - }; - - let result = verify_attestation_output(&output); - assert!(matches!(result, Err(VerifyError::NoValidAttestation(_)))); - } - - /// Test that tampering with the convenience public_key field is detected - #[test] - fn test_reject_tampered_nitro_public_key() { - let fixture = include_str!("../test-nitro-fixture.json"); - let mut output: AttestationOutput = serde_json::from_str(fixture) - .expect("Failed to parse test-nitro-fixture.json"); - - // Tamper with the convenience field (attacker tries to substitute their own key) - if let Some(ref mut nitro) = output.attestation.nitro { - nitro.public_key = "04aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(); - } - - let result = verify_attestation_output(&output); - assert!( - matches!(result, Err(VerifyError::SignatureInvalid(_))), - "Should reject tampered public_key field, got: {:?}", - result - ); - } - - /// Test that tampering with the convenience nonce field is detected - #[test] - fn test_reject_tampered_nitro_nonce() { - let fixture = include_str!("../test-nitro-fixture.json"); - let mut output: AttestationOutput = serde_json::from_str(fixture) - .expect("Failed to parse test-nitro-fixture.json"); - - // Tamper with the convenience field - if let Some(ref mut nitro) = output.attestation.nitro { - nitro.nonce = "deadbeef".to_string(); - } - - let result = verify_attestation_output(&output); - assert!( - matches!(result, Err(VerifyError::SignatureInvalid(_))), - "Should reject tampered nonce field, got: {:?}", - result - ); - } - - /// Test that tampering with SHA-384 PCR values is detected - #[test] - fn test_reject_tampered_pcr_values() { - let fixture = include_str!("../test-nitro-fixture.json"); - let mut output: AttestationOutput = serde_json::from_str(fixture) - .expect("Failed to parse test-nitro-fixture.json"); - - // Tamper with a SHA-384 PCR value - if let Some(sha384_pcrs) = output.pcrs.get_mut("sha384") { - sha384_pcrs.insert( - 0, - "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string() - ); - } - - let result = verify_attestation_output(&output); - // Tampering is detected at policy verification: AK's authPolicy doesn't match PCR values - assert!( - matches!(result, Err(VerifyError::InvalidAttest(_))), - "Should reject tampered PCR values, got: {:?}", - result - ); - } -} diff --git a/crates/rawdogtpm2-verify/src/nitro.rs b/crates/rawdogtpm2-verify/src/nitro.rs deleted file mode 100644 index a1c3f7f..0000000 --- a/crates/rawdogtpm2-verify/src/nitro.rs +++ /dev/null @@ -1,665 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! AWS Nitro Enclave attestation verification - -use std::collections::BTreeMap; - -use ciborium::Value as CborValue; -use coset::{CborSerializable, CoseSign1}; -use der::Decode; -use ecdsa::signature::hazmat::PrehashVerifier; -use p384::ecdsa::{Signature as P384Signature, VerifyingKey as P384VerifyingKey}; -use serde::Serialize; -use sha2::{Digest, Sha384}; -use x509_cert::Certificate; - -use pki_types::UnixTime; - -use crate::error::VerifyError; -use crate::x509::{extract_public_key, validate_cert_chain}; - -/// Result of successful Nitro attestation verification -/// -/// This struct is only returned when verification succeeds. -/// If signature or chain validation fails, an error is returned instead. -#[derive(Debug, Serialize)] -pub struct NitroVerifyResult { - /// Parsed attestation document fields - pub document: NitroDocument, - /// SHA-256 hash of the root CA's public key (hex string) - pub root_pubkey_hash: String, -} - -/// Parsed Nitro attestation document -#[derive(Debug, Serialize, Clone)] -pub struct NitroDocument { - /// Module ID - pub module_id: String, - /// Timestamp (milliseconds since epoch) - pub timestamp: u64, - /// TPM PCR values from Nitro document's `nitrotpm_pcrs` field (index -> hex SHA-384 digest) - /// These are the PCR values signed by AWS hardware. - pub pcrs: BTreeMap, - /// Public key (hex-encoded, if provided) - pub public_key: Option, - /// User data (hex-encoded, if provided) - pub user_data: Option, - /// Nonce (hex-encoded, if provided) - pub nonce: Option, - /// Digest algorithm used - pub digest: String, -} - -/// Verify Nitro attestation document -/// -/// # Arguments -/// * `document_hex` - CBOR-encoded COSE Sign1 attestation document as hex string -/// * `expected_nonce` - Expected nonce value (optional validation) -/// * `expected_pubkey_hex` - Expected public key in SECG format (optional validation) -/// -/// # Returns -/// Verification result with parsed document and root public key hash -pub fn verify_nitro_attestation( - document_hex: &str, - expected_nonce: Option<&[u8]>, - expected_pubkey_hex: Option<&str>, -) -> Result { - // Decode hex input - let document_bytes = hex::decode(document_hex)?; - - // Parse the COSE Sign1 structure (NSM returns untagged COSE) - let cose_sign1 = CoseSign1::from_slice(&document_bytes) - .map_err(|e| VerifyError::CoseVerify(format!("Failed to parse COSE Sign1: {}", e)))?; - - // Extract the payload - let payload = cose_sign1 - .payload - .as_ref() - .ok_or_else(|| VerifyError::CoseVerify("Missing payload".into()))?; - - // Parse payload as CBOR - let doc_value: CborValue = ciborium::from_reader(payload.as_slice()) - .map_err(|e| VerifyError::CborParse(format!("Failed to parse payload: {}", e)))?; - - // Extract document fields - let doc_map = match &doc_value { - CborValue::Map(m) => m, - _ => return Err(VerifyError::CborParse("Payload is not a map".into())), - }; - - let nitro_doc = parse_nitro_document(doc_map)?; - - // Validate nonce if provided - if let Some(expected) = expected_nonce { - if let Some(ref nonce_hex) = nitro_doc.nonce { - let nonce_bytes = hex::decode(nonce_hex)?; - if nonce_bytes != expected { - return Err(VerifyError::CoseVerify("Nonce mismatch".into())); - } - } - } - - // Validate public key if provided - if let Some(expected_pk) = expected_pubkey_hex { - if let Some(ref pk) = nitro_doc.public_key { - if pk != expected_pk { - return Err(VerifyError::CoseVerify("Public key mismatch".into())); - } - } - } - - // Extract certificate and CA bundle - let cert_der = extract_cbor_bytes(doc_map, "certificate")?; - let cabundle = extract_cbor_byte_array(doc_map, "cabundle")?; - - // Parse certificates - let leaf_cert = Certificate::from_der(&cert_der) - .map_err(|e| VerifyError::CertificateParse(format!("Invalid leaf cert: {}", e)))?; - - let mut chain = vec![leaf_cert]; - for ca_der in cabundle { - let ca_cert = Certificate::from_der(&ca_der) - .map_err(|e| VerifyError::CertificateParse(format!("Invalid CA cert: {}", e)))?; - chain.push(ca_cert); - } - - // Verify COSE signature using leaf certificate (fails on error) - // Do this before chain validation to fail fast on signature issues - let leaf_pubkey = extract_public_key(&chain[0])?; - verify_cose_signature(&cose_sign1, &leaf_pubkey, payload)?; - - // Validate certificate chain using webpki - // This validates signatures, dates, and returns root's public key hash - let chain_result = validate_cert_chain(&chain, UnixTime::now())?; - let root_pubkey_hash = chain_result.root_pubkey_hash; - - Ok(NitroVerifyResult { - document: nitro_doc, - root_pubkey_hash, - }) -} - -/// Parse Nitro document fields from CBOR map -fn parse_nitro_document( - map: &[(CborValue, CborValue)], -) -> Result { - let module_id = extract_cbor_text(map, "module_id")?; - let timestamp = extract_cbor_integer(map, "timestamp")?; - let digest = extract_cbor_text(map, "digest")?; - - // Parse PCRs - let pcrs = extract_cbor_pcrs(map)?; - - // Optional fields - let public_key = extract_cbor_bytes_optional(map, "public_key").map(|b| hex::encode(&b)); - let user_data = extract_cbor_bytes_optional(map, "user_data").map(|b| hex::encode(&b)); - let nonce = extract_cbor_bytes_optional(map, "nonce").map(|b| hex::encode(&b)); - - Ok(NitroDocument { - module_id, - timestamp, - pcrs, - public_key, - user_data, - nonce, - digest, - }) -} - -/// Extract text field from CBOR map -fn extract_cbor_text(map: &[(CborValue, CborValue)], key: &str) -> Result { - for (k, v) in map { - if let CborValue::Text(k_text) = k { - if k_text == key { - if let CborValue::Text(val) = v { - return Ok(val.clone()); - } - } - } - } - Err(VerifyError::CborParse(format!("Missing field: {}", key))) -} - -/// Extract integer field from CBOR map -fn extract_cbor_integer(map: &[(CborValue, CborValue)], key: &str) -> Result { - for (k, v) in map { - if let CborValue::Text(k_text) = k { - if k_text == key { - if let CborValue::Integer(val) = v { - let val_i128: i128 = (*val).into(); - // Validate range before casting - if val_i128 < 0 { - return Err(VerifyError::CborParse(format!( - "Field {} has negative value: {}", key, val_i128 - ))); - } - if val_i128 > u64::MAX as i128 { - return Err(VerifyError::CborParse(format!( - "Field {} exceeds u64 range: {}", key, val_i128 - ))); - } - return Ok(val_i128 as u64); - } - } - } - } - Err(VerifyError::CborParse(format!("Missing field: {}", key))) -} - -/// Extract bytes field from CBOR map -fn extract_cbor_bytes(map: &[(CborValue, CborValue)], key: &str) -> Result, VerifyError> { - for (k, v) in map { - if let CborValue::Text(k_text) = k { - if k_text == key { - if let CborValue::Bytes(val) = v { - return Ok(val.clone()); - } - } - } - } - Err(VerifyError::CborParse(format!("Missing field: {}", key))) -} - -/// Extract optional bytes field from CBOR map -fn extract_cbor_bytes_optional(map: &[(CborValue, CborValue)], key: &str) -> Option> { - for (k, v) in map { - if let CborValue::Text(k_text) = k { - if k_text == key { - if let CborValue::Bytes(val) = v { - return Some(val.clone()); - } - if let CborValue::Null = v { - return None; - } - } - } - } - None -} - -/// Extract byte array field from CBOR map -fn extract_cbor_byte_array( - map: &[(CborValue, CborValue)], - key: &str, -) -> Result>, VerifyError> { - for (k, v) in map { - if let CborValue::Text(k_text) = k { - if k_text == key { - if let CborValue::Array(arr) = v { - let mut result = Vec::new(); - for item in arr { - if let CborValue::Bytes(b) = item { - result.push(b.clone()); - } - } - return Ok(result); - } - } - } - } - Err(VerifyError::CborParse(format!("Missing field: {}", key))) -} - -/// Maximum valid PCR index for AWS Nitro Enclaves (0-15) -const MAX_NITRO_ENCLAVE_PCR_INDEX: u8 = 15; - -/// Maximum valid PCR index for TPMs (0-23) -const MAX_TPM_PCR_INDEX: u8 = 23; - -/// Extract PCRs from CBOR map -/// Handles both "pcrs" (Nitro Enclave) and "nitrotpm_pcrs" (Nitro TPM) field names -fn extract_cbor_pcrs(map: &[(CborValue, CborValue)]) -> Result, VerifyError> { - for (k, v) in map { - if let CborValue::Text(k_text) = k { - // Check for both field names: "pcrs" (enclave) and "nitrotpm_pcrs" (TPM) - let (is_pcrs, max_index) = if k_text == "pcrs" { - (true, MAX_NITRO_ENCLAVE_PCR_INDEX) - } else if k_text == "nitrotpm_pcrs" { - (true, MAX_TPM_PCR_INDEX) - } else { - (false, 0) - }; - - if is_pcrs { - if let CborValue::Map(pcr_map) = v { - let mut pcrs = BTreeMap::new(); - for (pk, pv) in pcr_map { - if let CborValue::Integer(idx) = pk { - if let CborValue::Bytes(val) = pv { - let idx_i128: i128 = (*idx).into(); - - // Validate PCR index bounds - if idx_i128 < 0 { - return Err(VerifyError::PcrIndexOutOfBounds( - format!("Negative PCR index: {}", idx_i128) - )); - } - if idx_i128 > max_index as i128 { - return Err(VerifyError::PcrIndexOutOfBounds( - format!("PCR index {} exceeds maximum {}", idx_i128, max_index) - )); - } - - pcrs.insert(idx_i128 as u8, hex::encode(val)); - } - } - } - return Ok(pcrs); - } - } - } - } - Err(VerifyError::CborParse("Missing pcrs or nitrotpm_pcrs field".into())) -} - -/// Verify COSE Sign1 signature -fn verify_cose_signature( - cose: &CoseSign1, - public_key: &[u8], - payload: &[u8], -) -> Result<(), VerifyError> { - // Nitro uses ES384 (ECDSA with P-384 and SHA-384) - // Build the Sig_structure for COSE_Sign1: - // Sig_structure = [ - // context : "Signature1", - // body_protected : protected, - // external_aad : bstr, - // payload : bstr - // ] - - // Get the protected header bytes using coset's serialization - let protected = cose.protected.clone().to_vec() - .map_err(|e| VerifyError::CoseVerify(format!("Failed to serialize protected header: {}", e)))?; - - let sig_structure = CborValue::Array(vec![ - CborValue::Text("Signature1".to_string()), - CborValue::Bytes(protected), - CborValue::Bytes(vec![]), // external_aad - CborValue::Bytes(payload.to_vec()), - ]); - - let mut sig_structure_bytes = Vec::new(); - ciborium::into_writer(&sig_structure, &mut sig_structure_bytes) - .map_err(|e| VerifyError::CoseVerify(format!("Failed to encode Sig_structure: {}", e)))?; - - // Hash the Sig_structure - let digest = Sha384::digest(&sig_structure_bytes); - - // Parse the public key - let verifying_key = P384VerifyingKey::from_sec1_bytes(public_key) - .map_err(|e| VerifyError::CoseVerify(format!("Invalid P-384 key: {}", e)))?; - - // Parse the signature (raw r||s format for COSE, not DER) - let sig_bytes = &cose.signature; - if sig_bytes.len() != 96 { - return Err(VerifyError::CoseVerify(format!( - "Invalid ES384 signature length: expected 96, got {}", - sig_bytes.len() - ))); - } - - // Convert raw r||s to DER format for the ecdsa crate - let signature = P384Signature::from_slice(sig_bytes) - .map_err(|e| VerifyError::CoseVerify(format!("Invalid signature: {}", e)))?; - - verifying_key - .verify_prehash(&digest, &signature) - .map_err(|e| VerifyError::CoseVerify(format!("Signature verification failed: {}", e))) -} - -#[cfg(test)] -mod tests { - use super::*; - - // === CBOR Field Extraction Tests === - - fn make_test_map() -> Vec<(CborValue, CborValue)> { - vec![ - (CborValue::Text("module_id".to_string()), CborValue::Text("test-module".to_string())), - (CborValue::Text("timestamp".to_string()), CborValue::Integer(1234567890.into())), - (CborValue::Text("digest".to_string()), CborValue::Text("SHA384".to_string())), - (CborValue::Text("pcrs".to_string()), CborValue::Map(vec![ - (CborValue::Integer(0.into()), CborValue::Bytes(vec![0x00; 48])), - (CborValue::Integer(1.into()), CborValue::Bytes(vec![0x01; 48])), - ])), - (CborValue::Text("certificate".to_string()), CborValue::Bytes(vec![0x30, 0x00])), - (CborValue::Text("cabundle".to_string()), CborValue::Array(vec![])), - ] - } - - #[test] - fn test_extract_cbor_text_valid() { - let map = make_test_map(); - let result = extract_cbor_text(&map, "module_id"); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "test-module"); - } - - #[test] - fn test_extract_cbor_text_missing() { - let map = make_test_map(); - let result = extract_cbor_text(&map, "nonexistent"); - assert!(matches!(result, Err(VerifyError::CborParse(_)))); - } - - #[test] - fn test_extract_cbor_text_wrong_type() { - let map = vec![ - (CborValue::Text("wrong".to_string()), CborValue::Integer(123.into())), - ]; - let result = extract_cbor_text(&map, "wrong"); - assert!(matches!(result, Err(VerifyError::CborParse(_)))); - } - - #[test] - fn test_extract_cbor_integer_valid() { - let map = make_test_map(); - let result = extract_cbor_integer(&map, "timestamp"); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 1234567890); - } - - #[test] - fn test_extract_cbor_integer_missing() { - let map = make_test_map(); - let result = extract_cbor_integer(&map, "nonexistent"); - assert!(matches!(result, Err(VerifyError::CborParse(_)))); - } - - #[test] - fn test_extract_cbor_bytes_valid() { - let map = make_test_map(); - let result = extract_cbor_bytes(&map, "certificate"); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), vec![0x30, 0x00]); - } - - #[test] - fn test_extract_cbor_bytes_missing() { - let map = make_test_map(); - let result = extract_cbor_bytes(&map, "nonexistent"); - assert!(matches!(result, Err(VerifyError::CborParse(_)))); - } - - #[test] - fn test_extract_cbor_bytes_optional_present() { - let map = vec![ - (CborValue::Text("data".to_string()), CborValue::Bytes(vec![1, 2, 3])), - ]; - let result = extract_cbor_bytes_optional(&map, "data"); - assert_eq!(result, Some(vec![1, 2, 3])); - } - - #[test] - fn test_extract_cbor_bytes_optional_null() { - let map = vec![ - (CborValue::Text("data".to_string()), CborValue::Null), - ]; - let result = extract_cbor_bytes_optional(&map, "data"); - assert_eq!(result, None); - } - - #[test] - fn test_extract_cbor_bytes_optional_missing() { - let map: Vec<(CborValue, CborValue)> = vec![]; - let result = extract_cbor_bytes_optional(&map, "data"); - assert_eq!(result, None); - } - - #[test] - fn test_extract_cbor_pcrs_valid() { - let map = make_test_map(); - let result = extract_cbor_pcrs(&map); - assert!(result.is_ok()); - let pcrs = result.unwrap(); - assert_eq!(pcrs.len(), 2); - assert!(pcrs.contains_key(&0)); - assert!(pcrs.contains_key(&1)); - } - - #[test] - fn test_extract_cbor_pcrs_missing() { - let map: Vec<(CborValue, CborValue)> = vec![]; - let result = extract_cbor_pcrs(&map); - assert!(matches!(result, Err(VerifyError::CborParse(_)))); - } - - // === verify_nitro_attestation Input Validation Tests === - - #[test] - fn test_reject_invalid_hex() { - let result = verify_nitro_attestation("not valid hex!!!", None, None); - assert!(matches!(result, Err(VerifyError::HexDecode(_)))); - } - - #[test] - fn test_reject_empty_document() { - let result = verify_nitro_attestation("", None, None); - // Empty string decodes to empty bytes, which fails COSE parsing - assert!(result.is_err()); - } - - #[test] - fn test_reject_truncated_cbor() { - // Valid hex but truncated CBOR - let result = verify_nitro_attestation("d28443", None, None); - assert!(matches!(result, Err(VerifyError::CoseVerify(_)))); - } - - #[test] - fn test_reject_non_cose_cbor() { - // Valid CBOR but not a COSE Sign1 (just an integer) - let mut buf = Vec::new(); - ciborium::into_writer(&CborValue::Integer(42.into()), &mut buf).unwrap(); - let hex_str = hex::encode(&buf); - - let result = verify_nitro_attestation(&hex_str, None, None); - assert!(matches!(result, Err(VerifyError::CoseVerify(_)))); - } - - #[test] - fn test_reject_wrong_cose_tag() { - // CBOR with a different tag (not COSE Sign1's 18) - let buf = vec![ - 0xd8, 0x63, // Tag 99 (not 18) - 0x80, // Empty array - ]; - let hex_str = hex::encode(&buf); - - let result = verify_nitro_attestation(&hex_str, None, None); - assert!(matches!(result, Err(VerifyError::CoseVerify(_)))); - } - - // === Signature Length Validation === - - #[test] - fn test_signature_length_check() { - // Directly test the signature length check in verify_cose_signature - // by checking that wrong-length signatures are rejected - - // Test that signatures with wrong length are rejected - // Expected length for ES384 is 96 bytes (48 for R + 48 for S) - let wrong_lengths = [0, 48, 64, 95, 97, 128]; - - for len in wrong_lengths { - let sig = vec![0u8; len]; - // Simulate what verify_cose_signature checks - if sig.len() != 96 { - // This is the check we're testing - assert!(true, "Length {} correctly identified as invalid", len); - } - } - - // Correct length should pass the check (but would fail signature verification) - let sig = vec![0u8; 96]; - assert_eq!(sig.len(), 96); - } - - // === Nonce Validation === - - #[test] - fn test_nonce_validation_matches() { - // When nonce is present and matches, no error from nonce check - // This tests the nonce comparison logic - let expected = b"test-nonce"; - let actual = hex::encode(expected); - - // Simulate the check in verify_nitro_attestation - let nonce_bytes = hex::decode(&actual).unwrap(); - assert_eq!(nonce_bytes, expected); - } - - #[test] - fn test_nonce_validation_mismatch() { - let expected = b"expected-nonce"; - let actual = b"different-nonce"; - - // These should not match - assert_ne!(expected.as_slice(), actual.as_slice()); - } - - // === PCR Index Bounds Tests === - - #[test] - fn test_pcr_index_valid_range() { - // Valid PCR indices: 0-15 - let map = vec![ - (CborValue::Text("pcrs".to_string()), CborValue::Map(vec![ - (CborValue::Integer(0.into()), CborValue::Bytes(vec![0x00; 48])), - (CborValue::Integer(15.into()), CborValue::Bytes(vec![0x0f; 48])), - ])), - ]; - let result = extract_cbor_pcrs(&map); - assert!(result.is_ok()); - let pcrs = result.unwrap(); - assert!(pcrs.contains_key(&0)); - assert!(pcrs.contains_key(&15)); - } - - #[test] - fn test_pcr_index_too_large() { - // PCR index 16 should be rejected - let map = vec![ - (CborValue::Text("pcrs".to_string()), CborValue::Map(vec![ - (CborValue::Integer(16.into()), CborValue::Bytes(vec![0x00; 48])), - ])), - ]; - let result = extract_cbor_pcrs(&map); - assert!(matches!(result, Err(VerifyError::PcrIndexOutOfBounds(_)))); - } - - #[test] - fn test_pcr_index_very_large() { - // Very large PCR index should be rejected - let map = vec![ - (CborValue::Text("pcrs".to_string()), CborValue::Map(vec![ - (CborValue::Integer(255.into()), CborValue::Bytes(vec![0x00; 48])), - ])), - ]; - let result = extract_cbor_pcrs(&map); - assert!(matches!(result, Err(VerifyError::PcrIndexOutOfBounds(_)))); - } - - #[test] - fn test_pcr_index_negative() { - // Negative PCR index should be rejected - let map = vec![ - (CborValue::Text("pcrs".to_string()), CborValue::Map(vec![ - (CborValue::Integer((-1).into()), CborValue::Bytes(vec![0x00; 48])), - ])), - ]; - let result = extract_cbor_pcrs(&map); - assert!(matches!(result, Err(VerifyError::PcrIndexOutOfBounds(_)))); - } - - // === Malicious Integer Tests === - - #[test] - fn test_reject_negative_timestamp() { - let map = vec![ - (CborValue::Text("timestamp".to_string()), CborValue::Integer((-1i64).into())), - ]; - let result = extract_cbor_integer(&map, "timestamp"); - assert!(matches!(result, Err(VerifyError::CborParse(_))), - "Should reject negative timestamp, got: {:?}", result); - } - - #[test] - fn test_accept_valid_timestamp() { - let map = vec![ - (CborValue::Text("timestamp".to_string()), CborValue::Integer(1234567890i64.into())), - ]; - let result = extract_cbor_integer(&map, "timestamp"); - assert_eq!(result.unwrap(), 1234567890); - } - - #[test] - fn test_accept_max_i64_timestamp() { - // i64::MAX is valid and fits in u64 - let map = vec![ - (CborValue::Text("timestamp".to_string()), CborValue::Integer(i64::MAX.into())), - ]; - let result = extract_cbor_integer(&map, "timestamp"); - assert_eq!(result.unwrap(), i64::MAX as u64); - } -} diff --git a/crates/rawdogtpm2-verify/src/tpm.rs b/crates/rawdogtpm2-verify/src/tpm.rs deleted file mode 100644 index bf031ed..0000000 --- a/crates/rawdogtpm2-verify/src/tpm.rs +++ /dev/null @@ -1,824 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! TPM attestation parsing and verification - -use std::collections::BTreeMap; - -use ecdsa::signature::hazmat::PrehashVerifier; -use p256::ecdsa::{Signature as P256Signature, VerifyingKey as P256VerifyingKey}; -use serde::Serialize; -use sha2::{Digest, Sha256}; - -use pki_types::UnixTime; - -use crate::error::VerifyError; -use crate::x509::{extract_public_key, parse_and_validate_cert_chain, parse_cert_chain_pem}; - -// Import from rawdogtpm2 (single source of truth) -use rawdogtpm2::{PcrOps, Tpm, TpmAlg}; - -/// Result of successful TPM attestation verification -/// -/// This struct is only returned when verification succeeds. -/// Verification checks: -/// 1. EK public key from attestation matches EK certificate's public key -/// 2. AK signature over nonce is valid -/// 3. Certificate chain validates to root CA -#[derive(Debug, Serialize)] -pub struct TpmVerifyResult { - /// The nonce that was signed (hex-encoded) - pub nonce: String, - /// SHA-256 hash of the root CA's public key (hex string) - pub root_pubkey_hash: String, -} - -/// Verify ECDSA-SHA256 signature over a message -pub fn verify_ecdsa_p256( - message: &[u8], - signature_der: &[u8], - public_key: &[u8], -) -> Result<(), VerifyError> { - // Parse the public key (SEC1/SECG format: 0x04 || X || Y for uncompressed) - let verifying_key = P256VerifyingKey::from_sec1_bytes(public_key) - .map_err(|e| VerifyError::SignatureInvalid(format!("Invalid public key: {}", e)))?; - - // Parse the DER-encoded signature - let signature = P256Signature::from_der(signature_der) - .map_err(|e| VerifyError::SignatureInvalid(format!("Invalid signature DER: {}", e)))?; - - // TPM signs the SHA-256 hash of the message - let digest = Sha256::digest(message); - - verifying_key - .verify_prehash(&digest, &signature) - .map_err(|e| VerifyError::SignatureInvalid(format!("Signature verification failed: {}", e))) -} - -/// Verify TPM attestation -/// -/// This verification approach works with TCG standard EKs (decrypt-only, cannot sign). -/// It verifies: -/// 1. The EK certificate chain validates to a root CA -/// 2. The EK public key from the attestation output matches the certificate's EK public key -/// 3. The AK's signature over the nonce is valid -/// -/// # Arguments -/// * `nonce_hex` - The nonce/attest_data as hex string -/// * `signature_hex` - DER-encoded ECDSA signature as hex string (from AK) -/// * `ak_pubkey_x_hex` - AK public key X coordinate (hex) -/// * `ak_pubkey_y_hex` - AK public key Y coordinate (hex) -/// * `ek_pubkey_x_hex` - EK public key X coordinate from attestation output (hex) -/// * `ek_pubkey_y_hex` - EK public key Y coordinate from attestation output (hex) -/// * `ek_certs_pem` - EK certificate chain in PEM format -/// -/// # Returns -/// Verification result with nonce and root public key hash. -/// Returns an error if signature, chain validation, or EK pubkey matching fails. -pub fn verify_tpm_attestation( - nonce_hex: &str, - signature_hex: &str, - ak_pubkey_x_hex: &str, - ak_pubkey_y_hex: &str, - ek_pubkey_x_hex: &str, - ek_pubkey_y_hex: &str, - ek_certs_pem: &str, -) -> Result { - // Decode hex inputs - let nonce = hex::decode(nonce_hex)?; - let signature = hex::decode(signature_hex)?; - let ak_x = hex::decode(ak_pubkey_x_hex)?; - let ak_y = hex::decode(ak_pubkey_y_hex)?; - let ek_x = hex::decode(ek_pubkey_x_hex)?; - let ek_y = hex::decode(ek_pubkey_y_hex)?; - - // Construct AK public key in SEC1 uncompressed format: 0x04 || X || Y - let mut ak_pubkey = vec![0x04]; - ak_pubkey.extend(&ak_x); - ak_pubkey.extend(&ak_y); - - // Construct EK public key in SEC1 uncompressed format: 0x04 || X || Y - let mut ek_pubkey = vec![0x04]; - ek_pubkey.extend(&ek_x); - ek_pubkey.extend(&ek_y); - - // Parse the certificate chain to extract leaf cert's public key - let chain = parse_cert_chain_pem(ek_certs_pem)?; - - // Extract EK public key from the leaf certificate - let cert_ek_pubkey = extract_public_key(&chain[0])?; - - // Compare EK public key from attestation output with certificate's EK public key - if ek_pubkey != cert_ek_pubkey { - return Err(VerifyError::SignatureInvalid( - "EK public key from attestation does not match certificate's EK public key".into() - )); - } - - // Verify the AK's signature over the nonce - // Note: The AK signs SHA-256(nonce), not the raw nonce - verify_ecdsa_p256(&nonce, &signature, &ak_pubkey)?; - - // Validate the certificate chain and get root's public key hash - // This uses webpki for signature and date validation - let chain_result = parse_and_validate_cert_chain(ek_certs_pem, UnixTime::now())?; - let root_pubkey_hash = chain_result.root_pubkey_hash; - - Ok(TpmVerifyResult { - nonce: nonce_hex.to_string(), - root_pubkey_hash, - }) -} - -/// Verify TPM signature only (without EK certificate chain validation) -/// -/// This is used in the Nitro path where trust comes from the Nitro attestation -/// binding the TPM signing key. No EK certificate validation is needed. -/// -/// # Arguments -/// * `nonce_hex` - The nonce/attest_data as hex string -/// * `signature_hex` - DER-encoded ECDSA signature as hex string (from AK) -/// * `ak_pubkey_x_hex` - AK public key X coordinate (hex) -/// * `ak_pubkey_y_hex` - AK public key Y coordinate (hex) -/// -/// # Returns -/// The verified nonce (hex-encoded) if signature is valid. -pub fn verify_tpm_signature_only( - nonce_hex: &str, - signature_hex: &str, - ak_pubkey_x_hex: &str, - ak_pubkey_y_hex: &str, -) -> Result { - // Decode hex inputs - let nonce = hex::decode(nonce_hex)?; - let signature = hex::decode(signature_hex)?; - let ak_x = hex::decode(ak_pubkey_x_hex)?; - let ak_y = hex::decode(ak_pubkey_y_hex)?; - - // Construct AK public key in SEC1 uncompressed format: 0x04 || X || Y - let mut ak_pubkey = vec![0x04]; - ak_pubkey.extend(&ak_x); - ak_pubkey.extend(&ak_y); - - // Verify the AK's signature over the nonce - verify_ecdsa_p256(&nonce, &signature, &ak_pubkey)?; - - Ok(nonce_hex.to_string()) -} - -/// Calculate the expected PCR policy digest from PCR values -/// -/// This calculates the TPM2 PolicyPCR digest that would be used as an -/// authPolicy for a key bound to the given PCR values. -/// -/// Uses the same implementation as rawdogtpm2 to ensure consistency. -/// -/// # Arguments -/// * `pcrs` - Map of PCR index to hex-encoded PCR value -/// * `pcr_alg` - The hash algorithm of the PCR bank (determines expected PCR size) -/// -/// # Returns -/// The expected policy digest as a hex-encoded string -/// -/// # Example -/// ```ignore -/// let mut pcrs = BTreeMap::new(); -/// pcrs.insert(0, "0000...".to_string()); // 64 hex chars for SHA-256 -/// pcrs.insert(1, "0000...".to_string()); -/// let policy = calculate_pcr_policy(&pcrs, TpmAlg::Sha256)?; -/// ``` -pub fn calculate_pcr_policy(pcrs: &BTreeMap, pcr_alg: TpmAlg) -> Result { - if pcrs.is_empty() { - return Err(VerifyError::InvalidAttest("No PCR values provided".into())); - } - - // Determine expected PCR size based on algorithm - let expected_size = match pcr_alg { - TpmAlg::Sha256 => 32, - TpmAlg::Sha384 => 48, - _ => return Err(VerifyError::InvalidAttest(format!( - "Unsupported PCR algorithm: {:?}", - pcr_alg - ))), - }; - - // Validate and convert PCR values from hex strings to bytes - // PCRs must be in sorted order (BTreeMap guarantees this) - let mut pcr_values: Vec<(u8, Vec)> = Vec::with_capacity(pcrs.len()); - for (&idx, value_hex) in pcrs.iter() { - if idx > 23 { - return Err(VerifyError::InvalidAttest(format!( - "PCR index {} out of range (max 23)", - idx - ))); - } - let value_bytes = hex::decode(value_hex)?; - if value_bytes.len() != expected_size { - return Err(VerifyError::InvalidAttest(format!( - "PCR {} has invalid length for {:?}: expected {} bytes, got {}", - idx, pcr_alg, expected_size, value_bytes.len() - ))); - } - pcr_values.push((idx, value_bytes)); - } - - // Use rawdogtpm2's implementation (single source of truth) - let policy_digest = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg) - .map_err(|e| VerifyError::InvalidAttest(format!("PCR policy calculation failed: {}", e)))?; - - Ok(hex::encode(policy_digest)) -} - -/// Verify that a policy digest matches the expected PCR values -/// -/// This is useful for verifying that an AK's authPolicy (if known) matches -/// the PCR values reported in an attestation. -/// -/// # Arguments -/// * `expected_policy_hex` - The expected policy digest (hex string) -/// * `pcrs` - The PCR values to verify against -/// * `pcr_alg` - The hash algorithm of the PCR bank -/// -/// # Returns -/// Ok(()) if the policy matches, error otherwise -pub fn verify_pcr_policy( - expected_policy_hex: &str, - pcrs: &BTreeMap, - pcr_alg: TpmAlg, -) -> Result<(), VerifyError> { - let calculated_policy = calculate_pcr_policy(pcrs, pcr_alg)?; - - if calculated_policy != expected_policy_hex { - return Err(VerifyError::InvalidAttest(format!( - "PCR policy mismatch: expected {}, calculated {}", - expected_policy_hex, calculated_policy - ))); - } - - Ok(()) -} - -// ============================================================================= -// TPM2B_ATTEST parsing and NIZK verification -// ============================================================================= - -/// TPM_GENERATED magic value (0xff544347 = "ÿTCG") -const TPM_GENERATED_VALUE: u32 = 0xff544347; - -/// TPM_ST_ATTEST_CERTIFY structure type -const TPM_ST_ATTEST_CERTIFY: u16 = 0x8017; - -/// Size of TPMS_CLOCK_INFO structure: clock(8) + resetCount(4) + restartCount(4) + safe(1) -const TPMS_CLOCK_INFO_SIZE: usize = 17; - -/// Parsed TPMS_ATTEST structure (from TPM2_Certify) -#[derive(Debug)] -pub struct TpmAttestInfo { - /// Nonce/qualifying data from extraData field (raw bytes) - pub nonce: Vec, - /// Name of the certified object (nameAlg || H(public_area)) - pub certified_name: Vec, - /// Name of the signing key - pub signer_name: Vec, -} - -/// Parse TPM2B_ATTEST structure (CERTIFY type) -/// -/// TPM2B_ATTEST contains a TPMS_ATTEST structure which includes: -/// - magic: 0xff544347 (TPM_GENERATED_VALUE) -/// - type: 0x8017 (TPM_ST_ATTEST_CERTIFY) -/// - qualifiedSigner: TPM2B_NAME -/// - extraData: TPM2B_DATA (our nonce) -/// - clockInfo: TPMS_CLOCK_INFO -/// - firmwareVersion: u64 -/// - attested.certify.name: TPM2B_NAME (certified object's name) -/// - attested.certify.qualifiedName: TPM2B_NAME -pub fn parse_tpm2b_attest(data: &[u8]) -> Result { - // Use a cursor to track position with overflow-safe arithmetic - let mut cursor = SafeCursor::new(data); - - // magic (4 bytes) - let magic_bytes = cursor.read_bytes(4, "magic")?; - let magic = u32::from_be_bytes(magic_bytes.try_into().unwrap()); - if magic != TPM_GENERATED_VALUE { - return Err(VerifyError::InvalidAttest(format!( - "Invalid TPM magic: expected 0x{:08x}, got 0x{:08x}", - TPM_GENERATED_VALUE, magic - ))); - } - - // type (2 bytes) - let type_bytes = cursor.read_bytes(2, "type")?; - let attest_type = u16::from_be_bytes(type_bytes.try_into().unwrap()); - if attest_type != TPM_ST_ATTEST_CERTIFY { - return Err(VerifyError::InvalidAttest(format!( - "Invalid attest type: expected 0x{:04x} (CERTIFY), got 0x{:04x}", - TPM_ST_ATTEST_CERTIFY, attest_type - ))); - } - - // qualifiedSigner (TPM2B_NAME) - let signer_name = cursor.read_tpm2b("qualifiedSigner")?; - - // extraData (TPM2B_DATA) - this is our nonce - let nonce = cursor.read_tpm2b("extraData")?; - - // clockInfo (TPMS_CLOCK_INFO) - skip it - cursor.skip(TPMS_CLOCK_INFO_SIZE, "clockInfo")?; - - // firmwareVersion (8 bytes) - skip it - cursor.skip(8, "firmwareVersion")?; - - // attested (TPMS_CERTIFY_INFO) - // - name (TPM2B_NAME) - let certified_name = cursor.read_tpm2b("certifiedName")?; - - Ok(TpmAttestInfo { - nonce, - certified_name, - signer_name, - }) -} - -/// Safe cursor for parsing binary data with overflow protection -struct SafeCursor<'a> { - data: &'a [u8], - offset: usize, -} - -impl<'a> SafeCursor<'a> { - fn new(data: &'a [u8]) -> Self { - Self { data, offset: 0 } - } - - /// Read exactly `len` bytes, returning error on overflow or truncation - fn read_bytes(&mut self, len: usize, field: &str) -> Result<&'a [u8], VerifyError> { - let end = self.offset.checked_add(len).ok_or_else(|| { - VerifyError::InvalidAttest(format!("Integer overflow reading {}", field)) - })?; - if end > self.data.len() { - return Err(VerifyError::InvalidAttest(format!("Truncated {}", field))); - } - let bytes = &self.data[self.offset..end]; - self.offset = end; - Ok(bytes) - } - - /// Skip exactly `len` bytes - fn skip(&mut self, len: usize, field: &str) -> Result<(), VerifyError> { - let end = self.offset.checked_add(len).ok_or_else(|| { - VerifyError::InvalidAttest(format!("Integer overflow skipping {}", field)) - })?; - if end > self.data.len() { - return Err(VerifyError::InvalidAttest(format!("Truncated {}", field))); - } - self.offset = end; - Ok(()) - } - - /// Read a TPM2B structure (2-byte size prefix + data) - fn read_tpm2b(&mut self, field: &str) -> Result, VerifyError> { - let size_bytes = self.read_bytes(2, field)?; - let size = u16::from_be_bytes(size_bytes.try_into().unwrap()) as usize; - let data = self.read_bytes(size, field)?; - Ok(data.to_vec()) - } -} - - -#[cfg(test)] -mod tests { - use super::*; - use ecdsa::signature::hazmat::PrehashSigner; - use p256::ecdsa::SigningKey; - use rawdogtpm2::TpmAlg; - use sha2::Sha256; - - /// Generate a test P-256 key pair and sign a message - /// The signature is over SHA256(message) to match what verify_ecdsa_p256 expects - fn sign_message(message: &[u8]) -> (Vec, Vec) { - // Use a fixed seed for deterministic tests - let secret_bytes: [u8; 32] = [ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, - 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, - ]; - let signing_key = SigningKey::from_bytes(&secret_bytes.into()).unwrap(); - - // Get the public key in SEC1 uncompressed format - let verifying_key = signing_key.verifying_key(); - let pubkey = verifying_key.to_encoded_point(false); - let pubkey_bytes = pubkey.as_bytes().to_vec(); - - // verify_ecdsa_p256 does: digest = SHA256(message), then verify_prehash - // So we need to sign_prehash over SHA256(message) - let digest = Sha256::digest(message); - let signature: p256::ecdsa::Signature = signing_key.sign_prehash(&digest).unwrap(); - let sig_der = signature.to_der().as_bytes().to_vec(); - - (pubkey_bytes, sig_der) - } - - // === ECDSA Verification Tests === - - #[test] - fn test_valid_p256_signature() { - let message = b"test message for signing"; - let (pubkey, signature) = sign_message(message); - - let result = verify_ecdsa_p256(message, &signature, &pubkey); - assert!(result.is_ok(), "Valid signature should verify: {:?}", result); - } - - #[test] - fn test_reject_wrong_message() { - let message = b"test message for signing"; - let wrong_message = b"different message"; - let (pubkey, signature) = sign_message(message); - - let result = verify_ecdsa_p256(wrong_message, &signature, &pubkey); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_invalid_pubkey_not_on_curve() { - let message = b"test message"; - let (_, signature) = sign_message(message); - - // Invalid public key: 0x04 prefix + arbitrary X, Y that's not on curve - let mut invalid_pubkey = vec![0x04]; - invalid_pubkey.extend([0x00u8; 32]); // X = 0 - invalid_pubkey.extend([0x01u8; 32]); // Y = 1 (not on curve) - - let result = verify_ecdsa_p256(message, &signature, &invalid_pubkey); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_identity_point() { - let message = b"test message"; - let (_, signature) = sign_message(message); - - // Identity point: X = 0, Y = 0 (invalid for P-256) - let mut identity = vec![0x04]; - identity.extend([0x00u8; 32]); // X = 0 - identity.extend([0x00u8; 32]); // Y = 0 - - let result = verify_ecdsa_p256(message, &signature, &identity); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_wrong_size_pubkey_too_short() { - let message = b"test message"; - let (_, signature) = sign_message(message); - - // Public key that's too short - let short_pubkey = vec![0x04, 0x01, 0x02, 0x03]; - - let result = verify_ecdsa_p256(message, &signature, &short_pubkey); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_wrong_size_pubkey_too_long() { - let message = b"test message"; - let (_, signature) = sign_message(message); - - // Public key that's too long - let mut long_pubkey = vec![0x04]; - long_pubkey.extend([0x01u8; 100]); - - let result = verify_ecdsa_p256(message, &signature, &long_pubkey); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_compressed_pubkey() { - let message = b"test message"; - let (_, signature) = sign_message(message); - - // Compressed public key (0x02 or 0x03 prefix) - only 33 bytes - let mut compressed = vec![0x02]; - compressed.extend([0x01u8; 32]); - - // This might work or fail depending on library support - // The important thing is it doesn't panic - let _ = verify_ecdsa_p256(message, &signature, &compressed); - } - - #[test] - fn test_reject_malformed_der_signature() { - let message = b"test message"; - let (pubkey, _) = sign_message(message); - - // Completely invalid DER - let invalid_sig = vec![0x00, 0x01, 0x02, 0x03]; - - let result = verify_ecdsa_p256(message, &invalid_sig, &pubkey); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_truncated_der_signature() { - let message = b"test message"; - let (pubkey, signature) = sign_message(message); - - // Truncate the signature - let truncated = &signature[..signature.len() / 2]; - - let result = verify_ecdsa_p256(message, truncated, &pubkey); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_empty_signature() { - let message = b"test message"; - let (pubkey, _) = sign_message(message); - - let result = verify_ecdsa_p256(message, &[], &pubkey); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_empty_pubkey() { - let message = b"test message"; - let (_, signature) = sign_message(message); - - let result = verify_ecdsa_p256(message, &signature, &[]); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_reject_wrong_pubkey() { - let message = b"test message"; - let (_, signature) = sign_message(message); - - // Generate a different valid key - let other_secret: [u8; 32] = [ - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, - 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, - 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, - ]; - let other_key = SigningKey::from_bytes(&other_secret.into()).unwrap(); - let other_pubkey = other_key.verifying_key().to_encoded_point(false); - - let result = verify_ecdsa_p256(message, &signature, other_pubkey.as_bytes()); - assert!(matches!(result, Err(VerifyError::SignatureInvalid(_)))); - } - - #[test] - fn test_empty_message() { - let message = b""; - let (pubkey, signature) = sign_message(message); - - // Empty message should still work - let result = verify_ecdsa_p256(message, &signature, &pubkey); - assert!(result.is_ok()); - } - - #[test] - fn test_large_message() { - // Test with a large message - let message = vec![0xABu8; 10000]; - let (pubkey, signature) = sign_message(&message); - - let result = verify_ecdsa_p256(&message, &signature, &pubkey); - assert!(result.is_ok()); - } - - // === PCR Policy Calculation Tests === - - #[test] - fn test_calculate_pcr_policy_single_pcr() { - // Test with a single PCR (all zeros) - let mut pcrs = BTreeMap::new(); - let pcr0 = "0".repeat(64); // 32 bytes of zeros as hex - pcrs.insert(0, pcr0); - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(result.is_ok()); - - let policy = result.unwrap(); - // Policy should be a 64-character hex string (32 bytes) - assert_eq!(policy.len(), 64); - } - - #[test] - fn test_calculate_pcr_policy_multiple_pcrs() { - // Test with PCRs 0, 1, 2 - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - pcrs.insert(1, "1".repeat(64)); // All 0x11... - pcrs.insert(2, "2".repeat(64)); // All 0x22... - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(result.is_ok()); - } - - #[test] - fn test_calculate_pcr_policy_non_contiguous() { - // Test with non-contiguous PCRs (0, 7, 15) - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - pcrs.insert(7, "7".repeat(64)); - pcrs.insert(15, "f".repeat(64)); - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(result.is_ok()); - } - - #[test] - fn test_calculate_pcr_policy_deterministic() { - // Same input should produce same output - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - - let policy1 = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap(); - let policy2 = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap(); - - assert_eq!(policy1, policy2); - } - - #[test] - fn test_calculate_pcr_policy_different_values() { - // Different PCR values should produce different policies - let mut pcrs1 = BTreeMap::new(); - pcrs1.insert(0, "0".repeat(64)); - - let mut pcrs2 = BTreeMap::new(); - pcrs2.insert(0, "1".repeat(64)); - - let policy1 = calculate_pcr_policy(&pcrs1, TpmAlg::Sha256).unwrap(); - let policy2 = calculate_pcr_policy(&pcrs2, TpmAlg::Sha256).unwrap(); - - assert_ne!(policy1, policy2); - } - - #[test] - fn test_calculate_pcr_policy_empty() { - let pcrs: BTreeMap = BTreeMap::new(); - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_calculate_pcr_policy_invalid_index() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(24, "0".repeat(64)); // Index 24 is invalid (max 23) - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_calculate_pcr_policy_invalid_length() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(32)); // Only 16 bytes, need 32 - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_calculate_pcr_policy_invalid_hex() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "gg".repeat(32)); // Invalid hex - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::HexDecode(_)))); - } - - #[test] - fn test_verify_pcr_policy_match() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - - let expected = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap(); - let result = verify_pcr_policy(&expected, &pcrs, TpmAlg::Sha256); - assert!(result.is_ok()); - } - - #[test] - fn test_verify_pcr_policy_mismatch() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - - // Wrong expected policy - let wrong_expected = "f".repeat(64); - let result = verify_pcr_policy(&wrong_expected, &pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - // === SHA-384 PCR Policy Tests === - - #[test] - fn test_calculate_pcr_policy_sha384() { - // Test with SHA-384 PCRs (48 bytes = 96 hex chars) - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(96)); - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha384); - assert!(result.is_ok()); - - let policy = result.unwrap(); - // Policy is always SHA-256 (32 bytes = 64 hex chars) - assert_eq!(policy.len(), 64); - } - - #[test] - fn test_calculate_pcr_policy_sha384_wrong_size() { - // SHA-384 expects 48 bytes, not 32 - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); // 32 bytes - wrong for SHA-384 - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha384); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_calculate_pcr_policy_different_alg_different_policy() { - // Same PCR value but different algorithms should produce different policies - // because the algorithm ID is encoded in the PCR selection structure - let mut pcrs256 = BTreeMap::new(); - pcrs256.insert(0, "0".repeat(64)); // 32 bytes for SHA-256 - - let mut pcrs384 = BTreeMap::new(); - pcrs384.insert(0, "0".repeat(96)); // 48 bytes for SHA-384 (different zeros count) - - let policy256 = calculate_pcr_policy(&pcrs256, TpmAlg::Sha256).unwrap(); - let policy384 = calculate_pcr_policy(&pcrs384, TpmAlg::Sha384).unwrap(); - - // Policies should differ due to algorithm ID in selection structure - assert_ne!(policy256, policy384); - } - - // === Malicious Input Tests for parse_tpm2b_attest === - - #[test] - fn test_attest_empty_input() { - let result = parse_tpm2b_attest(&[]); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_attest_truncated_magic() { - // Only 2 bytes when magic needs 4 - let result = parse_tpm2b_attest(&[0xff, 0x54]); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_attest_wrong_magic() { - // Valid length but wrong magic value - let mut data = vec![0x00, 0x00, 0x00, 0x00]; // Wrong magic - data.extend(&[0x80, 0x17]); // Correct type - let result = parse_tpm2b_attest(&data); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_attest_huge_signer_size() { - // Craft input with valid magic/type but huge signer size - let mut data = vec![]; - data.extend(&0xff544347u32.to_be_bytes()); // magic - data.extend(&0x8017u16.to_be_bytes()); // type - data.extend(&0xffffu16.to_be_bytes()); // signer size = 65535 (way too big) - - let result = parse_tpm2b_attest(&data); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_))), - "Should reject huge signer size, got: {:?}", result); - } - - #[test] - fn test_attest_huge_extra_size() { - // Craft input with valid magic/type, small signer, but huge extra size - let mut data = vec![]; - data.extend(&0xff544347u32.to_be_bytes()); // magic - data.extend(&0x8017u16.to_be_bytes()); // type - data.extend(&0x0000u16.to_be_bytes()); // signer size = 0 - data.extend(&0xffffu16.to_be_bytes()); // extra size = 65535 (way too big) - - let result = parse_tpm2b_attest(&data); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_))), - "Should reject huge extra size, got: {:?}", result); - } - - #[test] - fn test_attest_truncated_after_sizes() { - // Valid header but truncated before clockInfo - let mut data = vec![]; - data.extend(&0xff544347u32.to_be_bytes()); // magic - data.extend(&0x8017u16.to_be_bytes()); // type - data.extend(&0x0002u16.to_be_bytes()); // signer size = 2 - data.extend(&[0x00, 0x0b]); // signer (2 bytes) - data.extend(&0x0004u16.to_be_bytes()); // extra size = 4 - data.extend(&[0x01, 0x02, 0x03, 0x04]); // extra (4 bytes) - // Missing: clockInfo, firmwareVersion, certifiedName - - let result = parse_tpm2b_attest(&data); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_))), - "Should reject truncated input, got: {:?}", result); - } -} diff --git a/crates/rawdogtpm2-verify/src/x509.rs b/crates/rawdogtpm2-verify/src/x509.rs deleted file mode 100644 index 8d133af..0000000 --- a/crates/rawdogtpm2-verify/src/x509.rs +++ /dev/null @@ -1,532 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! X.509 certificate handling using rustls-webpki for chain validation - -use base64::{engine::general_purpose::STANDARD, Engine as _}; -use der::{Decode, Encode}; -use ecdsa::signature::Verifier; -use p256::ecdsa::{Signature as P256Signature, VerifyingKey as P256VerifyingKey}; -use p384::ecdsa::{Signature as P384Signature, VerifyingKey as P384VerifyingKey}; -use pki_types::{CertificateDer, UnixTime}; -use webpki::{anchor_from_trusted_cert, EndEntityCert, KeyUsage}; -use sha2::{Digest, Sha256}; -use x509_cert::Certificate; - -use crate::error::VerifyError; - -/// Maximum allowed certificate chain depth (to prevent DoS) -pub const MAX_CHAIN_DEPTH: usize = 10; - -/// PEM certificate begin marker -const PEM_CERT_BEGIN: &str = "-----BEGIN CERTIFICATE-----"; -/// PEM certificate end marker -const PEM_CERT_END: &str = "-----END CERTIFICATE-----"; - -/// Parse X.509 certificate chain from PEM format -/// -/// Returns certificates in order from the PEM file (typically leaf first, root last). -/// -/// This parser is strict about: -/// - Exact BEGIN/END markers (not just "contains") -/// - No non-whitespace data between certificates -/// - Valid base64 content within certificate blocks -pub fn parse_cert_chain_pem(pem: &str) -> Result, VerifyError> { - let mut certs = Vec::new(); - let mut current_cert = String::new(); - let mut in_cert = false; - let mut line_number = 0; - - for line in pem.lines() { - line_number += 1; - let trimmed = line.trim(); - - // Check for BEGIN marker - if trimmed == PEM_CERT_BEGIN { - if in_cert { - return Err(VerifyError::CertificateParse(format!( - "Line {}: Unexpected BEGIN marker inside certificate block", - line_number - ))); - } - in_cert = true; - current_cert.clear(); - continue; - } - - // Check for END marker - if trimmed == PEM_CERT_END { - if !in_cert { - return Err(VerifyError::CertificateParse(format!( - "Line {}: END marker without matching BEGIN", - line_number - ))); - } - in_cert = false; - - // Decode the certificate - if current_cert.is_empty() { - return Err(VerifyError::CertificateParse(format!( - "Line {}: Empty certificate content", - line_number - ))); - } - - let der_bytes = base64_decode(¤t_cert)?; - let cert = Certificate::from_der(&der_bytes) - .map_err(|e| VerifyError::CertificateParse(format!( - "Line {}: Invalid DER: {}", - line_number, e - )))?; - certs.push(cert); - continue; - } - - // Inside a certificate block: accumulate base64 content - if in_cert { - // Validate that line contains only base64 characters - if !trimmed.is_empty() && !is_valid_base64_line(trimmed) { - return Err(VerifyError::CertificateParse(format!( - "Line {}: Invalid base64 character in certificate", - line_number - ))); - } - current_cert.push_str(trimmed); - continue; - } - - // Outside certificate blocks: only whitespace is allowed - if !trimmed.is_empty() { - return Err(VerifyError::CertificateParse(format!( - "Line {}: Unexpected content outside certificate block: '{}'", - line_number, - if trimmed.len() > 20 { &trimmed[..20] } else { trimmed } - ))); - } - } - - // Check for unclosed certificate block - if in_cert { - return Err(VerifyError::CertificateParse( - "Unclosed certificate block (missing END marker)".into(), - )); - } - - if certs.is_empty() { - return Err(VerifyError::CertificateParse( - "No certificates found in PEM".into(), - )); - } - - Ok(certs) -} - -/// Check if a string contains only valid base64 characters -fn is_valid_base64_line(s: &str) -> bool { - s.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=') -} - -/// Decode base64 string -fn base64_decode(input: &str) -> Result, VerifyError> { - STANDARD - .decode(input) - .map_err(|e| VerifyError::CertificateParse(format!("Invalid base64: {}", e))) -} - -/// Extract raw public key bytes from an X.509 certificate -/// -/// Returns the SubjectPublicKeyInfo's bit string contents -pub fn extract_public_key(cert: &Certificate) -> Result, VerifyError> { - let spki = &cert.tbs_certificate.subject_public_key_info; - let pubkey_bits = spki.subject_public_key.as_bytes().ok_or_else(|| { - VerifyError::CertificateParse("Public key has unused bits".into()) - })?; - Ok(pubkey_bits.to_vec()) -} - -/// Compute SHA-256 hash of public key and return as hex string -pub fn hash_public_key(pubkey_bytes: &[u8]) -> String { - let digest = Sha256::digest(pubkey_bytes); - hex::encode(digest) -} - -/// Result of certificate chain validation -#[derive(Debug)] -pub struct ChainValidationResult { - /// SHA-256 hash of the root CA's public key (hex string) - pub root_pubkey_hash: String, -} - -/// Validate a certificate chain using webpki -/// -/// Chain should be leaf-first, root-last. Time must be provided by caller. -pub fn validate_cert_chain( - chain: &[Certificate], - time: UnixTime, -) -> Result { - if chain.is_empty() { - return Err(VerifyError::ChainValidation("Empty certificate chain".into())); - } - if chain.len() > MAX_CHAIN_DEPTH { - return Err(VerifyError::ChainValidation(format!( - "Certificate chain too deep: {} certificates (max {})", - chain.len(), - MAX_CHAIN_DEPTH - ))); - } - - // Get signature verification algorithms from rustls-rustcrypto - let sig_algs = rustls_rustcrypto::provider() - .signature_verification_algorithms - .all; - - // Convert to DER format for webpki - let cert_ders: Vec = chain - .iter() - .map(|c| { - let der = c.to_der().map_err(|e| { - VerifyError::CertificateParse(format!("Failed to encode cert to DER: {}", e)) - })?; - Ok(CertificateDer::from(der)) - }) - .collect::, VerifyError>>()?; - - // Root is last in chain - create trust anchor from it - let root_der = &cert_ders[cert_ders.len() - 1]; - let trust_anchor = anchor_from_trusted_cert(root_der) - .map_err(|e| VerifyError::ChainValidation(format!("Invalid root certificate: {:?}", e)))?; - - // Leaf is first - let ee_cert = EndEntityCert::try_from(&cert_ders[0]) - .map_err(|e| VerifyError::CertificateParse(format!("Invalid leaf certificate: {:?}", e)))?; - - // Intermediates are everything between leaf and root - let intermediates: Vec = if cert_ders.len() > 2 { - cert_ders[1..cert_ders.len() - 1].to_vec() - } else { - Vec::new() - }; - - // Use webpki to verify the chain - ee_cert - .verify_for_usage( - sig_algs, - &[trust_anchor], - &intermediates, - time, - KeyUsage::client_auth(), - None, // no revocation checking - None, // no custom path verification - ) - .map_err(|e| VerifyError::ChainValidation(format!("Chain validation failed: {:?}", e)))?; - - // Extract and hash root's public key - let root_pubkey = extract_public_key(&chain[chain.len() - 1])?; - let root_hash = hash_public_key(&root_pubkey); - - Ok(ChainValidationResult { - root_pubkey_hash: root_hash, - }) -} - -/// Parse PEM and validate certificate chain -/// -/// Convenience wrapper that parses PEM then validates. -/// Chain should be leaf-first, root-last in the PEM. -pub fn parse_and_validate_cert_chain( - chain_pem: &str, - time: UnixTime, -) -> Result { - let certs = parse_cert_chain_pem(chain_pem)?; - validate_cert_chain(&certs, time) -} - -/// Validate TPM EK certificate chain -/// -/// Similar to validate_cert_chain but without Extended Key Usage (EKU) checking. -/// TPM EK certificates use TPM-specific EKU OID (2.23.133.8.1) which is not -/// recognized by webpki's standard EKU validation. -/// -/// This function validates: -/// - Each certificate is signed by the next in chain -/// - Certificate validity periods include the specified time -/// - Chain is not too deep -/// -/// Chain should be leaf-first, root-last. -pub fn validate_tpm_cert_chain( - chain: &[Certificate], - time: UnixTime, -) -> Result { - if chain.is_empty() { - return Err(VerifyError::ChainValidation("Empty certificate chain".into())); - } - if chain.len() > MAX_CHAIN_DEPTH { - return Err(VerifyError::ChainValidation(format!( - "Certificate chain too deep: {} certificates (max {})", - chain.len(), - MAX_CHAIN_DEPTH - ))); - } - - // Validate each certificate is signed by the next one in chain - for i in 0..chain.len() - 1 { - let cert = &chain[i]; - let issuer = &chain[i + 1]; - - // Get issuer's public key - let issuer_pubkey = extract_public_key(issuer)?; - - // Get the TBS (to be signed) certificate bytes - let tbs_der = cert.tbs_certificate.to_der() - .map_err(|e| VerifyError::ChainValidation(format!("Failed to encode TBS: {}", e)))?; - - // Get the signature - let sig_bytes = cert.signature.raw_bytes(); - - // Determine algorithm and verify - let alg_oid = &cert.signature_algorithm.oid; - - // ECDSA with SHA-256 on P-256: 1.2.840.10045.4.3.2 - // ECDSA with SHA-384 on P-384: 1.2.840.10045.4.3.3 - const ECDSA_SHA256_OID: &str = "1.2.840.10045.4.3.2"; - const ECDSA_SHA384_OID: &str = "1.2.840.10045.4.3.3"; - - let alg_str = alg_oid.to_string(); - match alg_str.as_str() { - ECDSA_SHA256_OID => { - // P-256 verification - if issuer_pubkey.len() != 65 || issuer_pubkey[0] != 0x04 { - return Err(VerifyError::ChainValidation( - "Invalid issuer public key format for P-256".into() - )); - } - let verifying_key = P256VerifyingKey::from_sec1_bytes(&issuer_pubkey) - .map_err(|e| VerifyError::ChainValidation(format!("Invalid P-256 key: {}", e)))?; - - let signature = P256Signature::from_der(sig_bytes) - .map_err(|e| VerifyError::ChainValidation(format!("Invalid P-256 signature: {}", e)))?; - - verifying_key.verify(&tbs_der, &signature) - .map_err(|_| VerifyError::ChainValidation( - format!("Certificate {} signature verification failed", i) - ))?; - } - ECDSA_SHA384_OID => { - // P-384 verification - if issuer_pubkey.len() != 97 || issuer_pubkey[0] != 0x04 { - return Err(VerifyError::ChainValidation( - "Invalid issuer public key format for P-384".into() - )); - } - let verifying_key = P384VerifyingKey::from_sec1_bytes(&issuer_pubkey) - .map_err(|e| VerifyError::ChainValidation(format!("Invalid P-384 key: {}", e)))?; - - let signature = P384Signature::from_der(sig_bytes) - .map_err(|e| VerifyError::ChainValidation(format!("Invalid P-384 signature: {}", e)))?; - - verifying_key.verify(&tbs_der, &signature) - .map_err(|_| VerifyError::ChainValidation( - format!("Certificate {} signature verification failed", i) - ))?; - } - _ => { - return Err(VerifyError::ChainValidation(format!( - "Unsupported signature algorithm: {}", - alg_str - ))); - } - } - } - - // Validate time for each certificate - let unix_secs = time.as_secs(); - - for (i, cert) in chain.iter().enumerate() { - let validity = &cert.tbs_certificate.validity; - - // Convert not_before and not_after to unix timestamps - let not_before = validity.not_before.to_unix_duration().as_secs(); - let not_after = validity.not_after.to_unix_duration().as_secs(); - - if unix_secs < not_before { - return Err(VerifyError::ChainValidation(format!( - "Certificate {} is not yet valid", - i - ))); - } - if unix_secs > not_after { - return Err(VerifyError::ChainValidation(format!( - "Certificate {} has expired", - i - ))); - } - } - - // Extract and hash root's public key - let root_pubkey = extract_public_key(&chain[chain.len() - 1])?; - let root_hash = hash_public_key(&root_pubkey); - - Ok(ChainValidationResult { - root_pubkey_hash: root_hash, - }) -} - -/// Parse PEM and validate TPM certificate chain -/// -/// Convenience wrapper that parses PEM then validates without EKU checking. -/// Chain should be leaf-first, root-last in the PEM. -pub fn parse_and_validate_tpm_cert_chain( - chain_pem: &str, - time: UnixTime, -) -> Result { - let certs = parse_cert_chain_pem(chain_pem)?; - validate_tpm_cert_chain(&certs, time) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_hash_public_key() { - let pubkey = [0x04, 0x01, 0x02, 0x03]; - let hash = hash_public_key(&pubkey); - // SHA-256 of [0x04, 0x01, 0x02, 0x03] - assert_eq!(hash.len(), 64); // 32 bytes = 64 hex chars - } - - #[test] - fn test_base64_decode() { - let input = "SGVsbG8gV29ybGQ="; // "Hello World" - let decoded = STANDARD.decode(input).unwrap(); - assert_eq!(decoded, b"Hello World"); - } - - // === PEM Parsing Edge Cases === - - #[test] - fn test_reject_no_certificates() { - let pem = "This is not a PEM file at all"; - let result = parse_cert_chain_pem(pem); - assert!(matches!(result, Err(VerifyError::CertificateParse(_)))); - } - - #[test] - fn test_reject_empty_pem() { - let pem = ""; - let result = parse_cert_chain_pem(pem); - assert!(matches!(result, Err(VerifyError::CertificateParse(_)))); - } - - #[test] - fn test_reject_missing_end_marker() { - let pem = "-----BEGIN CERTIFICATE-----\nMIIB"; - let result = parse_cert_chain_pem(pem); - // Should fail because no END marker means no certificate is completed - assert!(matches!(result, Err(VerifyError::CertificateParse(_)))); - } - - #[test] - fn test_reject_empty_certificate_content() { - let pem = "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"; - let result = parse_cert_chain_pem(pem); - // Empty base64 content should fail DER parsing - assert!(matches!(result, Err(VerifyError::CertificateParse(_)))); - } - - #[test] - fn test_reject_invalid_base64() { - let pem = "-----BEGIN CERTIFICATE-----\n\ - !!!invalid base64!!!\n\ - -----END CERTIFICATE-----"; - let result = parse_cert_chain_pem(pem); - assert!(matches!(result, Err(VerifyError::CertificateParse(_)))); - } - - #[test] - fn test_reject_truncated_base64() { - // Valid base64 prefix but incomplete (missing padding) - let pem = "-----BEGIN CERTIFICATE-----\n\ - MIIB\n\ - -----END CERTIFICATE-----"; - let result = parse_cert_chain_pem(pem); - // Should fail either in base64 decode or DER parse - assert!(result.is_err()); - } - - #[test] - fn test_reject_invalid_der() { - // Valid base64, but not valid DER certificate - let pem = "-----BEGIN CERTIFICATE-----\n\ - SGVsbG8gV29ybGQ=\n\ - -----END CERTIFICATE-----"; - let result = parse_cert_chain_pem(pem); - assert!(matches!(result, Err(VerifyError::CertificateParse(_)))); - } - - // === Strict PEM Parsing Tests === - - #[test] - fn test_reject_garbage_between_markers() { - // Non-whitespace data outside certificate blocks should be rejected - // This tests that the parser rejects garbage BEFORE any cert - let pem = "garbage data here\n\ - -----BEGIN CERTIFICATE-----\n\ - SGVsbG8=\n\ - -----END CERTIFICATE-----"; - let result = parse_cert_chain_pem(pem); - assert!(matches!(result, Err(VerifyError::CertificateParse(ref msg)) if msg.contains("Unexpected content"))); - } - - #[test] - fn test_allow_whitespace_between_certs() { - // Whitespace between certificates should be allowed (though certs themselves are invalid DER) - let pem = "-----BEGIN CERTIFICATE-----\n\ - SGVsbG8=\n\ - -----END CERTIFICATE-----\n\ - \n\ - \n\ - -----BEGIN CERTIFICATE-----\n\ - V29ybGQ=\n\ - -----END CERTIFICATE-----"; - let result = parse_cert_chain_pem(pem); - // Will fail on invalid DER, not on parsing - assert!(matches!(result, Err(VerifyError::CertificateParse(ref msg)) if msg.contains("DER"))); - } - - #[test] - fn test_reject_nested_begin_marker() { - // Nested BEGIN marker should be rejected - let pem = "-----BEGIN CERTIFICATE-----\n\ - SGVsbG8=\n\ - -----BEGIN CERTIFICATE-----\n\ - V29ybGQ=\n\ - -----END CERTIFICATE-----"; - let result = parse_cert_chain_pem(pem); - assert!(matches!(result, Err(VerifyError::CertificateParse(ref msg)) if msg.contains("Unexpected BEGIN"))); - } - - #[test] - fn test_reject_end_without_begin() { - // END marker without BEGIN should be rejected - let pem = "-----END CERTIFICATE-----"; - let result = parse_cert_chain_pem(pem); - assert!(matches!(result, Err(VerifyError::CertificateParse(ref msg)) if msg.contains("without matching BEGIN"))); - } - - #[test] - fn test_reject_unclosed_block() { - // Unclosed certificate block should be rejected - let pem = "-----BEGIN CERTIFICATE-----\n\ - SGVsbG8=\n"; - let result = parse_cert_chain_pem(pem); - assert!(matches!(result, Err(VerifyError::CertificateParse(ref msg)) if msg.contains("missing END"))); - } - - #[test] - fn test_is_valid_base64_line() { - assert!(is_valid_base64_line("ABCDabcd0123+/==")); - assert!(!is_valid_base64_line("ABC!@#")); - assert!(!is_valid_base64_line("ABC DEF")); // space not allowed - assert!(is_valid_base64_line("")); // empty is valid - } -} diff --git a/crates/rawdogtpm2-verify/test-nitro-fixture.json b/crates/rawdogtpm2-verify/test-nitro-fixture.json deleted file mode 100644 index b4fbd49..0000000 --- a/crates/rawdogtpm2-verify/test-nitro-fixture.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "ek_certificates": {}, - "pcrs": { - "sha384": { - "0": "6e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec", - "1": "7e10323dec25050c22d2e91709373c930d194be275bde5c5e01331b9cc6ef81a9e2586991636652605bc3d322dbc7109", - "2": "518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4", - "3": "518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4", - "4": "1a1f166e1995c1e1bfdf1c1d13b1ebc9d5d4e44397501655c911a2062d92063483ca9e17f0264efe336d4b61c5b98d1e", - "5": "b3ef9dfcbc8be38f0104cea80711252de3f4f8bf19a3f27126d303fb08ec979ba90780f06a742bf66f444f62e99cc1fc", - "6": "518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4", - "7": "98441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35", - "8": "056a862312c282d635a1e486812191efbf58c8c5533a89c3579b1809854bfa0090854e04b82acd93a752cbd32f1f38c1", - "9": "4d5b0773776a7520553b4c08189632a76fe83024fa462035c649a0f62cd82f3951132c386488fd87d875ebf27552156d", - "10": "c1b534d2b043b50ae0fc0d415c4881d92f1633e6b318f967f8adb201eff718d080004fa7ce98b50f1bd8a808332fa1e2", - "11": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "12": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "13": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "14": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "15": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "16": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "17": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "18": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "19": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "20": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "21": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "22": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "23": "22a3e580d7d0931f4365ebfae2618b84e3cce2c0afbf0ea1fbe40e645a43a3771ceb3e188394803642266f5512ee0eac" - } - }, - "ek_public_keys": { - "ecc_p256": { - "x": "499109ce4bb49e2f0c85944fc79925c2a8d15c3362db9ee823eb16588e29e47e", - "y": "4b985e434557f4cd0e604e4a0ed013907c037010c856695569de45cd403b4791" - } - }, - "signing_key_public_keys": { - "ecc_p256": { - "x": "2417c2d23945e9e68c78c730102f951bf41f5042464d8272cd18e32d4b8ccfb5", - "y": "d4c3d4b93e361f7d8093f1f83c2396ad932a5c6bd238575e0de758a8c3a6ddc2" - } - }, - "attestation": { - "tpm": { - "ecc_p256": { - "nonce": "31373639393233313833", - "attest_data": "ff54434780170022000b149bb4c9c56850e3571bed5c4b6ed5e8736678d44d4564f9e2d9d400f7bff5ac000a313736393932333138330000000000240cb3e5738876f090bf840108bffe09fb566b9f0022000bc4a66f8f2b42b7d3d214802efab53fdf840024754cff2d4448657bf84f56d0520022000b149bb4c9c56850e3571bed5c4b6ed5e8736678d44d4564f9e2d9d400f7bff5ac", - "signature": "3045022100ba599d6291aa1eb117ecfc4ba17283096c3f3303b8b96be81c1659c595c96bcd022054b80456ccac86c6885820f294ff331d49dd68fe09143d349007ce446b728d29" - } - }, - "nitro": { - "public_key": "042417c2d23945e9e68c78c730102f951bf41f5042464d8272cd18e32d4b8ccfb5d4c3d4b93e361f7d8093f1f83c2396ad932a5c6bd238575e0de758a8c3a6ddc2", - "nonce": "31373639393233313833", - "document": "8444a1013822a05912e2bf696d6f64756c655f69647827692d30333061316632333366383334383639302d74706d3030303030303030303030303030303066646967657374665348413338346974696d657374616d701b0000019c17a4830f6d6e6974726f74706d5f70637273b8180058306e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec0158307e10323dec25050c22d2e91709373c930d194be275bde5c5e01331b9cc6ef81a9e2586991636652605bc3d322dbc7109025830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4035830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c40458301a1f166e1995c1e1bfdf1c1d13b1ebc9d5d4e44397501655c911a2062d92063483ca9e17f0264efe336d4b61c5b98d1e055830b3ef9dfcbc8be38f0104cea80711252de3f4f8bf19a3f27126d303fb08ec979ba90780f06a742bf66f444f62e99cc1fc065830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c407583098441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35085830056a862312c282d635a1e486812191efbf58c8c5533a89c3579b1809854bfa0090854e04b82acd93a752cbd32f1f38c10958304d5b0773776a7520553b4c08189632a76fe83024fa462035c649a0f62cd82f3951132c386488fd87d875ebf27552156d0a5830c1b534d2b043b50ae0fc0d415c4881d92f1633e6b318f967f8adb201eff718d080004fa7ce98b50f1bd8a808332fa1e20b58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f5830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000105830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff125830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff135830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff145830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff155830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff165830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff17583022a3e580d7d0931f4365ebfae2618b84e3cce2c0afbf0ea1fbe40e645a43a3771ceb3e188394803642266f5512ee0eac6b63657274696669636174655902733082026f308201f5a0030201020204697ed9f7300a06082a8648ce3d04030330818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30333061316632333366383334383639302e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230313034343333325a170d3236303230313037343333355a308193310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753313e303c06035504030c35692d30333061316632333366383334383639302d74706d303030303030303030303030303030302e75732d656173742d312e6177733076301006072a8648ce3d020106052b8104002203620004777cac8372ab736a2043f52369f0bdcc532e5e282c3695bfbeab6db877798b22bce1f8fd961c0c6ebad4bb6f6d09fabe2c13bf9020c52a70c10c6b07a761331cf90bd77f095cba83d682b2905ab58dca582e7cc21bc01cb4f9bf3fbd3efaf1e7a31d301b300c0603551d130101ff04023000300b0603551d0f0404030206c0300a06082a8648ce3d0403030368003065023100a32fefa84e976c7728110621dddb3d933ddbc47b598db144825daa4de7ffbbd2521846bdade93383e6b4d9fc2b81d512023014ebb021eb6888d1d440a2dde4479bb5d310e685137b272e96a376f5952fd0e98843117a84ed197938b8c1f985e73a4268636162756e646c65845902153082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff65902c1308202bd30820244a0030201020210578aa8f46a2b9e82304ad9173550c111300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3236303133303131303734365a170d3236303231393132303734365a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004325b2791b548f694a7e17d44d65a326ed1659e23da8972a0acff960f820e548b1b0fb443927dcae15e27a815355ca62ab0e590a2b353a67495b6e10a949d02dddb0e8d8e6ec82efcfaab167c3517fad31de4add54e54e7869966b5a106a85604a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e04160414f309d394ba73bc082274c84cfaa28d8c12ce95e7300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030367003064023076291fdcc1db1eff47f6ab41074f946e8c97f6045ec959cc119a577b8f5b02fdeaaff3ccc22559d8095f4cfd75d18afa02300d2cdee4fc10d95ef64217b06a45824958482666fedf04e30a433c7155b9e0108fa43b77a8c4750430798122ed8523ea590319308203153082029aa00302010202100d3d3e6da2d9da68bf96ed11663a0f35300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230313031323235395a170d3236303230363231323235385a308189313c303a06035504030c33343863666233333166346137616362612e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b810400220362000458da1984ae792b97312a98fd03ff8db0d930b5d4be21541f749e4ef96e841433cc82dbe43745e3dd46461b4ceedbb2b47932ed734bc17e6745359e54193d5f93d7a1dba4f1e3009335828fa847942f89ec22a6dd60f00c2041ef7044884f5e60a381ea3081e730120603551d130101ff040830060101ff020101301f0603551d23041830168014f309d394ba73bc082274c84cfaa28d8c12ce95e7301d0603551d0e041604146ac22d82f5ed4065cbc35a114a1e635e06ea2907300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f32613533313133362d383232332d346633632d396335632d3335333832643564663634632e63726c300a06082a8648ce3d0403030369003066023100fad27828efde63ae193f5c8e60abd7c98b0ce3c662e3ba33687bba971f4b9986596052d2f95926787243c62e8f518b790231009da96f7c7ba61dea6acf137327762efc41843e5a4d140963af8d91e852baa04819714491effee90fd081d6803c710c915902c3308202bf30820245a0030201020215008567cee73e22cb55f083d4d4f881ff3313c7e2b1300a06082a8648ce3d040303308189313c303a06035504030c33343863666233333166346137616362612e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3236303230313034343031395a170d3236303230323034343031395a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30333061316632333366383334383639302e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004562f892fd88ffc2c4bc40bdbfecc211949e1ece2007d7ebc9e535141fad0fafb7c9569de8aaf44e7a02af6687da8d5cbb971f7025a0c2aeb5d6e88835a7dec9f2b231dce3d00492bebd8337aeb89c190a684085fb7c0da808176ad972828659ca366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e04160414fdea49e532ddc19976817c8e1b5ffcec437e283f301f0603551d230418301680146ac22d82f5ed4065cbc35a114a1e635e06ea2907300a06082a8648ce3d040303036800306502301d1d14a339eadd7624b1c492300d9a6f1c365b1351718ffcbffd19be21e07b41c2762f560e9fcbe6f33c2669493395a60231009abafe346e9e177cb8bb760126d1d86455f4755af9eeaee0e9f666b2aa2980a836f67bcb75b8663593385981f3f3bd2b6a7075626c69635f6b65795841042417c2d23945e9e68c78c730102f951bf41f5042464d8272cd18e32d4b8ccfb5d4c3d4b93e361f7d8093f1f83c2396ad932a5c6bd238575e0de758a8c3a6ddc269757365725f64617461f6656e6f6e63654a31373639393233313833ff58609c3e3c2a983cd0cc8699922808812043abd958ae697df07b115a77d19c355cf5b71eb58b3a71a0047ec53f854a8d323d98270751953380ddecd150c44f41ab53ffc99898601352bf9ac71a698e9cb181fcfd7d8d6f256ba001cf91cab5b7762b" - } - } -} diff --git a/crates/rawdogtpm2/.gitignore b/crates/rawdogtpm2/.gitignore deleted file mode 100644 index 3367afd..0000000 --- a/crates/rawdogtpm2/.gitignore +++ /dev/null @@ -1 +0,0 @@ -old diff --git a/crates/rawdogtpm2/AWS-NITRO.md b/crates/rawdogtpm2/AWS-NITRO.md deleted file mode 100644 index 574f4dc..0000000 --- a/crates/rawdogtpm2/AWS-NITRO.md +++ /dev/null @@ -1,203 +0,0 @@ -# AWS Nitro TPM Attestation - -This document describes how TPM attestation works on AWS Nitro Enclaves and EC2 instances with NitroTPM. - -## Overview - -AWS Nitro provides two attestation mechanisms that work together: - -1. **Nitro Security Module (NSM)** - Hardware-based attestation signed by AWS -2. **NitroTPM** - A vTPM with PCR values signed in the Nitro document - -By combining these, we get a chain of trust from AWS hardware to arbitrary application data. - -## Chain of Trust - -```mermaid -flowchart TD - A["AWS Nitro Root CA
Verification returns root pubkey hash"] - B["Nitro Attestation Document
COSE Sign1 structure containing:
• nitrotpm_pcrs: SHA-384 PCR values
• public_key: Application key binding
• nonce: Freshness proof
• Certificate chain to AWS root"] - C["TPM Attestation Key (AK)
• ECC P-256 signing key
• authPolicy = PolicyPCR(SHA-384 bank)
• Can only sign when PCRs match"] - D["TPM2B_ATTEST
• extraData: nonce (matches Nitro)
• certifiedName: includes authPolicy
• Proves: AK bound to PCR values"] - - A -->|"Signs (ECDSA P-384)"| B - B -->|"public_key field binds"| C - C -->|"Signs (ECDSA P-256)"| D -``` - -## Why SHA-384 PCRs? - -The Nitro document signs `nitrotpm_pcrs` which contains **SHA-384** PCR values. For a coherent chain of trust: - -- The AK's `authPolicy` references the SHA-384 PCR bank -- Verification computes policy from SHA-384 PCRs -- The signed Nitro PCRs match what the AK is bound to - -This ensures a single, verifiable path from AWS hardware to the attested data. - -## Attestation Flow - -### Generation (on Nitro instance) - -```rust -// 1. Detect Nitro and choose PCR bank -let is_nitro = tpm.is_nitro_tpm()?; -let pcr_alg = if is_nitro { TpmAlg::Sha384 } else { TpmAlg::Sha256 }; - -// 2. Read PCR values from chosen bank -let pcr_values = tpm.read_pcrs(pcr_alg)?; - -// 3. Compute PCR policy digest -let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg)?; - -// 4. Create AK bound to this policy -let ak = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?; - -// 5. AK self-certifies (proves it exists with this policy) -let certify_result = tpm.certify(ak.handle, ak.handle, &nonce)?; - -// 6. Get Nitro attestation binding the AK public key -let nitro_doc = tpm.nsm_attest( - None, // user_data - Some(nonce.to_vec()), // nonce (same as TPM) - Some(ak_public_key_secg), // public_key (binds AK) -)?; -``` - -### Verification (anywhere) - -```rust -// 1. Parse attestation output -let output: AttestationOutput = serde_json::from_str(&json)?; - -// 2. Verify Nitro document (COSE signature, cert chain) -let nitro_result = verify_nitro_attestation(&nitro_doc)?; - -// 3. Verify AK public key matches Nitro's public_key binding -assert!(ak_pubkey == nitro_result.document.public_key); - -// 4. Verify TPM signature over TPM2B_ATTEST -verify_ecdsa_p256(&attest_data, &signature, &ak_pubkey)?; - -// 5. Compute expected AK name from PCR policy -let policy = calculate_pcr_policy(&sha384_pcrs, TpmAlg::Sha384)?; -let expected_name = compute_ecc_p256_name(&ak_x, &ak_y, &policy); - -// 6. Verify certified name matches (proves PCR binding) -assert!(attest_info.certified_name == expected_name); - -// 7. Verify nonces match (proves freshness and binding) -assert!(tpm_nonce == nitro_nonce); - -// 8. Verify PCR values match signed Nitro document -assert!(output.pcrs["sha384"] == nitro_result.document.pcrs); -``` - -## Security Properties - -### What This Proves - -1. **Hardware Root of Trust** - The Nitro document is signed by AWS hardware (certificate chain to AWS root CA) - -2. **PCR Integrity** - The SHA-384 PCR values in the attestation match what AWS hardware measured - -3. **Key Binding** - The AK is bound to specific PCR values via `authPolicy` (it cannot sign unless PCRs match) - -4. **Freshness** - The nonce in both TPM and Nitro attestations proves the attestation is fresh - -5. **AK Authenticity** - The Nitro document's `public_key` field proves the AK belongs to this Nitro instance - -### What This Does NOT Prove - -1. **EK Certificate** - We don't verify EK certificates in the Nitro path (unnecessary - Nitro document provides stronger binding) - -2. **PCR Meaning** - The attestation proves PCR *values*, not what they *mean*. You need to understand what software produces which PCR measurements. - -3. **Enclave vs Instance** - This works for both Nitro Enclaves and regular EC2 with NitroTPM. The PCR values differ based on what's running. - -## Nitro Document Structure - -The Nitro attestation document is a COSE Sign1 structure: - -``` -COSE_Sign1 = [ - protected: { alg: ES384 }, - unprotected: {}, - payload: { - module_id: "i-xxx-tpm00000000000000", - digest: "SHA384", - timestamp: 1234567890, - nitrotpm_pcrs: { - 0: <48 bytes>, - 1: <48 bytes>, - ... - }, - certificate: , - cabundle: [], - public_key: <65 bytes, SECG uncompressed>, - user_data: , - nonce: , - }, - signature: <96 bytes, ECDSA P-384> -] -``` - -## PCR Values on NitroTPM - -| PCR | Description | -|-----|-------------| -| 0 | Firmware/BIOS measurements | -| 1 | Platform configuration | -| 2-3 | Option ROMs | -| 4 | Boot loader | -| 5 | Boot loader configuration | -| 6 | Resume events | -| 7 | Secure Boot state | -| 8-9 | Kernel and initrd | -| 10 | IMA (Integrity Measurement Architecture) | -| 11-16 | Application-defined | -| 17-22 | Reserved (0xFF...) | -| 23 | Application-defined | - -## Example Output - -```json -{ - "pcrs": { - "sha384": { - "0": "6e901b16932f6e036747d7a57696e4a2...", - "1": "7e10323dec25050c22d2e91709373c93...", - ... - } - }, - "signing_key_public_keys": { - "ecc_p256": { - "x": "3678325466f129d8279056737fe48378...", - "y": "9384bc5fafdc7938f9a51e09490a5555..." - } - }, - "attestation": { - "tpm": { - "ecc_p256": { - "nonce": "31373639393231303439", - "attest_data": "ff544347801700...", - "signature": "304402207c8fb54ca1603b3a..." - } - }, - "nitro": { - "public_key": "043678325466f129d827...", - "nonce": "31373639393231303439", - "document": "8444a1013822a059..." - } - } -} -``` - -Note: Only SHA-384 PCRs are included because that's the bank bound to the AK and signed in the Nitro document. - -## References - -- [AWS Nitro Enclaves Attestation](https://docs.aws.amazon.com/enclaves/latest/user/verify-root.html) -- [NitroTPM Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nitrotpm.html) -- [TPM 2.0 Library Specification](https://trustedcomputinggroup.org/resource/tpm-library-specification/) -- [COSE (RFC 8152)](https://datatracker.ietf.org/doc/html/rfc8152) diff --git a/crates/rawdogtpm2/CLAUDE.md b/crates/rawdogtpm2/CLAUDE.md deleted file mode 100644 index 1e2f325..0000000 --- a/crates/rawdogtpm2/CLAUDE.md +++ /dev/null @@ -1,30 +0,0 @@ -# rawdogtpm2 - -Minimal TPM 2.0 protocol implementation in pure Rust without C dependencies. - -## Build - -Debug build: -```bash -cargo build -``` - -For deployment, we target static musl binaries: -```bash -cargo build --target x86_64-unknown-linux-musl -``` - -## Architecture - -- Direct communication with TPM via /dev/tpmrm0 (resource manager) or /dev/tpm0 (direct) -- No tpm2-tss or other C library dependencies -- Implements TPM 2.0 command/response protocol per TCG specification - -## Key Modules - -- `credential.rs` - Policy session operations and TPM object name computation -- `ek.rs` - EK and signing key operations -- `pcr.rs` - PCR read/extend operations -- `a9n.rs` - Attestation generation -- `nv.rs` - NV RAM read/write operations -- `nsm.rs` - AWS Nitro-specific vendor commands diff --git a/crates/rawdogtpm2/Cargo.toml b/crates/rawdogtpm2/Cargo.toml deleted file mode 100644 index 65b1293..0000000 --- a/crates/rawdogtpm2/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "rawdogtpm2" -version = "0.1.0" -edition = "2021" -description = "Minimal TPM 2.0 protocol implementation without C dependencies" -license.workspace = true - -[dependencies] -anyhow = { workspace = true } -thiserror = { workspace = true } -sha1 = { workspace = true } -sha2 = { workspace = true } -hmac = { workspace = true } -base64 = { workspace = true } -serde = { workspace = true } -ciborium = { workspace = true } -serde_bytes = { workspace = true } -hex = { workspace = true } -serde_json = { workspace = true } - -[lib] -name = "rawdogtpm2" -path = "src/lib.rs" diff --git a/crates/rawdogtpm2/README.md b/crates/rawdogtpm2/README.md deleted file mode 100644 index 13b2d9a..0000000 --- a/crates/rawdogtpm2/README.md +++ /dev/null @@ -1,289 +0,0 @@ -# rawdogtpm2 - -A TPM 2.0 client in pure Rust with **zero C dependencies**. - -> "Oh but subsection 2 of the spec says that according to the working group meeting..." - -FTS! Rawdog the TPM, *git'r done!* - ---- - -Most people wrap the crusty C-based `tss2` monolith. Instead, this library implements the bare minimum TPM 2.0 wire-protocol to "Get Shit Done" (GTS), yeeting bytes directly to `/dev/tpm0`: - -- No libtss2, or any C toolchain -- Direct protocol command/response serialization -- Minimal, and focused on the most common operations -- Comprehensive `selftest` binary - -## Features - -### PCR Operations (`PcrOps` trait) - -- Read PCR values from all active banks (SHA-1, SHA-256, SHA-384, SHA-512) -- Extend PCRs with automatic multi-bank support -- Query which banks a PCR is allocated in -- Read all allocated PCRs from the TPM - -### Key Management - -- Create primary ECC P-256 signing keys -- Sign data with TPM keys (ECDSA signatures) -- Create PCR-sealed keys (policy-based authorization) - -### NV RAM Operations (`NvOps` trait) - -- **Read Operations:** - - Read from TPM Non-Volatile storage - - Read NV index public information - - Enumerate all NV indices - - Read Endorsement Key (EK) certificates -- **Write Operations:** - - Define new NV spaces with attributes - - Write data to NV indices - - Undefine (delete) NV spaces - - Find free NV indices - -### Attestation - -- Certify keys using other keys -- Generate attestation structures -- PCR policy digest calculation - -### AWS Nitro Security Module (NSM) Support (`NsmOps` trait) - -- Request attestation documents from AWS Nitro Secure Module -- Includes PCR values, optional user data, nonce, and public key -- Automatic detection of Nitro TPM via vendor string - -### TPM Vendor Detection - -- Query TPM manufacturer and vendor information -- Check if running on AWS Nitro TPM (`is_nitro_tpm()`) - -## Quick Start - -### Basic TPM Operations - -```rust -use rawdogtpm2::{Tpm, PcrOps, TPM_RH_OWNER}; - -fn main() -> anyhow::Result<()> { - // Open the TPM - let mut tpm = Tpm::open()?; - - // Read PCR 0 from all banks - let pcrs = tpm.pcr_read_all_banks(&[0])?; - for (index, alg, value) in pcrs { - println!("PCR {} [{}]: {:02x?}", index, alg.name(), value); - } - - // Extend PCR 23 with data (extends all allocated banks automatically) - tpm.pcr_extend(23, b"measurement data")?; - - // Create a signing key - let key = tpm.create_primary_ecc_key(TPM_RH_OWNER)?; - println!("Key handle: 0x{:08X}", key.handle); - - // Sign some data - let digest = sha256(b"Hello, TPM!"); - let signature = tpm.sign(key.handle, &digest)?; - - // Clean up - tpm.flush_context(key.handle)?; - - Ok(()) -} -``` - -### NV RAM Operations - -```rust -use rawdogtpm2::{Tpm, NvOps, TPM_ALG_SHA256}; -use rawdogtpm2::nv::{NV_INDEX_USER_START, NV_INDEX_USER_END}; -use rawdogtpm2::nv::{TPMA_NV_AUTHWRITE, TPMA_NV_AUTHREAD}; - -fn main() -> anyhow::Result<()> { - let mut tpm = Tpm::open()?; - - // Find a free NV index - let nv_index = tpm.nv_find_free_index(NV_INDEX_USER_START, NV_INDEX_USER_END)?; - - // Define a new NV space (1024 bytes) - let attrs = TPMA_NV_AUTHWRITE | TPMA_NV_AUTHREAD; - tpm.nv_define_space(nv_index, 1024, attrs, TPM_ALG_SHA256)?; - - // Write data - tpm.nv_write(nv_index, b"Hello from NV RAM!")?; - - // Read it back - let data = tpm.nv_read(nv_index)?; - println!("Read: {:?}", String::from_utf8_lossy(&data)); - - // Clean up - tpm.nv_undefine_space(nv_index)?; - - Ok(()) -} -``` - -### AWS Nitro TPM Support - -```rust -use rawdogtpm2::{Tpm, NsmOps}; - -fn main() -> anyhow::Result<()> { - let mut tpm = Tpm::open()?; - - // Check if this is a Nitro TPM - if tpm.is_nitro_tpm()? { - println!("Running on AWS Nitro TPM!"); - - // Get NSM attestation document (requires /dev/tpm0) - let mut tpm_direct = Tpm::open_direct()?; - let doc = tpm_direct.nsm_attest( - Some(b"user data".to_vec()), - Some(b"nonce".to_vec()), - None // Optional: public key - )?; - - println!("Attestation document: {} bytes", doc.len()); - } - - Ok(()) -} -``` - -## Running Tests - -### `selftest` - Comprehensive TPM Test Suite - -The "self test" binary exercises all major functionality: - -```bash -cargo run --bin selftest -``` - -It performs: -1. Query TPM properties (manufacturer, firmware version, etc.) -2. Detect AWS Nitro TPM -3. Query active PCR banks -4. Read all non-zero PCR values -5. Extend PCR 23 and verify the change -6. Create a primary ECC signing key -7. Sign test data -8. Access the Endorsement Key -9. Read EK certificates from NV RAM -10. Enumerate all NV indices -11. Create PCR-sealed keys -12. Certify keys (attestation) - -### `nsmtest` - AWS Nitro Security Module Test - -Test NSM attestation functionality (requires AWS Nitro TPM): - -```bash -cargo run --bin nsmtest -``` - -This requests an attestation document from the Nitro Secure Module and displays the result. - -## Requirements - -- Linux with TPM 2.0 support -- `/dev/tpmrm0` accessible (TPM Resource Manager) - for most operations -- `/dev/tpm0` accessible (Direct TPM access) - for NSM vendor commands -- Rust - -**Note:** Most operations use `/dev/tpmrm0` (TPM Resource Manager), which handles context management automatically. AWS Nitro NSM vendor commands require direct access via `/dev/tpm0` and should use `Tpm::open_direct()`. - -## Technical Details - -### Protocol Implementation - -The library implements the TPM 2.0 command/response protocol as specified in the [TPM 2.0 Library Specification](https://trustedcomputinggroup.org/resource/tpm-library-specification/): - -- Big-endian serialization for all multi-byte values -- TPM2B (size-prefixed buffer) structures -- Command headers with proper sizing -- Response parsing with error handling -- Session-based authorization (password sessions) - -### Supported Commands - -**Standard TPM 2.0 Commands:** -- `TPM2_PCR_Read` - Read PCR values -- `TPM2_PCR_Extend` - Extend PCRs with measurements -- `TPM2_GetCapability` - Query TPM capabilities and properties -- `TPM2_CreatePrimary` - Create primary keys -- `TPM2_Sign` - Sign data with TPM keys -- `TPM2_FlushContext` - Release handles -- `TPM2_NV_Read` - Read NV storage -- `TPM2_NV_ReadPublic` - Get NV index info -- `TPM2_NV_DefineSpace` - Create NV indices -- `TPM2_NV_Write` - Write to NV storage -- `TPM2_NV_UndefineSpace` - Delete NV indices -- `TPM2_PolicyPCR` - PCR policy operations -- `TPM2_Certify` - Key certification/attestation - -**Vendor-Specific Commands:** -- `TPM2_CC_VENDOR_AWS_NSM_REQUEST` (0x20000001) - AWS Nitro Security Module attestation - -### Extension Traits - -The library uses extension traits to organize functionality: - -- **`PcrOps`** - PCR read/extend operations -- **`NvOps`** - NV RAM read/write operations -- **`NsmOps`** - AWS Nitro Security Module operations -- **`EkOps`** - Endorsement Key operations (create standard EK, signing keys) - -Import the traits you need: -```rust -use rawdogtpm2::{Tpm, PcrOps, NvOps, NsmOps, EkOps}; -``` - -### Hash Algorithms - -Supports multiple PCR banks: -- SHA-1 (20 bytes) -- SHA-256 (32 bytes) -- SHA-384 (48 bytes) -- SHA-512 (64 bytes) - -## Architecture - -### Attestation Model - -The library implements a TPM-based attestation model where: - -1. **Endorsement Key (EK)** - A TPM's identity, certified by the manufacturer. Created using the TCG standard template for deterministic key derivation. The EK is decrypt-only (cannot sign). - -2. **Attestation Key (AK)** - A signing key created with a PCR policy. The AK can only be used when PCRs match specific values, cryptographically binding the key to the system state. - -3. **PCR Policy** - A SHA-256 digest computed from PCR values. When an AK is created with an `authPolicy`, it can only sign when `TPM2_PolicyPCR` succeeds with matching values. - -4. **TPM2_Certify** - The AK self-certifies, producing a `TPM2B_ATTEST` structure containing the AK's name (which includes its `authPolicy`). This proves the AK exists and is bound to specific PCR values. - -### Chain of Trust - -```mermaid -flowchart TD - A["Trust Anchor
Nitro Root CA / TPM Manufacturer / Cloud Provider"] - B["Platform Attestation
Nitro Document / EK Certificate / AK Certificate"] - C["Signing Key (AK)
Bound to PCR values via authPolicy"] - D["TPM2B_ATTEST + Signature
Contains: nonce, AK name (proves PCR binding)"] - - A -->|signs| B - B -->|binds| C - C -->|signs| D -``` - -### Platform-Specific Trust - -| Platform | Trust Anchor | AK Binding Method | -|----------|-------------|-------------------| -| AWS Nitro | Nitro Root CA | Nitro document `public_key` field | -| GCP Shielded VM | Google CA | AK certificate (NV 0x01c10000) | -| Azure Trusted Launch | Microsoft CA | AK certificate (NV 0x01C101D0) | - -See [AWS-NITRO.md](./AWS-NITRO.md) for detailed AWS Nitro attestation documentation. diff --git a/crates/rawdogtpm2/src/a9n.rs b/crates/rawdogtpm2/src/a9n.rs deleted file mode 100644 index 4dbb3af..0000000 --- a/crates/rawdogtpm2/src/a9n.rs +++ /dev/null @@ -1,268 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! TPM attestation functionality -//! -//! Provides high-level attestation operations including: -//! - Retrieving EK certificates from NV RAM -//! - Creating and certifying attestation keys (AK) -//! - Reading PCR values -//! - Generating attestation documents - -use anyhow::{anyhow, Context, Result}; -use base64::{engine::general_purpose::STANDARD, Engine as _}; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, BTreeMap}; - -use crate::{Tpm, EkOps, NvOps, PcrOps, NsmOps, TPM_RH_OWNER}; -use crate::{NV_INDEX_RSA_2048_EK_CERT, NV_INDEX_ECC_P256_EK_CERT, NV_INDEX_ECC_P384_EK_CERT}; -use crate::credential::compute_ecc_p256_name; - -/// DER SEQUENCE tag with 2-byte length (0x30 0x82) -/// Used to detect valid X.509 certificates in DER format -const DER_SEQUENCE_LONG: [u8; 2] = [0x30, 0x82]; - -/// Complete attestation output containing all TPM attestation data -#[derive(Debug, Serialize, Deserialize)] -pub struct AttestationOutput { - pub ek_certificates: EkCertificates, - pub pcrs: HashMap>, - pub ek_public_keys: HashMap, - pub signing_key_public_keys: HashMap, - pub attestation: AttestationContainer, -} - -/// Endorsement Key certificates in PEM format -#[derive(Debug, Serialize, Deserialize)] -pub struct EkCertificates { - #[serde(skip_serializing_if = "Option::is_none")] - pub rsa_2048: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ecc_p256: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ecc_p384: Option, -} - -/// ECC public key coordinates -#[derive(Debug, Serialize, Deserialize)] -pub struct EccPublicKeyCoords { - pub x: String, - pub y: String, -} - -/// Container for both TPM and optional Nitro attestations -#[derive(Debug, Serialize, Deserialize)] -pub struct AttestationContainer { - pub tpm: HashMap, - #[serde(skip_serializing_if = "Option::is_none")] - pub nitro: Option, -} - -/// TPM attestation data (certify response with NIZK proof) -#[derive(Debug, Serialize, Deserialize)] -pub struct AttestationData { - /// The nonce/challenge provided to the attestation (hex-encoded) - /// This is duplicated from attest_data.extraData for easy access. - /// Verification MUST check this matches the nonce in attest_data. - pub nonce: String, - /// TPM2B_ATTEST structure from TPM2_Certify (hex-encoded) - pub attest_data: String, - /// ECDSA signature over attest_data (DER, hex-encoded) - pub signature: String, -} - -/// Nitro Enclave attestation data -#[derive(Debug, Serialize, Deserialize)] -pub struct NitroAttestationData { - pub public_key: String, - pub nonce: String, - pub document: String, -} - -/// Convert DER-encoded data to PEM format -pub fn der_to_pem(der: &[u8], label: &str) -> String { - let base64_encoded = STANDARD.encode(der); - let mut pem = format!("-----BEGIN {}-----\n", label); - for chunk in base64_encoded.as_bytes().chunks(64) { - pem.push_str(std::str::from_utf8(chunk).unwrap()); - pem.push('\n'); - } - pem.push_str(&format!("-----END {}-----\n", label)); - pem -} - -/// Generate a complete TPM attestation document -/// -/// This function: -/// 1. Retrieves EK certificates from NV RAM -/// 2. Creates the TCG standard EK (matches certificate public key) -/// 3. Detects platform and chooses PCR bank (SHA-384 for Nitro, SHA-256 otherwise) -/// 4. Reads PCR values from the chosen bank -/// 5. Computes PCR policy and creates a signing key (AK) bound to it -/// 6. AK self-certifies via TPM2_Certify (proves AK exists with this policy) -/// 7. If on AWS Nitro, gets Nitro attestation binding the AK public key -/// -/// # Arguments -/// * `nonce` - User-provided nonce/challenge to include in attestation -/// -/// # Returns -/// JSON-encoded attestation document containing all attestation data -pub fn attest(nonce: &[u8]) -> Result { - let mut tpm = Tpm::open_direct()?; - - // Step 1: Retrieve EK certificates from NV RAM - let mut ek_certs = EkCertificates { - rsa_2048: None, - ecc_p256: None, - ecc_p384: None, - }; - - // Try to read RSA EK cert - if let Ok(cert) = tpm.nv_read(NV_INDEX_RSA_2048_EK_CERT) { - if cert.starts_with(&DER_SEQUENCE_LONG) { - ek_certs.rsa_2048 = Some(der_to_pem(&cert, "CERTIFICATE")); - } - } - - // Try to read ECC P-256 EK cert - if let Ok(cert) = tpm.nv_read(NV_INDEX_ECC_P256_EK_CERT) { - if cert.starts_with(&DER_SEQUENCE_LONG) { - ek_certs.ecc_p256 = Some(der_to_pem(&cert, "CERTIFICATE")); - } - } - - // Try to read ECC P-384 EK cert - if let Ok(cert) = tpm.nv_read(NV_INDEX_ECC_P384_EK_CERT) { - if cert.starts_with(&DER_SEQUENCE_LONG) { - ek_certs.ecc_p384 = Some(der_to_pem(&cert, "CERTIFICATE")); - } - } - - // Step 2: Create TCG standard EK (public key should match certificate) - // Note: Standard EK is decrypt-only, cannot sign. We use it only for - // identity verification (comparing public key with certificate). - let ek = tpm.create_standard_ek() - .context("Failed to create standard EK - endorsement hierarchy may require authentication")?; - - let mut ek_public_keys = HashMap::new(); - ek_public_keys.insert("ecc_p256".to_string(), EccPublicKeyCoords { - x: hex::encode(&ek.public_key.x), - y: hex::encode(&ek.public_key.y), - }); - - // Step 3: Detect platform and choose PCR bank - // Nitro TPMs use SHA-384 for signed PCRs, so we bind AK to SHA-384 bank - // Other platforms use SHA-256 - let is_nitro = tpm.is_nitro_tpm()?; - let pcr_alg = if is_nitro { - crate::TpmAlg::Sha384 - } else { - crate::TpmAlg::Sha256 - }; - - // Step 4: Read all allocated PCRs from all banks - let all_pcrs = tpm.read_all_allocated_pcrs()?; - - // Get PCR values for the chosen bank - let pcr_values: Vec<(u8, Vec)> = all_pcrs.iter() - .filter(|(_, alg, _)| *alg == pcr_alg) - .map(|(idx, _, val)| (*idx, val.clone())) - .collect(); - - if pcr_values.is_empty() { - return Err(anyhow!("No {:?} PCRs allocated on this TPM", pcr_alg)); - } - - // Only include PCRs relevant to the chain of trust in output - let mut pcrs_by_alg: HashMap> = HashMap::new(); - let pcr_map = pcrs_by_alg.entry(pcr_alg.name().to_string()).or_insert_with(BTreeMap::new); - for (idx, value) in &pcr_values { - pcr_map.insert(*idx, hex::encode(value)); - } - - // Step 5: Compute policy from PCR values - let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg)?; - - // Create signing key (AK) bound to this policy - let signing_key = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?; - - let mut signing_key_public_keys = HashMap::new(); - signing_key_public_keys.insert("ecc_p256".to_string(), EccPublicKeyCoords { - x: hex::encode(&signing_key.public_key.x), - y: hex::encode(&signing_key.public_key.y), - }); - - // Compute AK name (used for PCR policy verification) - let _ak_name = compute_ecc_p256_name( - &signing_key.public_key.x, - &signing_key.public_key.y, - &auth_policy, - ); - - // Step 6: AK self-certifies via TPM2_Certify (produces TPM2B_ATTEST + signature) - // This produces TPM2B_ATTEST containing the AK's name (which includes authPolicy) - let cert_result = tpm.certify( - signing_key.handle, // object to certify (AK itself) - signing_key.handle, // signing key (AK) - nonce, // qualifying data (becomes extraData in TPM2B_ATTEST) - )?; - - let attestation_data = AttestationData { - nonce: hex::encode(nonce), - attest_data: hex::encode(&cert_result.attest_data), - signature: hex::encode(&cert_result.signature), - }; - - let mut tpm_attestations = HashMap::new(); - tpm_attestations.insert("ecc_p256".to_string(), attestation_data); - - // Get Nitro attestation if available (we already detected Nitro earlier) - let nitro_attestation = if is_nitro { - // Encode signing key public key in SECG format (0x04 || X || Y) - let mut public_key_secg = Vec::with_capacity(1 + signing_key.public_key.x.len() + signing_key.public_key.y.len()); - public_key_secg.push(0x04); // Uncompressed point indicator - public_key_secg.extend_from_slice(&signing_key.public_key.x); - public_key_secg.extend_from_slice(&signing_key.public_key.y); - - match tpm.nsm_attest( - None, // user_data - Some(nonce.to_vec()), // nonce - Some(public_key_secg.clone()) // public_key - ) { - Ok(document) => { - Some(NitroAttestationData { - public_key: hex::encode(&public_key_secg), - nonce: hex::encode(nonce), - document: hex::encode(&document), - }) - } - Err(_e) => { - None - } - } - } - else { - None - }; - - let attestation = AttestationContainer { - tpm: tpm_attestations, - nitro: nitro_attestation, - }; - - // Cleanup TPM handles - tpm.flush_context(signing_key.handle)?; - tpm.flush_context(ek.handle)?; - - // Step 7: Build and output JSON - let output = AttestationOutput { - ek_certificates: ek_certs, - pcrs: pcrs_by_alg, - ek_public_keys, - signing_key_public_keys, - attestation, - }; - - let json = serde_json::to_string_pretty(&output)?; - - Ok(json) -} diff --git a/crates/rawdogtpm2/src/bin/nsmtest.rs b/crates/rawdogtpm2/src/bin/nsmtest.rs deleted file mode 100644 index c72341e..0000000 --- a/crates/rawdogtpm2/src/bin/nsmtest.rs +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! AWS Nitro Security Module (NSM) test binary -//! -//! Tests NSM functionality via TPM vendor commands - -use anyhow::Result; -use hex; -use rawdogtpm2::{Tpm, NsmOps}; - -fn main() -> Result<()> { - println!("AWS Nitro TPM - NSM Test"); - println!("========================\n"); - - // Open direct TPM device for NSM vendor commands - println!("Opening TPM device (/dev/tpm0 - required for NSM vendor commands)..."); - let mut tpm = Tpm::open_direct()?; - println!("✓ TPM device opened successfully\n"); - - // Test 2: Attestation - println!("Test 2: NSM Attestation"); - println!("-----------------------"); - - println!("Requesting attestation document (no user_data, nonce, or public_key)..."); - match tpm.nsm_attest(None, None, None) { - Ok(attestation_doc) => { - println!("✓ Attestation successful!\n"); - - println!("Attestation Document:"); - println!(" Size: {} bytes", attestation_doc.len()); - println!(" First 64 bytes (hex): {}", hex::encode(&attestation_doc[..64.min(attestation_doc.len())])); - - println!("\n✓ All NSM tests passed!"); - Ok(()) - } - Err(e) => { - println!("✗ Attestation failed!"); - println!(" Error: {}", e); - Err(e) - } - } -} diff --git a/crates/rawdogtpm2/src/bin/selftest.rs b/crates/rawdogtpm2/src/bin/selftest.rs deleted file mode 100644 index 54fd9cc..0000000 --- a/crates/rawdogtpm2/src/bin/selftest.rs +++ /dev/null @@ -1,662 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! TPM 2.0 selftest binary -//! -//! Connects to the local TPM and runs basic functionality tests - -use anyhow::Result; -use hex; -use sha2::{Sha256, Digest}; - -use rawdogtpm2::{ - Tpm, TpmAlg, TPM_RH_OWNER, TPM_RH_ENDORSEMENT, - NV_INDEX_RSA_2048_EK_CERT, NV_INDEX_ECC_P256_EK_CERT, NV_INDEX_ECC_P384_EK_CERT, - EkOps, PcrOps, NvOps, - compute_ecc_p256_name, der_to_pem, -}; - -// TPM fixed property identifiers (TPM_PT) -const TPM_PT_FAMILY_INDICATOR: u32 = 0x00000100; -const TPM_PT_LEVEL: u32 = 0x00000101; -const TPM_PT_REVISION: u32 = 0x00000102; -const TPM_PT_DAY_OF_YEAR: u32 = 0x00000103; -const TPM_PT_YEAR: u32 = 0x00000104; -const TPM_PT_MANUFACTURER: u32 = 0x00000105; -const TPM_PT_VENDOR_STRING_1: u32 = 0x00000106; -const TPM_PT_VENDOR_TPM_TYPE: u32 = 0x0000010A; -const TPM_PT_FIRMWARE_VERSION_1: u32 = 0x0000010B; -const TPM_PT_FIRMWARE_VERSION_2: u32 = 0x0000010C; - -fn main() -> Result<()> { - // Parse command-line arguments - let args: Vec = std::env::args().collect(); - - if args.iter().any(|arg| arg == "--help" || arg == "-h") { - print_help(); - return Ok(()); - } - - // Run standard TPM tests - standard_tests() -} - -fn print_help() { - println!("TPM 2.0 Selftest"); - println!("================\n"); - println!("Usage: selftest [OPTIONS]\n"); - println!("Options:"); - println!(" --help, -h Show this help message\n"); - println!("Runs standard TPM tests"); -} - -fn standard_tests() -> Result<()> { - println!("TPM 2.0 Selftest"); - println!("================\n"); - - // Open TPM device - println!("Opening TPM device..."); - let mut tpm = Tpm::open()?; - println!("✓ TPM device opened successfully\n"); - - // Test -1: Query TPM properties (manufacturer, version, etc.) - println!("Test -1: Querying TPM properties"); - println!("----------------------------------"); - - // Helper to convert u32 to ASCII string (for manufacturer ID) - let u32_to_ascii = |val: u32| -> String { - let bytes = val.to_be_bytes(); - String::from_utf8_lossy(&bytes).trim_end_matches('\0').to_string() - }; - - // Query all properties - match tpm.get_property(TPM_PT_FAMILY_INDICATOR) { - Ok(val) => println!("Family Indicator: {} (\"{}\")", u32_to_ascii(val), u32_to_ascii(val)), - Err(e) => println!("Family Indicator: Error: {}", e), - } - - match tpm.get_property(TPM_PT_LEVEL) { - Ok(val) => println!("Level: {}", val), - Err(e) => println!("Level: Error: {}", e), - } - - match tpm.get_property(TPM_PT_REVISION) { - Ok(val) => println!("Revision: {}.{}", (val >> 16) & 0xFFFF, val & 0xFFFF), - Err(e) => println!("Revision: Error: {}", e), - } - - match tpm.get_property(TPM_PT_DAY_OF_YEAR) { - Ok(val) => println!("Day of Year: {}", val), - Err(e) => println!("Day of Year: Error: {}", e), - } - - match tpm.get_property(TPM_PT_YEAR) { - Ok(val) => println!("Year: {}", val), - Err(e) => println!("Year: Error: {}", e), - } - - match tpm.get_property(TPM_PT_MANUFACTURER) { - Ok(val) => println!("Manufacturer: 0x{:08X} (\"{}\")", val, u32_to_ascii(val)), - Err(e) => println!("Manufacturer: Error: {}", e), - } - - // Vendor string is 16 bytes total across 4 properties - let mut vendor_string = String::new(); - for i in 0..4 { - match tpm.get_property(TPM_PT_VENDOR_STRING_1 + i) { - Ok(val) => { - vendor_string.push_str(&u32_to_ascii(val)); - if i == 0 { - print!("Vendor String {}: 0x{:08X} (\"{}\")", i + 1, val, u32_to_ascii(val)); - } else { - print!("\nVendor String {}: 0x{:08X} (\"{}\")", i + 1, val, u32_to_ascii(val)); - } - } - Err(e) => print!("\nVendor String {}: Error: {}", i + 1, e), - } - } - println!("\nFull Vendor String: \"{}\"", vendor_string.trim()); - - match tpm.get_property(TPM_PT_VENDOR_TPM_TYPE) { - Ok(val) => println!("Vendor TPM Type: 0x{:08X}", val), - Err(e) => println!("Vendor TPM Type: Error: {}", e), - } - - match tpm.get_property(TPM_PT_FIRMWARE_VERSION_1) { - Ok(val) => println!("Firmware Version 1: 0x{:08X} ({}.{})", val, (val >> 16) & 0xFFFF, val & 0xFFFF), - Err(e) => println!("Firmware Version 1: Error: {}", e), - } - - match tpm.get_property(TPM_PT_FIRMWARE_VERSION_2) { - Ok(val) => println!("Firmware Version 2: 0x{:08X} ({}.{})", val, (val >> 16) & 0xFFFF, val & 0xFFFF), - Err(e) => println!("Firmware Version 2: Error: {}", e), - } - - println!("\n✓ TPM properties queried successfully\n"); - - // Test -0.5: Check if this is a Nitro TPM - println!("Test -0.5: Checking for AWS Nitro TPM"); - println!("--------------------------------------"); - match tpm.is_nitro_tpm() { - Ok(true) => println!("✓ This is an AWS Nitro TPM"), - Ok(false) => println!("✗ This is NOT an AWS Nitro TPM"), - Err(e) => println!("⚠ Could not determine TPM type: {}", e), - } - println!(); - - // Test 0: Query active PCR banks - println!("Test 0: Querying active PCR banks"); - println!("----------------------------------"); - let banks = tpm.get_active_pcr_banks()?; - - if banks.is_empty() { - println!("⚠ No active PCR banks found!"); - } else { - println!("Active PCR banks:"); - for bank in &banks { - println!(" - {} (0x{:04X}) - {} bytes per digest", - bank.name(), - *bank as u16, - bank.digest_size().unwrap_or(0)); - } - } - println!("✓ Found {} active PCR bank(s)\n", banks.len()); - - // Test 0.5: Query allocated PCRs - println!("Test 0.5: Querying allocated PCRs (0-31)"); - println!("-----------------------------------------"); - let allocated_pcrs = tpm.get_allocated_pcrs()?; - println!("Allocated PCRs: {} out of 32 possible", allocated_pcrs.len()); - for (pcr_idx, banks) in &allocated_pcrs { - let bank_names: Vec = banks.iter().map(|b| b.name().to_string()).collect(); - println!(" PCR {:2}: {}", pcr_idx, bank_names.join(", ")); - } - println!(); - - // Test 1: Read all non-zero PCR values from all banks - println!("Test 1: Reading all non-zero PCR values (all banks)"); - println!("----------------------------------------------------"); - let pcrs = tpm.read_nonzero_pcrs_all_banks()?; - - if pcrs.is_empty() { - println!("No non-zero PCRs found (all PCRs are zero in all banks)"); - } else { - for (index, alg, value) in &pcrs { - println!("PCR {:2} [{}]: {}", index, alg.name(), hex::encode(value)); - } - } - - println!("\n✓ PCR read successful"); - println!(" Found {} non-zero PCR values\n", pcrs.len()); - - // Test 2: Extend PCR 23 (application-specific PCR) - println!("Test 2: Extending PCR 23 with test data"); - println!("----------------------------------------"); - let test_extend_data = b"TPM selftest measurement v1.0"; - println!("Data to extend: {:?}", std::str::from_utf8(test_extend_data).unwrap()); - println!("Data length: {} bytes", test_extend_data.len()); - - // Check which banks PCR 23 is allocated in - println!("\nChecking which banks PCR 23 is allocated in..."); - let allocated_banks = tpm.get_pcr_allocated_banks(23)?; - println!("PCR 23 is allocated in {} bank(s):", allocated_banks.len()); - for bank in &allocated_banks { - println!(" - {}", bank.name()); - } - - // Read PCR 23 before extension (all banks) - println!("\nPCR 23 values BEFORE extension:"); - let pcr23_before = tpm.pcr_read_all_banks(&[23])?; - for (_index, alg, value) in &pcr23_before { - println!(" [{}]: {}", alg.name(), hex::encode(value)); - } - - // Extend PCR 23 with all allocated banks - println!("\nExtending PCR 23..."); - tpm.pcr_extend(23, test_extend_data)?; - println!("✓ PCR 23 extended successfully"); - println!(" Extended {} bank(s): {}", - allocated_banks.len(), - allocated_banks.iter().map(|a| a.name()).collect::>().join(", ")); - - // Read PCR 23 after extension (all banks) - println!("\nPCR 23 values AFTER extension:"); - let pcr23_after = tpm.pcr_read_all_banks(&[23])?; - for (_index, alg, value) in &pcr23_after { - println!(" [{}]: {}", alg.name(), hex::encode(value)); - } - - // Verify the value changed - if pcr23_before != pcr23_after { - println!("\n✓ PCR 23 value changed as expected"); - } else { - println!("\n⚠ Warning: PCR 23 value did not change"); - } - println!(); - - // Test 3: Create primary ECC key - println!("Test 3: Creating primary ECC signing key"); - println!("-----------------------------------------"); - let key_result = tpm.create_primary_ecc_key(TPM_RH_OWNER)?; - println!("✓ Primary key created"); - println!(" Handle: 0x{:08X}", key_result.handle); - println!(" Public X: {}", hex::encode(&key_result.public_key.x)); - println!(" Public Y: {}", hex::encode(&key_result.public_key.y)); - println!(); - - // Test 4: Sign data - println!("Test 4: Signing test data"); - println!("-------------------------"); - let test_data = b"Hello, TPM!"; - let digest = Sha256::digest(test_data); - println!("Test data: {:?}", std::str::from_utf8(test_data).unwrap()); - println!("SHA256 digest: {}", hex::encode(&digest)); - - let signature = tpm.sign(key_result.handle, &digest)?; - println!("✓ Signature created (DER-encoded)"); - println!(" Signature ({} bytes): {}", signature.len(), hex::encode(&signature)); - println!(); - - // Test 5: Check for Endorsement Key - println!("Test 5: Checking for Endorsement Key (EK)"); - println!("------------------------------------------"); - match tpm.create_primary_ecc_key(TPM_RH_ENDORSEMENT) { - Ok(ek_result) => { - println!("✓ EK created/accessed in endorsement hierarchy"); - println!(" EK Handle: 0x{:08X}", ek_result.handle); - println!(" EK Public X: {}", hex::encode(&ek_result.public_key.x)); - println!(" EK Public Y: {}", hex::encode(&ek_result.public_key.y)); - - // Flush EK - tpm.flush_context(ek_result.handle)?; - } - Err(e) => { - println!("⚠ Could not access endorsement hierarchy: {}", e); - println!(" This is normal for:"); - println!(" - swtpm without EK provisioning"); - println!(" - Some local TPMs with EK password set"); - println!(" - Restricted TPM configurations"); - } - } - println!(); - - // Test 6: Check for EK Certificate in NV RAM - println!("Test 6: Checking for EK Certificate in NV RAM"); - println!("----------------------------------------------"); - - check_ek_cert(&mut tpm, NV_INDEX_RSA_2048_EK_CERT, "RSA 2048 EK", "rsa_ek_cert"); - check_ek_cert(&mut tpm, NV_INDEX_ECC_P256_EK_CERT, "ECC P-256 EK", "ecc_p256_ek_cert"); - check_ek_cert(&mut tpm, NV_INDEX_ECC_P384_EK_CERT, "ECC P-384 EK", "ecc_p384_ek_cert"); - - println!(); - - // Test 6.5: List all NV indices - println!("Test 6.5: Enumerating all NV RAM indices"); - println!("-----------------------------------------"); - - let nv_indices = tpm.nv_indices()?; - println!("Found {} NV indices\n", nv_indices.len()); - - for nv_index in &nv_indices { - println!("NV Index: 0x{:08X}", nv_index); - - // Get NV index info - match tpm.nv_readpublic(*nv_index) { - Ok(info) => { - println!(" Name Algorithm: 0x{:04X}", info.name_alg); - println!(" Attributes: 0x{:08X}", info.attributes); - decode_nv_attributes(info.attributes, 4); - println!(" Auth Policy: {} bytes", info.auth_policy.len()); - if !info.auth_policy.is_empty() { - println!(" {}", hex::encode(&info.auth_policy)); - } - println!(" Data Size: {} bytes", info.data_size); - - // Try to read the data (may fail if auth is required) - if info.data_size > 0 && info.data_size <= 2048 { - match tpm.nv_read(*nv_index) { - Ok(data) => { - println!(" Data (hex dump):"); - hex_dump(&data, 4); - } - Err(e) => { - println!(" Data: ", e); - } - } - } else if info.data_size > 2048 { - println!(" Data: ", info.data_size); - } - } - Err(e) => { - println!(" Error reading public info: {}", e); - } - } - println!(); - } - - // Test 7: PCR-sealed key - println!("Test 7: Creating PCR-sealed signing key"); - println!("-----------------------------------------"); - - // Read ALL allocated PCRs (including zeros) for proper security policy - println!("Reading all allocated PCRs..."); - let all_pcrs = tpm.read_all_allocated_pcrs()?; - println!("Sealing to ALL {} allocated PCR values:", all_pcrs.len()); - - for (index, alg, value) in &all_pcrs { - println!(" PCR {:2} [{}]: {}", index, alg.name(), hex::encode(value)); - } - - // Get SHA-256 PCR values (must use SHA-256 for consistency with verification) - let pcr_values: Vec<(u8, Vec)> = all_pcrs.iter() - .filter(|(_, alg, _)| *alg == TpmAlg::Sha256) - .map(|(idx, _, val)| (*idx, val.clone())) - .collect(); - - if pcr_values.is_empty() { - anyhow::bail!("No SHA-256 PCRs allocated on this TPM"); - } - - println!("\nCreating key sealed to {} SHA-256 PCRs", pcr_values.len()); - - // Compute policy from the PCR values we already have (SHA-256 bank) - let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, TpmAlg::Sha256)?; - let sealed_key = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?; - println!("✓ PCR-sealed key created"); - println!(" Handle: 0x{:08X}", sealed_key.handle); - println!(" Public X: {}", hex::encode(&sealed_key.public_key.x)); - println!(" Public Y: {}", hex::encode(&sealed_key.public_key.y)); - - // Test 8: Certify the PCR-sealed key with the EK - println!("\nTest 8: Certifying PCR-sealed key with EK"); - println!("------------------------------------------"); - - // First, we need the EK - println!("Creating/accessing EK..."); - let ek = match tpm.create_primary_ecc_key(TPM_RH_ENDORSEMENT) { - Ok(ek) => { - println!("✓ EK handle: 0x{:08X}", ek.handle); - ek - } - Err(e) => { - println!("⚠ Could not access EK: {}", e); - println!("Skipping certification test (EK not available)"); - tpm.flush_context(sealed_key.handle)?; - println!(); - tpm.flush_context(key_result.handle)?; - println!("All tests passed!"); - return Ok(()); - } - }; - - // Generate qualifying data (using a nonce/challenge for this example) - let qualifying_data = b"attestation-challenge-12345"; - println!("Using qualifying data: {:?}", std::str::from_utf8(qualifying_data).unwrap()); - - // Certify the PCR-sealed key using the EK - println!("\nCertifying PCR-sealed key with EK..."); - let cert_result = tpm.certify(sealed_key.handle, ek.handle, qualifying_data)?; - - println!("✓ Certification complete!"); - println!(" Attestation data: {} bytes", cert_result.attest_data.len()); - println!(" Signature: {} bytes (DER-encoded)", cert_result.signature.len()); - - // Save attestation for inspection - if let Err(e) = std::fs::write("/tmp/attestation.bin", &cert_result.attest_data) { - eprintln!(" Warning: Could not write /tmp/attestation.bin: {}", e); - } else { - println!(" Saved attestation to: /tmp/attestation.bin"); - } - - if let Err(e) = std::fs::write("/tmp/attestation_signature.der", &cert_result.signature) { - eprintln!(" Warning: Could not write /tmp/attestation_signature.der: {}", e); - } else { - println!(" Saved signature to: /tmp/attestation_signature.der"); - } - - // Cleanup EK and sealed key (but keep key_result for more tests) - tpm.flush_context(ek.handle)?; - tpm.flush_context(sealed_key.handle)?; - - // Test 9: Standard EK creation - println!("\nTest 9: Standard EK Creation (TCG Template)"); - println!("--------------------------------------------"); - - // Try to create the TCG standard EK - match tpm.create_standard_ek() { - Ok(standard_ek) => { - println!("✓ Standard EK created using TCG EK Credential Profile template"); - println!(" Handle: 0x{:08X}", standard_ek.handle); - println!(" Public X: {}", hex::encode(&standard_ek.public_key.x)); - println!(" Public Y: {}", hex::encode(&standard_ek.public_key.y)); - println!(" Note: Certificate comparison requires rawdogtpm2-verify selftest"); - - tpm.flush_context(standard_ek.handle)?; - } - Err(e) => { - println!("⚠ Could not create standard EK: {}", e); - println!(" (Endorsement hierarchy may require authentication)"); - } - } - println!(); - - // Test 10: ReadPublic and name verification - println!("Test 10: ReadPublic and Name Verification"); - println!("------------------------------------------"); - - let read_result = tpm.read_public(key_result.handle)?; - println!("ReadPublic returned:"); - println!(" Public area: {} bytes", read_result.public_area.len()); - println!(" Name: {}", hex::encode(&read_result.name)); - - // Compute expected name and compare - // For our signing key, authPolicy is empty - let computed_name = compute_ecc_p256_name( - &key_result.public_key.x, - &key_result.public_key.y, - &[], // empty policy for basic signing key - ); - println!(" Computed name: {}", hex::encode(&computed_name)); - - if read_result.name == computed_name { - println!("✓ TPM's name matches our computed name"); - } else { - println!("⚠ Name mismatch - TPM uses different computation"); - println!(" (This is expected if key has non-empty authPolicy)"); - } - println!(); - - // Test 11: Policy session operations - println!("Test 11: Policy Session Operations"); - println!("-----------------------------------"); - - // Start a policy session - let policy_session = tpm.start_policy_session()?; - println!("✓ Policy session started: 0x{:08X}", policy_session); - - // Get initial policy digest (should be all zeros) - let initial_digest = tpm.policy_get_digest(policy_session)?; - println!(" Initial policy digest: {}", hex::encode(&initial_digest)); - - let expected_empty = vec![0u8; 32]; - if initial_digest == expected_empty { - println!("✓ Initial digest is empty (all zeros) as expected"); - } else { - println!("⚠ Initial digest is not empty - unexpected"); - } - - // Execute PolicySecret(TPM_RH_ENDORSEMENT) - println!("\nExecuting PolicySecret(TPM_RH_ENDORSEMENT)..."); - match tpm.policy_secret(policy_session, TPM_RH_ENDORSEMENT) { - Ok(()) => { - println!("✓ PolicySecret executed successfully"); - - // Get updated policy digest - let updated_digest = tpm.policy_get_digest(policy_session)?; - println!(" Updated policy digest: {}", hex::encode(&updated_digest)); - - // Expected digest for PolicySecret(TPM_RH_ENDORSEMENT) with SHA-256 - // This is the standard EK authPolicy - let expected_ek_policy: [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, - ]; - println!(" Expected EK policy: {}", hex::encode(&expected_ek_policy)); - - if updated_digest == expected_ek_policy { - println!("✓ Policy digest matches standard EK authPolicy!"); - } else { - println!("⚠ Policy digest does not match expected value"); - } - } - Err(e) => { - println!("⚠ PolicySecret failed: {}", e); - println!(" (This is normal if endorsement hierarchy requires authentication)"); - } - } - - // Flush policy session - tpm.flush_context(policy_session)?; - println!("✓ Policy session flushed"); - - println!(); - - // Cleanup: Flush the signing key handle - tpm.flush_context(key_result.handle)?; - - println!("======================"); - println!("All tests completed!"); - println!("======================"); - Ok(()) -} - -/// Check for and display an EK certificate from NV RAM -fn check_ek_cert(tpm: &mut Tpm, nv_index: u32, description: &str, filename_base: &str) { - print!("Checking {} cert (0x{:08X})... ", description, nv_index); - match tpm.nv_read(nv_index) { - Ok(cert) => { - println!("✓ Found!"); - println!(" Size: {} bytes", cert.len()); - if cert.len() > 0 { - let der_path = format!("/tmp/{}.der", filename_base); - let pem_path = format!("/tmp/{}.pem", filename_base); - - // Save raw DER to file - if let Err(e) = std::fs::write(&der_path, &cert) { - eprintln!(" Warning: Could not write {}: {}", der_path, e); - } else { - println!(" Saved DER to: {}", der_path); - println!(" Verify with: openssl x509 -inform DER -in {} -text -noout", der_path); - } - - // Check if it looks like a DER-encoded certificate - if cert.starts_with(&[0x30, 0x82]) { - println!(" Format: DER-encoded X.509 certificate"); - - let pem = der_to_pem(&cert, "CERTIFICATE"); - - // Save PEM to file - if let Err(e) = std::fs::write(&pem_path, &pem) { - eprintln!(" Warning: Could not write {}: {}", pem_path, e); - } else { - println!(" Saved PEM to: {}", pem_path); - println!(" Verify with: openssl x509 -in {} -text -noout", pem_path); - } - - println!("\n{}", pem); - } else { - println!(" Format: Unknown (not standard DER)"); - println!(" First 32 bytes: {}", hex::encode(&cert[..cert.len().min(32)])); - } - } - } - Err(e) => { - println!("Not found"); - println!(" Error: {}", e); - } - } -} - -/// Decode NV index attributes bitfield -fn decode_nv_attributes(attrs: u32, indent_spaces: usize) { - let indent = " ".repeat(indent_spaces); - - // TPMA_NV bit definitions from TPM 2.0 Part 2, Section 13.2 - if attrs & (1 << 1) != 0 { println!("{} - PPWRITE: Platform can write", indent); } - if attrs & (1 << 2) != 0 { println!("{} - OWNERWRITE: Owner can write", indent); } - if attrs & (1 << 3) != 0 { println!("{} - AUTHWRITE: Auth required for write", indent); } - if attrs & (1 << 4) != 0 { println!("{} - POLICYWRITE: Policy required for write", indent); } - - // Bits 7-10: TPM_NT (NV Type) - let nv_type = (attrs >> 4) & 0xF; - print!("{} - TYPE: ", indent); - match nv_type { - 0x0 => println!("Ordinary (0x0)"), - 0x1 => println!("Counter (0x1)"), - 0x2 => println!("Bits (0x2)"), - 0x4 => println!("Extend (0x4)"), - 0x8 => println!("PIN Fail (0x8)"), - 0x9 => println!("PIN Pass (0x9)"), - _ => println!("Unknown (0x{:X})", nv_type), - } - - if attrs & (1 << 10) != 0 { println!("{} - POLICY_DELETE: Policy required to delete", indent); } - if attrs & (1 << 11) != 0 { println!("{} - WRITELOCKED: Currently write-locked", indent); } - if attrs & (1 << 12) != 0 { println!("{} - WRITEALL: Must write full size at once", indent); } - if attrs & (1 << 13) != 0 { println!("{} - WRITEDEFINE: Can be written after definition", indent); } - if attrs & (1 << 14) != 0 { println!("{} - WRITE_STCLEAR: Write locked until TPM restart", indent); } - if attrs & (1 << 15) != 0 { println!("{} - GLOBALLOCK: Write locked by global lock", indent); } - if attrs & (1 << 16) != 0 { println!("{} - PPREAD: Platform can read", indent); } - if attrs & (1 << 17) != 0 { println!("{} - OWNERREAD: Owner can read", indent); } - if attrs & (1 << 18) != 0 { println!("{} - AUTHREAD: Auth required for read", indent); } - if attrs & (1 << 19) != 0 { println!("{} - POLICYREAD: Policy required for read", indent); } - if attrs & (1 << 20) != 0 { println!("{} - NO_DA: Not subject to dictionary attack protection", indent); } - if attrs & (1 << 21) != 0 { println!("{} - ORDERLY: Only updated on orderly shutdown", indent); } - if attrs & (1 << 22) != 0 { println!("{} - CLEAR_STCLEAR: Cleared on TPM reset", indent); } - if attrs & (1 << 23) != 0 { println!("{} - READLOCKED: Currently read-locked", indent); } - if attrs & (1 << 24) != 0 { println!("{} - WRITTEN: Has been written", indent); } - if attrs & (1 << 25) != 0 { println!("{} - PLATFORMCREATE: Created by platform", indent); } - if attrs & (1 << 26) != 0 { println!("{} - READ_STCLEAR: Readable after restart", indent); } -} - -/// Display a hex dump of data with optional indentation -fn hex_dump(data: &[u8], indent_spaces: usize) { - let indent = " ".repeat(indent_spaces); - const BYTES_PER_LINE: usize = 16; - - for (line_num, chunk) in data.chunks(BYTES_PER_LINE).enumerate() { - let offset = line_num * BYTES_PER_LINE; - print!("{} {:04x} ", indent, offset); - - // Print hex bytes - for (i, byte) in chunk.iter().enumerate() { - print!("{:02x} ", byte); - if i == 7 { - print!(" "); // Extra space in the middle - } - } - - // Padding for last line if not full - if chunk.len() < BYTES_PER_LINE { - for i in chunk.len()..BYTES_PER_LINE { - print!(" "); - if i == 7 { - print!(" "); - } - } - } - - // Print ASCII representation - print!(" |"); - for byte in chunk { - let c = if *byte >= 0x20 && *byte <= 0x7e { - *byte as char - } else { - '.' - }; - print!("{}", c); - } - println!("|"); - } -} diff --git a/crates/rawdogtpm2/src/credential.rs b/crates/rawdogtpm2/src/credential.rs deleted file mode 100644 index a056659..0000000 --- a/crates/rawdogtpm2/src/credential.rs +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! TPM2 policy session operations -//! -//! Provides policy session management for TPM authorization flows. -//! Also includes utility functions for computing TPM object names. - -use anyhow::Result; -use sha2::{Digest, Sha256}; - -use crate::{ - CommandBuffer, Tpm, TpmAlg, TpmCc, TpmSt, - TPM_RS_PW, TPM_SE_POLICY, TPM_RH_NULL, -}; - -/// Result from ReadPublic -#[derive(Debug)] -pub struct ReadPublicResult { - /// The raw TPMT_PUBLIC structure - pub public_area: Vec, - /// The object's name as computed by the TPM - pub name: Vec, -} - -/// Policy session operations -impl Tpm { - /// Read the public area and name of an object - /// - /// This is useful for verifying that our name computation matches - /// what the TPM actually has for an object. - pub fn read_public(&mut self, object_handle: u32) -> Result { - let command = CommandBuffer::new() - .write_u32(object_handle) - .finalize(TpmSt::NoSessions, TpmCc::ReadPublic); - - let mut resp = self.transmit(&command)?; - - // Parse response: outPublic (TPM2B_PUBLIC), name (TPM2B_NAME), qualifiedName (TPM2B_NAME) - let public_area = resp.read_tpm2b()?; - let name = resp.read_tpm2b()?; - let _qualified_name = resp.read_tpm2b()?; - - Ok(ReadPublicResult { - public_area, - name, - }) - } - - /// Start a policy session - /// - /// Creates a new policy session that can be used for policy-based authorization. - /// The session must be flushed after use with `flush_context()`. - pub fn start_policy_session(&mut self) -> Result { - // TPM2_StartAuthSession - // tpmKey = TPM_RH_NULL (no salt) - // bind = TPM_RH_NULL (no bind) - // nonceCaller = empty (TPM will generate) - // encryptedSalt = empty - // sessionType = TPM_SE_POLICY - // symmetric = TPM_ALG_NULL - // authHash = TPM_ALG_SHA256 - - // Generate a random nonce (some TPMs require non-empty nonceCaller) - // Use hash of current time as simple entropy source - let nonce_data = Sha256::digest( - &std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - .to_le_bytes() - ); - let nonce_caller = &nonce_data[..16]; // Use 16 bytes - - let command = CommandBuffer::new() - .write_u32(TPM_RH_NULL) // tpmKey - .write_u32(TPM_RH_NULL) // bind - .write_tpm2b(nonce_caller) // nonceCaller (16 bytes) - .write_u16(0) // encryptedSalt size (no salt) - .write_u8(TPM_SE_POLICY) // sessionType - .write_u16(TpmAlg::Null as u16) // symmetric.algorithm = TPM_ALG_NULL - .write_u16(TpmAlg::Sha256 as u16) // authHash = TPM_ALG_SHA256 - .finalize(TpmSt::NoSessions, TpmCc::StartAuthSession); - - let mut resp = self.transmit(&command)?; - - // Parse response: sessionHandle, nonceTPM - let session_handle = resp.read_u32()?; - let _nonce_tpm = resp.read_tpm2b()?; // We don't need the nonce for simple policy - - Ok(session_handle) - } - - /// Execute PolicySecret command - /// - /// Satisfies a policy that requires PolicySecret(authHandle). - /// For standard EK, authHandle should be TPM_RH_ENDORSEMENT. - pub fn policy_secret(&mut self, policy_session: u32, auth_handle: u32) -> Result<()> { - // TPM2_PolicySecret - // authHandle: the entity providing authorization (TPM_RH_ENDORSEMENT) - // policySession: the policy session to update - // nonceTPM: empty - // cpHashA: empty - // policyRef: empty - // expiration: 0 - - let command = CommandBuffer::new() - .write_u32(auth_handle) // authHandle - .write_u32(policy_session) // policySession - // Authorization for authHandle (password session, empty) - .write_u32(9) // authorizationSize - .write_u32(TPM_RS_PW) - .write_u16(0) // nonce - .write_u8(0) // attributes - .write_u16(0) // password - // Parameters - .write_u16(0) // nonceTPM size - .write_u16(0) // cpHashA size - .write_u16(0) // policyRef size - .write_u32(0) // expiration (INT32, 0 = no expiration) - .finalize(TpmSt::Sessions, TpmCc::PolicySecret); - - let mut resp = self.transmit(&command)?; - - // Parse response - skip timeout and policyTicket - let _parameter_size = resp.read_u32()?; - // We don't need the timeout or ticket for our purposes - - Ok(()) - } - - /// Get the current policy digest from a policy session - /// - /// Useful for debugging to verify the policy matches expectations. - pub fn policy_get_digest(&mut self, policy_session: u32) -> Result> { - // TPM2_PolicyGetDigest - // Input: policySession handle - // Output: policyDigest (TPM2B_DIGEST) - - let command = CommandBuffer::new() - .write_u32(policy_session) - .finalize(TpmSt::NoSessions, TpmCc::PolicyGetDigest); - - let mut resp = self.transmit(&command)?; - - // Parse response: policyDigest (TPM2B_DIGEST) - let digest = resp.read_tpm2b()?; - - Ok(digest.to_vec()) - } -} - -/// Compute TPM object name from public key and authPolicy -/// -/// name = nameAlg || H(TPMT_PUBLIC) -/// -/// For ECC P-256 signing keys with PCR policy. -pub fn compute_ecc_p256_name( - pubkey_x: &[u8], - pubkey_y: &[u8], - auth_policy: &[u8], -) -> Vec { - // Build TPMT_PUBLIC for ECC P-256 signing key - let mut public_area = Vec::new(); - - // type: TPM_ALG_ECC (0x0023) - public_area.extend_from_slice(&0x0023u16.to_be_bytes()); - // nameAlg: TPM_ALG_SHA256 (0x000B) - public_area.extend_from_slice(&0x000Bu16.to_be_bytes()); - // objectAttributes: fixedTPM | fixedParent | sensitiveDataOrigin | userWithAuth | decrypt | sign - // bits: 1,4,5,6,17,18 = 0x00060072 - public_area.extend_from_slice(&0x00060072u32.to_be_bytes()); - // authPolicy (TPM2B_DIGEST) - public_area.extend_from_slice(&(auth_policy.len() as u16).to_be_bytes()); - public_area.extend_from_slice(auth_policy); - // parameters (TPMS_ECC_PARMS): - // symmetric: TPM_ALG_NULL (0x0010) - public_area.extend_from_slice(&0x0010u16.to_be_bytes()); - // scheme: TPM_ALG_NULL (0x0010) - public_area.extend_from_slice(&0x0010u16.to_be_bytes()); - // curveID: TPM_ECC_NIST_P256 (0x0003) - public_area.extend_from_slice(&0x0003u16.to_be_bytes()); - // kdf: TPM_ALG_NULL (0x0010) - public_area.extend_from_slice(&0x0010u16.to_be_bytes()); - // unique (TPMS_ECC_POINT): - // x (TPM2B_ECC_PARAMETER) - public_area.extend_from_slice(&(pubkey_x.len() as u16).to_be_bytes()); - public_area.extend_from_slice(pubkey_x); - // y (TPM2B_ECC_PARAMETER) - public_area.extend_from_slice(&(pubkey_y.len() as u16).to_be_bytes()); - public_area.extend_from_slice(pubkey_y); - - // name = nameAlg || H(TPMT_PUBLIC) - let mut name = Vec::new(); - name.extend_from_slice(&0x000Bu16.to_be_bytes()); // SHA256 - name.extend_from_slice(&Sha256::digest(&public_area)); - - name -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_compute_name_deterministic() { - let x = [0x01u8; 32]; - let y = [0x02u8; 32]; - let policy = [0x03u8; 32]; - - let name1 = compute_ecc_p256_name(&x, &y, &policy); - let name2 = compute_ecc_p256_name(&x, &y, &policy); - - assert_eq!(name1, name2); - // Name should be 2 (alg) + 32 (hash) = 34 bytes - assert_eq!(name1.len(), 34); - // Should start with SHA256 algorithm ID - assert_eq!(&name1[0..2], &[0x00, 0x0B]); - } - -} diff --git a/crates/rawdogtpm2/src/ek.rs b/crates/rawdogtpm2/src/ek.rs deleted file mode 100644 index 2cfbf23..0000000 --- a/crates/rawdogtpm2/src/ek.rs +++ /dev/null @@ -1,525 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! EK and key operations -//! -//! Provides TPM key management operations including: -//! - Creating ECC primary keys (standard and custom templates) -//! - TCG-compliant standard EK creation -//! - Signing operations -//! - Key certification - -use anyhow::{bail, Result}; - -use crate::{ - CertifyResult, CommandBuffer, EccPublicKey, ObjectAttributes, PrimaryKeyResult, - ResponseBuffer, TpmAlg, TpmCc, TpmEccCurve, TpmSt, Tpm, - TPM_RH_ENDORSEMENT, TPM_RH_NULL, TPM_RS_PW, -}; - -/// Standard EK authPolicy digest for SHA-256 (TCG EK Credential Profile 2.6) -/// This is PolicySecret(TPM_RH_ENDORSEMENT) with SHA-256 -const STANDARD_EK_AUTH_POLICY: [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, -]; - -/// EK and key operations extension trait -pub trait EkOps { - /// Create a primary ECC P-256 signing key in the specified hierarchy (no policy) - fn create_primary_ecc_key(&mut self, hierarchy: u32) -> Result; - - /// Create a primary ECC P-256 signing key with a specific authPolicy - fn create_primary_ecc_key_with_policy( - &mut self, - hierarchy: u32, - auth_policy: &[u8], - ) -> Result; - - /// Create the TCG standard ECC P-256 Endorsement Key - /// - /// This creates an EK using the TCG standard template, which should produce - /// a key whose public key matches the one in the EK certificate (if the certificate - /// was issued for this TPM using the standard template). - /// - /// The standard EK is a decrypt-only key (cannot sign) with: - /// - Attributes: fixedTPM, fixedParent, sensitiveDataOrigin, adminWithPolicy, restricted, decrypt - /// - authPolicy: PolicySecret(TPM_RH_ENDORSEMENT) - /// - Symmetric: AES-128-CFB - /// - Curve: NIST P-256 - fn create_standard_ek(&mut self) -> Result; - - /// Sign data with a TPM key (returns DER-encoded ECDSA signature) - fn sign(&mut self, key_handle: u32, digest: &[u8]) -> Result>; - - /// Certify a key using another key (e.g., certify signing key with EK) - /// Returns (attestation_data, signature) - fn certify( - &mut self, - object_handle: u32, - sign_handle: u32, - qualifying_data: &[u8], - ) -> Result; -} - -impl EkOps for Tpm { - fn create_primary_ecc_key(&mut self, hierarchy: u32) -> Result { - let public_area = build_ecc_public_area(); - - let command = CommandBuffer::new() - .write_u32(hierarchy) - .write_auth_empty_pw() - // inSensitive (TPM2B_SENSITIVE_CREATE) - .write_u16(4) - .write_u16(0) // userAuth size = 0 - .write_u16(0) // data size = 0 - // inPublic (TPM2B_PUBLIC) - .write_tpm2b(&public_area) - // outsideInfo (TPM2B_DATA) - empty - .write_u16(0) - // creationPCR (TPML_PCR_SELECTION) - empty - .write_u32(0) - .finalize(TpmSt::Sessions, TpmCc::CreatePrimary); - let mut resp = self.transmit(&command)?; - - // Parse response - let handle = resp.read_u32()?; - let parameter_size = resp.read_u32()?; - - // Track where parameters start - let param_start = resp.offset(); - - // Read outPublic (TPM2B_PUBLIC) - let public_size = resp.read_u16()? as usize; - let public_data = resp.read_bytes(public_size)?; - - // Parse the public key from the public area - let public_key = parse_ecc_public_key(public_data)?; - - // Skip remaining CreatePrimary output parameters - let bytes_read = resp.offset() - param_start; - if bytes_read < parameter_size as usize { - let remaining = parameter_size as usize - bytes_read; - resp.read_bytes(remaining)?; - } - - // Verify we read exactly parameter_size bytes - let final_bytes_read = resp.offset() - param_start; - if final_bytes_read != parameter_size as usize { - bail!( - "Parameter size mismatch: TPM said {} bytes, we read {} bytes", - parameter_size, - final_bytes_read - ); - } - - Ok(PrimaryKeyResult { handle, public_key }) - } - - fn create_standard_ek(&mut self) -> Result { - let public_area = build_standard_ek_public_area(); - - let command = CommandBuffer::new() - .write_u32(TPM_RH_ENDORSEMENT) - .write_auth_empty_pw() - // inSensitive (TPM2B_SENSITIVE_CREATE) - .write_u16(4) - .write_u16(0) // userAuth size = 0 - .write_u16(0) // data size = 0 - // inPublic (TPM2B_PUBLIC) - .write_tpm2b(&public_area) - // outsideInfo (TPM2B_DATA) - empty - .write_u16(0) - // creationPCR (TPML_PCR_SELECTION) - empty - .write_u32(0) - .finalize(TpmSt::Sessions, TpmCc::CreatePrimary); - let mut resp = self.transmit(&command)?; - - // Parse response - let handle = resp.read_u32()?; - let parameter_size = resp.read_u32()?; - - // Track where parameters start - let param_start = resp.offset(); - - // Read outPublic (TPM2B_PUBLIC) - let public_size = resp.read_u16()? as usize; - let public_data = resp.read_bytes(public_size)?; - let public_key = parse_ecc_public_key(public_data)?; - - // Skip remaining CreatePrimary output parameters - let bytes_read = resp.offset() - param_start; - if bytes_read < parameter_size as usize { - let remaining = parameter_size as usize - bytes_read; - resp.read_bytes(remaining)?; - } - - // Verify we read exactly parameter_size bytes - let final_bytes_read = resp.offset() - param_start; - if final_bytes_read != parameter_size as usize { - bail!( - "Parameter size mismatch: TPM said {} bytes, we read {} bytes", - parameter_size, - final_bytes_read - ); - } - - Ok(PrimaryKeyResult { handle, public_key }) - } - - fn sign(&mut self, key_handle: u32, digest: &[u8]) -> Result> { - if digest.len() != 32 { - bail!("Digest must be 32 bytes for SHA-256"); - } - - let command = CommandBuffer::new() - .write_u32(key_handle) - .write_auth_empty_pw() - // digest (TPM2B_DIGEST) - .write_tpm2b(digest) - // inScheme (TPMT_SIG_SCHEME) - ECDSA with SHA256 - .write_u16(TpmAlg::EcDsa as u16) - .write_u16(TpmAlg::Sha256 as u16) - // validation (TPMT_TK_HASHCHECK) - NULL ticket - .write_u16(0x8024) // TPM_ST_HASHCHECK - .write_u32(TPM_RH_NULL) - .write_u16(0) // digest size = 0 - .finalize(TpmSt::Sessions, TpmCc::Sign); - let mut resp = self.transmit(&command)?; - - // Parse response - let parameter_size = resp.read_u32()?; - let param_start = resp.offset(); - - // TPMT_SIGNATURE - let sig_alg = resp.read_u16()?; - if sig_alg != TpmAlg::EcDsa as u16 { - bail!("Unexpected signature algorithm: 0x{:04X}", sig_alg); - } - - let hash_alg = resp.read_u16()?; - if hash_alg != TpmAlg::Sha256 as u16 { - bail!("Unexpected hash algorithm: 0x{:04X}", hash_alg); - } - - // TPMS_SIGNATURE_ECC - let r = resp.read_tpm2b()?; - let s = resp.read_tpm2b()?; - - // Verify we read exactly parameter_size bytes - let bytes_read = resp.offset() - param_start; - if bytes_read != parameter_size as usize { - bail!( - "Parameter size mismatch in Sign: TPM said {} bytes, we read {} bytes", - parameter_size, - bytes_read - ); - } - - // Convert to DER-encoded signature - Ok(encode_ecdsa_der_signature(&r, &s)) - } - - fn certify( - &mut self, - object_handle: u32, - sign_handle: u32, - qualifying_data: &[u8], - ) -> Result { - let command = CommandBuffer::new() - .write_u32(object_handle) - .write_u32(sign_handle) - // Authorization area - two sessions (one for each handle) - // Total auth size = 2 * 9 = 18 bytes - .write_u32(18) - // Auth for objectHandle (password session, empty password) - .write_u32(TPM_RS_PW) - .write_u16(0) // nonce - .write_u8(0) // attributes - .write_u16(0) // password - // Auth for signHandle (password session, empty password) - .write_u32(TPM_RS_PW) - .write_u16(0) // nonce - .write_u8(0) // attributes - .write_u16(0) // password - // qualifyingData (TPM2B_DATA) - .write_tpm2b(qualifying_data) - // inScheme (TPMT_SIG_SCHEME) - ECDSA with SHA256 - .write_u16(TpmAlg::EcDsa as u16) - .write_u16(TpmAlg::Sha256 as u16) - .finalize(TpmSt::Sessions, TpmCc::Certify); - let mut resp = self.transmit(&command)?; - - // Parse response - let parameter_size = resp.read_u32()?; - let param_start = resp.offset(); - - // certifyInfo (TPM2B_ATTEST) - let attest_data = resp.read_tpm2b()?; - - // signature (TPMT_SIGNATURE) - let sig_alg = resp.read_u16()?; - if sig_alg != TpmAlg::EcDsa as u16 { - bail!("Unexpected signature algorithm: 0x{:04X}", sig_alg); - } - - let hash_alg = resp.read_u16()?; - if hash_alg != TpmAlg::Sha256 as u16 { - bail!("Unexpected hash algorithm: 0x{:04X}", hash_alg); - } - - // TPMS_SIGNATURE_ECC - let r = resp.read_tpm2b()?; - let s = resp.read_tpm2b()?; - - // Verify we read exactly parameter_size bytes - let bytes_read = resp.offset() - param_start; - if bytes_read != parameter_size as usize { - bail!( - "Parameter size mismatch in Certify: TPM said {} bytes, we read {} bytes", - parameter_size, - bytes_read - ); - } - - let signature = encode_ecdsa_der_signature(&r, &s); - - Ok(CertifyResult { - attest_data: attest_data.to_vec(), - signature, - }) - } - - fn create_primary_ecc_key_with_policy( - &mut self, - hierarchy: u32, - auth_policy: &[u8], - ) -> Result { - let public_area = build_ecc_public_area_with_policy(auth_policy); - - let command = CommandBuffer::new() - .write_u32(hierarchy) - .write_auth_empty_pw() - // inSensitive (TPM2B_SENSITIVE_CREATE) - .write_u16(4) - .write_u16(0) // userAuth size = 0 - .write_u16(0) // data size = 0 - // inPublic (TPM2B_PUBLIC) - with authPolicy - .write_tpm2b(&public_area) - // outsideInfo (TPM2B_DATA) - empty - .write_u16(0) - // creationPCR (TPML_PCR_SELECTION) - empty - .write_u32(0) - .finalize(TpmSt::Sessions, TpmCc::CreatePrimary); - let mut resp = self.transmit(&command)?; - - // Parse response - let handle = resp.read_u32()?; - let parameter_size = resp.read_u32()?; - - // Track where parameters start - let param_start = resp.offset(); - - // Read outPublic (TPM2B_PUBLIC) - let public_size = resp.read_u16()? as usize; - let public_data = resp.read_bytes(public_size)?; - let public_key = parse_ecc_public_key(public_data)?; - - // Skip remaining CreatePrimary output parameters - let bytes_read = resp.offset() - param_start; - if bytes_read < parameter_size as usize { - let remaining = parameter_size as usize - bytes_read; - resp.read_bytes(remaining)?; - } - - // Verify we read exactly parameter_size bytes - let final_bytes_read = resp.offset() - param_start; - if final_bytes_read != parameter_size as usize { - bail!( - "Parameter size mismatch: TPM said {} bytes, we read {} bytes", - parameter_size, - final_bytes_read - ); - } - - Ok(PrimaryKeyResult { handle, public_key }) - } -} - -/// Build a TPM2B_PUBLIC structure for an ECC P-256 signing key -fn build_ecc_public_area() -> Vec { - let attrs = ObjectAttributes::new() - .fixed_tpm() - .fixed_parent() - .sensitive_data_origin() - .user_with_auth() - .decrypt() - .sign_encrypt(); - - CommandBuffer::new() - // TPMT_PUBLIC - .write_u16(TpmAlg::Ecc as u16) // type - .write_u16(TpmAlg::Sha256 as u16) // nameAlg - .write_u32(attrs.value()) // objectAttributes - .write_u16(0) // authPolicy (empty) - // parameters (TPMS_ECC_PARMS) - .write_u16(TpmAlg::Null as u16) // symmetric - .write_u16(TpmAlg::Null as u16) // scheme - .write_u16(TpmEccCurve::NistP256 as u16) // curveID - .write_u16(TpmAlg::Null as u16) // kdf - // unique (TPMS_ECC_POINT) - empty - .write_u16(0) // x size - .write_u16(0) // y size - .into_vec() -} - -/// Build a TPM2B_PUBLIC structure for an ECC P-256 signing key with authPolicy -fn build_ecc_public_area_with_policy(auth_policy: &[u8]) -> Vec { - let attrs = ObjectAttributes::new() - .fixed_tpm() - .fixed_parent() - .sensitive_data_origin() - .user_with_auth() - .decrypt() - .sign_encrypt(); - - CommandBuffer::new() - // TPMT_PUBLIC - .write_u16(TpmAlg::Ecc as u16) // type - .write_u16(TpmAlg::Sha256 as u16) // nameAlg - .write_u32(attrs.value()) // objectAttributes - .write_tpm2b(auth_policy) // authPolicy - // parameters (TPMS_ECC_PARMS) - .write_u16(TpmAlg::Null as u16) // symmetric - .write_u16(TpmAlg::Null as u16) // scheme - .write_u16(TpmEccCurve::NistP256 as u16) // curveID - .write_u16(TpmAlg::Null as u16) // kdf - // unique (TPMS_ECC_POINT) - empty - .write_u16(0) // x size - .write_u16(0) // y size - .into_vec() -} - -/// Build a TPM2B_PUBLIC structure for the TCG standard ECC P-256 EK -/// -/// Per TCG EK Credential Profile 2.6, the standard EK template (Template L-2) has: -/// - Object attributes: 0x000300b2 (fixedTPM, fixedParent, sensitiveDataOrigin, -/// adminWithPolicy, restricted, decrypt) -/// - authPolicy: PolicySecret(TPM_RH_ENDORSEMENT) -/// - Symmetric: AES-128-CFB -/// - Curve: NIST P-256 -/// - Unique: x = 32 zero bytes, y = 32 zero bytes -fn build_standard_ek_public_area() -> Vec { - let attrs = ObjectAttributes::new() - .fixed_tpm() - .fixed_parent() - .sensitive_data_origin() - .admin_with_policy() - .restricted() - .decrypt(); - - // Per TCG EK Credential Profile, unique field must be 32 zero bytes for x and y - let zero_32 = [0u8; 32]; - - CommandBuffer::new() - // TPMT_PUBLIC - .write_u16(TpmAlg::Ecc as u16) // type - .write_u16(TpmAlg::Sha256 as u16) // nameAlg - .write_u32(attrs.value()) // objectAttributes - .write_tpm2b(&STANDARD_EK_AUTH_POLICY) // authPolicy - // parameters (TPMS_ECC_PARMS) - // symmetric (TPMT_SYM_DEF_OBJECT) - AES-128-CFB - .write_u16(TpmAlg::Aes as u16) - .write_u16(128) // keyBits - .write_u16(TpmAlg::Cfb as u16) // mode - .write_u16(TpmAlg::Null as u16) // scheme (decrypt-only, no signing) - .write_u16(TpmEccCurve::NistP256 as u16) // curveID - .write_u16(TpmAlg::Null as u16) // kdf - // unique (TPMS_ECC_POINT) - 32 zero bytes each per TCG template - .write_tpm2b(&zero_32) // x - .write_tpm2b(&zero_32) // y - .into_vec() -} - -/// Parse ECC public key from TPMT_PUBLIC structure -pub(crate) fn parse_ecc_public_key(data: &[u8]) -> Result { - let mut resp = ResponseBuffer::new(data.to_vec()); - - // Parse TPMT_PUBLIC structure - let key_type = resp.read_u16()?; - let name_alg = resp.read_u16()?; - let object_attributes = resp.read_u32()?; - let auth_policy = resp.read_tpm2b()?; - - // Parse parameters (TPMS_ECC_PARMS for ECC keys) - let symmetric = resp.read_u16()?; - - // If symmetric is not NULL, read symmetric details - if symmetric != TpmAlg::Null as u16 { - let _key_bits = resp.read_u16()?; - let _mode = resp.read_u16()?; - } - - let scheme = resp.read_u16()?; - - // Only read scheme details if scheme is not NULL - if scheme != TpmAlg::Null as u16 { - let _scheme_detail = resp.read_u16()?; - } - - let curve_id = resp.read_u16()?; - let kdf = resp.read_u16()?; - - // Read unique (TPMS_ECC_POINT) - let x = resp.read_tpm2b()?; - let y = resp.read_tpm2b()?; - - Ok(EccPublicKey { - key_type, - name_alg, - object_attributes, - auth_policy, - symmetric, - scheme, - curve_id, - kdf, - x, - y, - }) -} - -/// Encode ECDSA signature as DER -pub(crate) fn encode_ecdsa_der_signature(r: &[u8], s: &[u8]) -> Vec { - fn encode_integer(value: &[u8]) -> Vec { - let mut result = vec![0x02]; // INTEGER tag - - // Remove leading zeros - let trimmed: Vec = value.iter().skip_while(|&&b| b == 0).copied().collect(); - - let bytes = if trimmed.is_empty() { - vec![0x00] - } else if trimmed[0] & 0x80 != 0 { - // Add padding byte if high bit is set - let mut v = vec![0x00]; - v.extend_from_slice(&trimmed); - v - } else { - trimmed - }; - - result.push(bytes.len() as u8); - result.extend_from_slice(&bytes); - result - } - - let r_encoded = encode_integer(r); - let s_encoded = encode_integer(s); - - let mut signature = vec![0x30]; // SEQUENCE tag - let content_len = r_encoded.len() + s_encoded.len(); - signature.push(content_len as u8); - signature.extend_from_slice(&r_encoded); - signature.extend_from_slice(&s_encoded); - signature -} diff --git a/crates/rawdogtpm2/src/lib.rs b/crates/rawdogtpm2/src/lib.rs deleted file mode 100644 index 3331a0e..0000000 --- a/crates/rawdogtpm2/src/lib.rs +++ /dev/null @@ -1,589 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! Minimal TPM 2.0 protocol implementation -//! -//! Direct communication with TPM via /dev/tpmrm0 without any C dependencies. -//! Based on TPM 2.0 specification for command/response protocol. - -use anyhow::{Context, Result, bail}; -use std::fs::{File, OpenOptions}; -use std::io::{Read, Write}; - -pub mod a9n; -pub mod credential; -pub mod ek; -pub mod nsm; -pub mod nv; -pub mod pcr; - -// Re-export extension traits for convenience -pub use ek::EkOps; -pub use nsm::NsmOps; -pub use nv::NvOps; -pub use pcr::PcrOps; - -// Re-export credential functions -pub use credential::{ - compute_ecc_p256_name, - ReadPublicResult, -}; - -pub use a9n::{attest, der_to_pem}; - -/// TPM 2.0 command codes -#[repr(u32)] -#[derive(Debug, Clone, Copy)] -pub enum TpmCc { - PcrRead = 0x0000017E, - PcrExtend = 0x00000182, - GetCapability = 0x0000017A, - CreatePrimary = 0x00000131, - Sign = 0x0000015D, - Quote = 0x00000158, - FlushContext = 0x00000165, - NvRead = 0x0000014E, - NvReadPublic = 0x00000169, - NvDefineSpace = 0x0000012A, - NvWrite = 0x00000137, - NvUndefineSpace = 0x00000122, - PolicyPCR = 0x0000017F, - PolicySecret = 0x00000151, - PolicyGetDigest = 0x00000189, - Certify = 0x00000148, - ActivateCredential = 0x00000147, - MakeCredential = 0x00000168, - StartAuthSession = 0x00000176, - ReadPublic = 0x00000173, -} - -/// TPM 2.0 structure tags -#[repr(u16)] -#[derive(Debug, Clone, Copy)] -pub enum TpmSt { - NoSessions = 0x8001, - Sessions = 0x8002, -} - -/// TPM 2.0 return codes -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum TpmRc { - Success = 0x000, -} - -/// TPM 2.0 algorithm identifiers -#[repr(u16)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TpmAlg { - Sha1 = 0x0004, - Sha256 = 0x000B, - Sha384 = 0x000C, - Sha512 = 0x000D, - Aes = 0x0006, - Cfb = 0x0043, - Ecc = 0x0023, - EcDsa = 0x0018, - Null = 0x0010, -} - -impl TpmAlg { - /// Get the digest size in bytes for hash algorithms - pub fn digest_size(&self) -> Option { - match self { - TpmAlg::Sha1 => Some(20), - TpmAlg::Sha256 => Some(32), - TpmAlg::Sha384 => Some(48), - TpmAlg::Sha512 => Some(64), - _ => None, - } - } - - /// Get the algorithm name as a string - pub fn name(&self) -> &'static str { - match self { - TpmAlg::Sha1 => "sha1", - TpmAlg::Sha256 => "sha256", - TpmAlg::Sha384 => "sha384", - TpmAlg::Sha512 => "sha512", - TpmAlg::Aes => "aes", - TpmAlg::Cfb => "cfb", - TpmAlg::Ecc => "ecc", - TpmAlg::EcDsa => "ecdsa", - TpmAlg::Null => "null", - } - } - - /// Try to convert a u16 to a TpmAlg - pub fn from_u16(val: u16) -> Option { - match val { - 0x0004 => Some(TpmAlg::Sha1), - 0x000B => Some(TpmAlg::Sha256), - 0x000C => Some(TpmAlg::Sha384), - 0x000D => Some(TpmAlg::Sha512), - 0x0023 => Some(TpmAlg::Ecc), - 0x0018 => Some(TpmAlg::EcDsa), - 0x0010 => Some(TpmAlg::Null), - _ => None, - } - } -} - -impl TryFrom for TpmAlg { - type Error = (); - - fn try_from(val: u16) -> Result { - Self::from_u16(val).ok_or(()) - } -} - -/// TPM handle for the owner hierarchy -pub const TPM_RH_OWNER: u32 = 0x40000001; - -/// TPM handle for the endorsement hierarchy -pub const TPM_RH_ENDORSEMENT: u32 = 0x4000000B; - -/// TPM handle representing a null handle -pub const TPM_RH_NULL: u32 = 0x40000007; - -/// Password authorization session handle -pub const TPM_RS_PW: u32 = 0x40000009; - -/// NV index for RSA-2048 EK certificate (standard location) -pub const NV_INDEX_RSA_2048_EK_CERT: u32 = 0x01C00002; - -/// NV index for ECC-P256 EK certificate (standard location) -pub const NV_INDEX_ECC_P256_EK_CERT: u32 = 0x01C0000A; - -/// NV index for ECC-P384 EK certificate (standard location) -pub const NV_INDEX_ECC_P384_EK_CERT: u32 = 0x01C00016; - -/// TPM capability types -pub const TPM_CAP_HANDLES: u32 = 0x00000001; -pub const TPM_CAP_PCRS: u32 = 0x00000005; -pub const TPM_CAP_TPM_PROPERTIES: u32 = 0x00000006; - -/// TPM fixed property identifiers (TPM_PT) -pub const TPM_PT_VENDOR_STRING_1: u32 = 0x00000106; -pub const TPM_PT_VENDOR_STRING_2: u32 = 0x00000107; - -/// Session types -pub const TPM_SE_HMAC: u8 = 0x00; -pub const TPM_SE_POLICY: u8 = 0x01; -pub const TPM_SE_TRIAL: u8 = 0x03; - -/// Algorithm IDs -pub const TPM_ALG_NULL: u16 = 0x0010; -pub const TPM_ALG_SHA256: u16 = 0x000B; -pub const TPM_ALG_SHA512: u16 = 0x000D; - -/// ECC curve identifiers -#[repr(u16)] -#[derive(Debug, Clone, Copy)] -pub enum TpmEccCurve { - NistP256 = 0x0003, -} - -/// Object attributes -pub struct ObjectAttributes(u32); - -impl Default for ObjectAttributes { - fn default() -> Self { - Self::new() - } -} - -impl ObjectAttributes { - pub fn new() -> Self { - Self(0) - } - - pub fn fixed_tpm(mut self) -> Self { - self.0 |= 1 << 1; - self - } - - pub fn fixed_parent(mut self) -> Self { - self.0 |= 1 << 4; - self - } - - pub fn sensitive_data_origin(mut self) -> Self { - self.0 |= 1 << 5; - self - } - - pub fn user_with_auth(mut self) -> Self { - self.0 |= 1 << 6; - self - } - - pub fn decrypt(mut self) -> Self { - self.0 |= 1 << 17; - self - } - - pub fn sign_encrypt(mut self) -> Self { - self.0 |= 1 << 18; - self - } - - pub fn admin_with_policy(mut self) -> Self { - self.0 |= 1 << 7; - self - } - - pub fn restricted(mut self) -> Self { - self.0 |= 1 << 16; - self - } - - pub fn value(&self) -> u32 { - self.0 - } -} - -/// TPM 2.0 command header -#[derive(Debug)] -struct TpmCommandHeader { - tag: TpmSt, - size: u32, - code: TpmCc, -} - -impl TpmCommandHeader { - /// Create a new command header - fn new(tag: TpmSt, size: u32, code: TpmCc) -> Self { - Self { tag, size, code } - } - - /// Serialize to bytes - fn to_bytes(&self) -> [u8; 10] { - let mut bytes = [0u8; 10]; - bytes[0..2].copy_from_slice(&(self.tag as u16).to_be_bytes()); - bytes[2..6].copy_from_slice(&self.size.to_be_bytes()); - bytes[6..10].copy_from_slice(&(self.code as u32).to_be_bytes()); - bytes - } -} - -/// TPM 2.0 response header -#[derive(Debug)] -pub struct TpmResponseHeader { - pub tag: u16, - pub size: u32, - pub code: u32, -} - -impl TpmResponseHeader { - /// Parse a response header from a 10-byte buffer - fn from_bytes(bytes: &[u8; 10]) -> Self { - let tag = u16::from_be_bytes([bytes[0], bytes[1]]); - let size = u32::from_be_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]); - let code = u32::from_be_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]); - Self { tag, size, code } - } -} - -/// Helper for building TPM commands with big-endian serialization -pub(crate) struct CommandBuffer { - data: Vec, -} - -impl CommandBuffer { - fn new() -> Self { - Self { data: Vec::new() } - } - - fn write_u8(mut self, val: u8) -> Self { - self.data.push(val); - self - } - - fn write_u16(mut self, val: u16) -> Self { - self.data.extend_from_slice(&val.to_be_bytes()); - self - } - - fn write_u32(mut self, val: u32) -> Self { - self.data.extend_from_slice(&val.to_be_bytes()); - self - } - - fn write_bytes(mut self, bytes: &[u8]) -> Self { - self.data.extend_from_slice(bytes); - self - } - - /// Write a TPM2B (size-prefixed buffer) - fn write_tpm2b(mut self, bytes: &[u8]) -> Self { - self.data.extend_from_slice(&(bytes.len() as u16).to_be_bytes()); - self.data.extend_from_slice(bytes); - self - } - - fn write_auth_empty_pw(self) -> Self { - // Authorization area (password session with empty password) - // Size = 4 (sessionHandle) + 2 (nonce) + 1 (attributes) + 2 (password) = 9 bytes - self.write_u32(9) // authorizationSize - .write_u32(TPM_RS_PW) // sessionHandle - password session - .write_u16(0) // nonce - empty - .write_u8(0) // sessionAttributes - continue session - .write_u16(0) // password/hmac - empty - } - - fn finalize(mut self, tag: TpmSt, code: TpmCc) -> Vec { - let total_size = 10 + self.data.len(); // header is 10 bytes - let header = TpmCommandHeader::new(tag, total_size as u32, code); - let mut result = Vec::new(); - result.extend_from_slice(&header.to_bytes()); - result.append(&mut self.data); - result - } - - /// Finalize command with a vendor-specific command code - fn finalize_vendor(mut self, tag: TpmSt, vendor_code: u32) -> Vec { - let total_size = 10 + self.data.len(); // header is 10 bytes - let mut result = Vec::new(); - result.extend_from_slice(&(tag as u16).to_be_bytes()); - result.extend_from_slice(&(total_size as u32).to_be_bytes()); - result.extend_from_slice(&vendor_code.to_be_bytes()); - result.append(&mut self.data); - result - } - - /// Convert to raw bytes without finalizing as a command - /// - /// Use this when building non-command data structures like TPM2B_PUBLIC - fn into_vec(self) -> Vec { - self.data - } -} - -/// Helper for parsing TPM responses -pub struct ResponseBuffer { - data: Vec, - offset: usize, -} - -impl ResponseBuffer { - fn new(data: Vec) -> Self { - Self { data, offset: 0 } - } - - fn read_u8(&mut self) -> Result { - if self.offset >= self.data.len() { - bail!("Response buffer underflow"); - } - let val = self.data[self.offset]; - self.offset += 1; - Ok(val) - } - - fn read_u16(&mut self) -> Result { - if self.offset + 2 > self.data.len() { - bail!("Response buffer underflow"); - } - let val = u16::from_be_bytes([self.data[self.offset], self.data[self.offset + 1]]); - self.offset += 2; - Ok(val) - } - - fn read_u32(&mut self) -> Result { - if self.offset + 4 > self.data.len() { - bail!("Response buffer underflow"); - } - let val = u32::from_be_bytes([ - self.data[self.offset], - self.data[self.offset + 1], - self.data[self.offset + 2], - self.data[self.offset + 3], - ]); - self.offset += 4; - Ok(val) - } - - fn read_bytes(&mut self, len: usize) -> Result<&[u8]> { - if self.offset + len > self.data.len() { - bail!("Response buffer underflow: trying to read {} bytes at offset {}, but only {} bytes total (remaining: {})", - len, self.offset, self.data.len(), self.remaining()); - } - let bytes = &self.data[self.offset..self.offset + len]; - self.offset += len; - Ok(bytes) - } - - /// Read a TPM2B (size-prefixed buffer) - fn read_tpm2b(&mut self) -> Result> { - let size = self.read_u16()? as usize; - Ok(self.read_bytes(size)?.to_vec()) - } - - fn remaining(&self) -> usize { - self.data.len() - self.offset - } - - fn offset(&self) -> usize { - self.offset - } -} - -/// TPM 2.0 device context -pub struct Tpm { - device: File, -} - -impl Tpm { - /// Open the TPM device (defaults to /dev/tpmrm0) - pub fn open() -> Result { - Self::open_path("/dev/tpmrm0") - } - - /// Open a specific TPM device path - pub fn open_path(path: &str) -> Result { - let device = OpenOptions::new() - .read(true) - .write(true) - .open(path) - .context(format!("Failed to open TPM device at {}", path))?; - - Ok(Self { device }) - } - - /// Open direct TPM device (/dev/tpm0) - required for vendor commands - pub fn open_direct() -> Result { - Self::open_path("/dev/tpm0") - } - - /// Send a command and receive response - /// - /// Returns a ResponseBuffer containing the response body (without the header) - pub(crate) fn transmit(&mut self, command: &[u8]) -> Result { - // Write command - self.device.write_all(command) - .context("Failed to write TPM command")?; - - // Read response header first (10 bytes) - let mut header_buf = [0u8; 10]; - self.device.read_exact(&mut header_buf) - .context("Failed to read TPM response header")?; - - // Parse response header - let header = TpmResponseHeader::from_bytes(&header_buf); - - if header.size < 10 { - bail!("Invalid TPM response size: {}", header.size); - } - - // Read response body (excluding header) - let body_size = header.size as usize - 10; - let mut body = vec![0u8; body_size]; - self.device.read_exact(&mut body) - .context("Failed to read TPM response body")?; - - // Check response code - if header.code != TpmRc::Success as u32 { - bail!("TPM command failed with code: 0x{:08X}", header.code); - } - - Ok(ResponseBuffer::new(body)) - } - - /// Flush a context (close a handle) - pub fn flush_context(&mut self, handle: u32) -> Result<()> { - let command = CommandBuffer::new() - .write_u32(handle) - .finalize(TpmSt::NoSessions, TpmCc::FlushContext); - self.transmit(&command)?; - - Ok(()) - } - - /// Execute TPM2_GetCapability command (internal helper) - /// - /// Returns (more_data, ResponseBuffer) positioned after capability verification. - /// The ResponseBuffer is ready to parse capability-specific data. - pub fn get_capability(&mut self, capability: u32, property: u32, property_count: u32) -> Result<(bool, ResponseBuffer)> { - let command = CommandBuffer::new() - .write_u32(capability) - .write_u32(property) - .write_u32(property_count) - .finalize(TpmSt::NoSessions, TpmCc::GetCapability); - let mut resp = self.transmit(&command)?; - - // Parse common response fields - let more_data = resp.read_u8()? != 0; - let returned_capability = resp.read_u32()?; - if returned_capability != capability { - bail!("Unexpected capability type: 0x{:08X} (expected 0x{:08X})", - returned_capability, capability); - } - - Ok((more_data, resp)) - } - - /// Get TPM fixed properties - /// - /// Query a specific TPM property value - pub fn get_property(&mut self, property: u32) -> Result { - let (_more_data, mut resp) = self.get_capability(TPM_CAP_TPM_PROPERTIES, property, 1)?; - - // Parse capability-specific data: TPML_TAGGED_TPM_PROPERTY - let count = resp.read_u32()?; - if count == 0 { - bail!("Property 0x{:08X} not found", property); - } - - // TPMS_TAGGED_PROPERTY - let returned_property = resp.read_u32()?; - if returned_property != property { - bail!("Unexpected property type: 0x{:08X} (expected 0x{:08X})", - returned_property, property); - } - - Ok(resp.read_u32()?) - } - - /// Check if this is an AWS Nitro TPM - /// - /// Returns true if the vendor string matches "NitroTPM*" - pub fn is_nitro_tpm(&mut self) -> Result { - // Expected vendor string parts: "Nitr" + "oTPM" - const NITRO_VENDOR_STRING_1: u32 = 0x4E697472; // "Nitr" - const NITRO_VENDOR_STRING_2: u32 = 0x6F54504D; // "oTPM" - - let vendor_str_1 = self.get_property(TPM_PT_VENDOR_STRING_1)?; - let vendor_str_2 = self.get_property(TPM_PT_VENDOR_STRING_2)?; - - Ok(vendor_str_1 == NITRO_VENDOR_STRING_1 && vendor_str_2 == NITRO_VENDOR_STRING_2) - } - -} - -/// ECC public key information parsed from TPMT_PUBLIC -#[derive(Debug, Clone)] -pub struct EccPublicKey { - pub key_type: u16, - pub name_alg: u16, - pub object_attributes: u32, - pub auth_policy: Vec, - pub symmetric: u16, - pub scheme: u16, - pub curve_id: u16, - pub kdf: u16, - pub x: Vec, - pub y: Vec, -} - -/// Result from creating a primary key -pub struct PrimaryKeyResult { - pub handle: u32, - pub public_key: EccPublicKey, -} - -/// Result from TPM2_Certify -#[derive(Debug)] -pub struct CertifyResult { - pub attest_data: Vec, // TPMS_ATTEST structure - pub signature: Vec, // DER-encoded ECDSA signature -} - diff --git a/crates/rawdogtpm2/src/nsm.rs b/crates/rawdogtpm2/src/nsm.rs deleted file mode 100644 index 570f1f0..0000000 --- a/crates/rawdogtpm2/src/nsm.rs +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! AWS Nitro Secure Module (NSM) API - Clean Implementation -//! -//! Single-approach implementation based on TPM 2.0 spec and AWS trace analysis. -//! No "try everything" - either works correctly or fails with clear diagnostics. - -use crate::{Tpm, TpmSt, CommandBuffer, TPM_ALG_SHA256}; -use crate::nv::{NV_INDEX_USER_START, NV_INDEX_USER_END, NvOps, TPM2_PT_NV_BUFFER_MAX}; -use crate::nv::{TPMA_NV_AUTHWRITE, TPMA_NV_AUTHREAD}; -use anyhow::{Result, bail}; -use serde::{Deserialize, Serialize}; - -/// NSM Request types -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Request { - Attestation { - user_data: Option>, - nonce: Option>, - public_key: Option>, - }, -} - -/// Digest algorithm used by NSM -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Digest { - SHA256, - SHA384, - SHA512, -} - -/// NSM Response types -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub enum Response { - Attestation { - #[serde(rename = "document", with = "serde_bytes")] - document: Vec, - }, - Error(ErrorCode), -} - -/// NSM Error codes -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ErrorCode { - InvalidArgument, - InvalidIndex, - InvalidResponse, - ReadOnlyIndex, - InvalidOperation, - BufferTooSmall, - InputTooLarge, - InternalError, -} - -impl Request { - pub fn to_bytes(&self) -> Result> { - let mut buf = Vec::new(); - ciborium::into_writer(self, &mut buf) - .map_err(|e| anyhow::anyhow!("Failed to serialize NSM request: {}", e))?; - Ok(buf) - } -} - -impl Response { - pub fn from_bytes(data: &[u8]) -> Result { - // ciborium::from_reader handles trailing bytes (0xFF padding) gracefully - - // it stops after parsing one complete CBOR object - ciborium::from_reader(data) - .map_err(|e| anyhow::anyhow!("Failed to deserialize NSM response: {}", e)) - } -} - -/// AWS Nitro TPM vendor-specific command for NSM requests -pub const TPM2_VENDOR_AWS_NSM_REQUEST: u32 = 0x20000001; - -/// Extension trait for AWS Nitro Security Module (NSM) operations -pub trait NsmOps { - /// Request an attestation document from NSM - /// - /// This requests an attestation document from the AWS Nitro Secure Module. - /// The attestation document is a signed CBOR structure that includes: - /// - PCR values from the TPM - /// - Optional user data (up to 512 bytes) - /// - Optional nonce (up to 512 bytes) - /// - Optional public key (DER-encoded) - /// - /// # Arguments - /// * `user_data` - Optional user-provided data to include in attestation - /// * `nonce` - Optional nonce/challenge for freshness - /// * `public_key` - Optional public key to include in attestation - /// - /// # Returns - /// The attestation document as a CBOR-encoded byte vector - fn nsm_attest( - &mut self, - user_data: Option>, - nonce: Option>, - public_key: Option>, - ) -> Result>; -} - -/// Execute AWS NSM vendor command with password authentication -fn nsm_vendor_command( - tpm: &mut Tpm, - nv_index: u32 -) -> Result<()> { - // Use password session with empty password - let command = CommandBuffer::new() - .write_u32(nv_index) // nvAuth - .write_u32(nv_index) // nvIndex - .write_auth_empty_pw() - .finalize_vendor(TpmSt::Sessions, TPM2_VENDOR_AWS_NSM_REQUEST); - tpm.transmit(&command)?; - Ok(()) -} - -/// Execute AWS Nitro Security Module (NSM) request -fn nsm_raw_request(tpm: &mut Tpm, request_data: &[u8]) -> Result> { - // Query TPM's maximum NV buffer size (for writes) - let max_nv_bufsz = tpm.get_property(TPM2_PT_NV_BUFFER_MAX)?; - if request_data.len() > max_nv_bufsz as usize { - bail!("NSM request too large: {} bytes (TPM max buffer: {} bytes)", - request_data.len(), max_nv_bufsz); - } - - // Find a free NV index - let nv_index = tpm.nv_find_free_index(NV_INDEX_USER_START, NV_INDEX_USER_END)?; - - // Use a guard to ensure cleanup - struct NvGuard<'a> { - tpm: &'a mut Tpm, - nv_index: u32, - } - - impl Drop for NvGuard<'_> { - fn drop(&mut self) { - let _ = self.tpm.nv_undefine_space(self.nv_index); - } - } - - // The NV space must be large enough to accommodate max(len(req),len(resp)) - tpm.nv_define_space(nv_index, 8192, TPMA_NV_AUTHWRITE | TPMA_NV_AUTHREAD, TPM_ALG_SHA256)?; - - let guard = NvGuard { tpm, nv_index }; - - // Write request data - guard.tpm.nv_write(nv_index, request_data)?; - nsm_vendor_command(guard.tpm, nv_index)?; - - // Read response - let response = guard.tpm.nv_read(nv_index)?; - drop(guard); - Ok(response) -} - -impl NsmOps for Tpm { - /// Request an attestation document from NSM - fn nsm_attest( - &mut self, - user_data: Option>, - nonce: Option>, - public_key: Option>, - ) -> Result> { - let request = Request::Attestation { - user_data, - nonce, - public_key, - }; - let request_bytes = request.to_bytes()?; - - let response_bytes = nsm_raw_request(self, &request_bytes)?; - - let response = Response::from_bytes(&response_bytes)?; - - match response { - Response::Attestation{document} => Ok(document), - Response::Error(err) => bail!("NSM returned error: {:?}", err), - } - } -} diff --git a/crates/rawdogtpm2/src/nv.rs b/crates/rawdogtpm2/src/nv.rs deleted file mode 100644 index f4da0c6..0000000 --- a/crates/rawdogtpm2/src/nv.rs +++ /dev/null @@ -1,293 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! NV (Non-Volatile) RAM operations -//! -//! Extension trait providing NV-related functionality for TPM. - -use crate::{Tpm, TpmCc, TpmSt, CommandBuffer}; -use crate::{TPM_RH_OWNER, TPM_CAP_HANDLES}; -use anyhow::{Result, bail}; - -/// TPM handle types -pub const TPM_HT_NV_INDEX: u32 = 0x01000000; - -/// TPM property identifiers -pub const TPM2_PT_NV_BUFFER_MAX: u32 = 0x0000010D; // Max NV buffer size (different from index max) -pub const TPM2_PT_NV_INDEX_MAX: u32 = 0x00000112; // Max NV index data size - -/// NV index range for user-defined indices (0x01800000 - 0x01BFFFFF) -pub const NV_INDEX_USER_START: u32 = 0x01800000; -pub const NV_INDEX_USER_END: u32 = 0x01BFFFFF; - -/// NV attribute bits -pub const TPMA_NV_PPWRITE: u32 = 1 << 0; // Platform hierarchy can write -pub const TPMA_NV_OWNERWRITE: u32 = 1 << 1; // Owner hierarchy can write -pub const TPMA_NV_AUTHWRITE: u32 = 1 << 2; // Authorizations to write are allowed -pub const TPMA_NV_POLICYWRITE: u32 = 1 << 3; // Policy can be used to authorize write -pub const TPMA_NV_PPREAD: u32 = 1 << 16; // Platform hierarchy can read -pub const TPMA_NV_OWNERREAD: u32 = 1 << 17; // Owner hierarchy can read -pub const TPMA_NV_AUTHREAD: u32 = 1 << 18; // Authorizations to read are allowed -pub const TPMA_NV_POLICYREAD: u32 = 1 << 19; // Policy can be used to authorize read -pub const TPMA_NV_NO_DA: u32 = 1 << 25; // Authorization failures do not affect DA logic -pub const TPMA_NV_ORDERLY: u32 = 1 << 26; // NV Index state is only required to be saved on orderly shutdown - -/// NV index public information -#[derive(Debug)] -pub struct NvPublicInfo { - pub nv_index: u32, - pub name_alg: u16, - pub attributes: u32, - pub auth_policy: Vec, - pub data_size: u16, - pub name: Vec, -} - -/// Extension trait for NV RAM operations -pub trait NvOps { - /// Get list of defined NV indices - fn nv_indices(&mut self) -> Result>; - - /// Get NV index public information - fn nv_readpublic(&mut self, nv_index: u32) -> Result; - - /// Read a raw block from NV RAM (low-level, no authentication) - fn nv_read_raw_block(&mut self, nv_index: u32, size: u16, offset: u16) -> Result>; - - /// Read data from NV RAM (handles multi-block reads automatically) - fn nv_read(&mut self, nv_index: u32) -> Result>; - - /// Define an NV space with the given attributes and authorization value - fn nv_define_space( - &mut self, - nv_index: u32, - data_size: u16, - attributes: u32, - name_alg: u16 - ) -> Result<()>; - - /// Write data to NV RAM - fn nv_write( - &mut self, - nv_index: u32, - data: &[u8] - ) -> Result<()>; - - /// Undefine (delete) an NV space - fn nv_undefine_space(&mut self, nv_index: u32) -> Result<()>; - - /// Find a free NV index in the specified range - fn nv_find_free_index(&mut self, start: u32, end: u32) -> Result; -} - -impl NvOps for Tpm { - /// Get list of defined NV indices - fn nv_indices(&mut self) -> Result> { - let mut all_handles = Vec::new(); - let mut more_data = true; - let mut property = TPM_HT_NV_INDEX; // Start from first NV index - - while more_data { - let (has_more, mut resp) = self.get_capability(TPM_CAP_HANDLES, property, 64)?; - more_data = has_more; - - // Parse capability-specific data: TPML_HANDLE - let count = resp.read_u32()?; - - for _ in 0..count { - let handle = resp.read_u32()?; - all_handles.push(handle); - property = handle + 1; // Next query starts after this handle - } - - // If we got no handles, stop even if more_data is set - if count == 0 { - break; - } - } - - Ok(all_handles) - } - - /// Get information about an NV index (size, attributes, policy) - fn nv_readpublic(&mut self, nv_index: u32) -> Result { - let command = CommandBuffer::new() - .write_u32(nv_index) // nvIndex - .finalize(TpmSt::NoSessions, TpmCc::NvReadPublic); - let mut resp = self.transmit(&command)?; - - // Parse response - // nvPublic (TPM2B_NV_PUBLIC) - let _nv_public_size = resp.read_u16()? as usize; - - // TPMS_NV_PUBLIC structure - let nv_index_ret = resp.read_u32()?; - let name_alg = resp.read_u16()?; - let attributes = resp.read_u32()?; - - // authPolicy (TPM2B_DIGEST) - let auth_policy = resp.read_tpm2b()?; - - // dataSize - let data_size = resp.read_u16()?; - - // nvName (TPM2B_NAME) - let name = resp.read_tpm2b()?; - - Ok(NvPublicInfo { - nv_index: nv_index_ret, - name_alg, - attributes, - auth_policy, - data_size, - name, - }) - } - - /// Read a block of data from NV RAM at the specified index and offset - fn nv_read_raw_block(&mut self, nv_index: u32, size: u16, offset: u16) -> Result> { - let command = CommandBuffer::new() - .write_u32(nv_index) // authHandle (use the nv_index itself for public read) - .write_u32(nv_index) // nvIndex - // Authorization area (empty password session) - .write_auth_empty_pw() - .write_u16(size) // size - .write_u16(offset) // offset - .finalize(TpmSt::Sessions, TpmCc::NvRead); - let mut resp = self.transmit(&command)?; - - // Parse response - // Skip parameterSize (sessions response) - let _parameter_size = resp.read_u32()?; - - // Read data (TPM2B_MAX_NV_BUFFER) - let data = resp.read_tpm2b()?; - - Ok(data) - } - - /// Read complete contents from an NV index (handles chunking automatically) - fn nv_read(&mut self, nv_index: u32) -> Result> { - // First, get the actual size of the NV index - let nv_info = self.nv_readpublic(nv_index)?; - - // Read in chunks of 512 bytes (safe for most TPMs) - const MAX_CHUNK: u16 = 512; - let mut result = Vec::new(); - let mut offset = 0u16; - let total_size = nv_info.data_size; - - while offset < total_size { - let bytes_remaining = total_size - offset; - let chunk_size = if bytes_remaining > MAX_CHUNK { - MAX_CHUNK - } else { - bytes_remaining - }; - - let mut chunk = self.nv_read_raw_block(nv_index, chunk_size, offset)?; - - if chunk.is_empty() { - bail!("Unexpected empty chunk at offset {}", offset); - } - - offset += chunk.len() as u16; - result.append(&mut chunk); - } - - Ok(result) - } - - /// Define (allocate) a new NV index - /// - /// Creates a new NV index with the specified size and attributes. - /// The auth_value is used to protect read/write operations. - fn nv_define_space( - &mut self, - nv_index: u32, - data_size: u16, - attributes: u32, - name_alg: u16 - ) -> Result<()> { - // Build TPM2B_NV_PUBLIC structure - let mut nv_public = Vec::new(); - - // TPMS_NV_PUBLIC - nv_public.extend_from_slice(&nv_index.to_be_bytes()); // nvIndex - nv_public.extend_from_slice(&name_alg.to_be_bytes()); // nameAlg - nv_public.extend_from_slice(&attributes.to_be_bytes()); // attributes - nv_public.extend_from_slice(&0u16.to_be_bytes()); // authPolicy size = 0 (empty) - nv_public.extend_from_slice(&data_size.to_be_bytes()); // dataSize - - let command = CommandBuffer::new() - .write_u32(TPM_RH_OWNER) // authHandle (owner hierarchy) - .write_auth_empty_pw() - .write_tpm2b(&Vec::new()) // auth (TPM2B_AUTH) - the authorization value for the NV index - .write_tpm2b(&nv_public) // publicInfo (TPM2B_NV_PUBLIC) - .finalize(TpmSt::Sessions, TpmCc::NvDefineSpace); - - self.transmit(&command)?; - Ok(()) - } - - /// Write data to an NV index - /// - /// Writes data to the NV index. For large data, this handles chunking automatically. - /// The auth_value must match what was used in nv_define_space. - fn nv_write( - &mut self, - nv_index: u32, - data: &[u8] - ) -> Result<()> { - // Write in chunks (max 512 bytes per write for compatibility) - const MAX_CHUNK: usize = 512; - let mut offset = 0usize; - - while offset < data.len() { - let chunk_size = std::cmp::min(MAX_CHUNK, data.len() - offset); - let chunk = &data[offset..offset + chunk_size]; - - let command = CommandBuffer::new() - .write_u32(nv_index) // authHandle (the NV index itself) - .write_u32(nv_index) // nvIndex - .write_auth_empty_pw() - .write_tpm2b(chunk) // data (TPM2B_MAX_NV_BUFFER) - .write_u16(offset as u16) - .finalize(TpmSt::Sessions, TpmCc::NvWrite); - - self.transmit(&command)?; - offset += chunk_size; - } - - Ok(()) - } - - /// Undefine (delete) an NV index - /// - /// Removes the NV index and frees its storage. - fn nv_undefine_space(&mut self, nv_index: u32) -> Result<()> { - let command = CommandBuffer::new() - .write_u32(TPM_RH_OWNER) // authHandle (owner hierarchy) - .write_u32(nv_index) // nvIndex - .write_auth_empty_pw() - .finalize(TpmSt::Sessions, TpmCc::NvUndefineSpace); - - self.transmit(&command)?; - Ok(()) - } - - /// Find a free NV index in the specified range - /// - /// Searches for an available NV index that is not currently defined. - /// Returns the first free index found, or an error if none are available. - fn nv_find_free_index(&mut self, start: u32, end: u32) -> Result { - let defined_indices = self.nv_indices()?; - - for candidate in start..=end { - if !defined_indices.contains(&candidate) { - return Ok(candidate); - } - } - - bail!("No free NV index found in range 0x{:08X}-0x{:08X}", start, end) - } -} diff --git a/crates/rawdogtpm2/src/pcr.rs b/crates/rawdogtpm2/src/pcr.rs deleted file mode 100644 index c643dca..0000000 --- a/crates/rawdogtpm2/src/pcr.rs +++ /dev/null @@ -1,425 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! PCR (Platform Configuration Register) operations -//! -//! Extension trait providing PCR-related functionality for TPM. - -use crate::{Tpm, TpmAlg, TpmCc, TpmSt, CommandBuffer, TPM_CAP_PCRS}; -use anyhow::{Result, bail}; -use sha1::Sha1; -use sha2::{Sha256, Sha384, Sha512, Digest}; - -/// Extension trait for PCR operations -pub trait PcrOps { - /// Get list of active PCR banks (algorithms) - fn get_active_pcr_banks(&mut self) -> Result>; - - /// Get PCR allocation info: which PCRs exist in which banks - /// Returns Vec of (algorithm, Vec) - fn get_pcr_allocation(&mut self) -> Result)>>; - - /// Read PCR values from a specific bank/algorithm - /// Returns (pcr_index, value) tuples - fn pcr_read_bank(&mut self, pcr_indices: &[u8], alg: TpmAlg) -> Result)>>; - - /// Read PCR values from all active banks - /// Returns (pcr_index, algorithm, value) tuples - fn pcr_read_all_banks(&mut self, pcr_indices: &[u8]) -> Result)>>; - - /// Get which banks (algorithms) a specific PCR is allocated in - fn get_pcr_allocated_banks(&mut self, pcr_index: u8) -> Result>; - - /// Extend a PCR in a specific bank with a digest value - fn pcr_extend_bank(&mut self, pcr_index: u8, alg: TpmAlg, digest: &[u8]) -> Result<()>; - - /// Extend a PCR with arbitrary data (hashes the data for all active banks) - fn pcr_extend(&mut self, pcr_index: u8, data: &[u8]) -> Result<()>; - - /// Read all PCRs from all active banks and return only non-zero ones - /// Returns (pcr_index, algorithm, value) tuples - fn read_nonzero_pcrs_all_banks(&mut self) -> Result)>>; - - /// Get list of all allocated PCRs and which banks they're in - /// Returns (pcr_index, Vec) tuples - fn get_allocated_pcrs(&mut self) -> Result)>>; - - /// Read all allocated PCRs from all banks - /// Returns (pcr_index, algorithm, value) tuples - fn read_all_allocated_pcrs(&mut self) -> Result)>>; - - /// Calculate PCR policy digest for the given PCR values - /// - /// # Arguments - /// * `pcr_values` - The PCR index and value pairs - /// * `pcr_alg` - The PCR bank algorithm (determines which bank is referenced in policy) - fn calculate_pcr_policy_digest(pcr_values: &[(u8, Vec)], pcr_alg: TpmAlg) -> Result>; -} - -impl PcrOps for Tpm { - fn get_active_pcr_banks(&mut self) -> Result> { - let pcr_allocation = self.get_pcr_allocation()?; - Ok(pcr_allocation.into_iter().map(|(alg, _)| alg).collect()) - } - - /// Get PCR allocation info: which PCRs exist in which banks - /// Returns Vec of (algorithm, Vec) - fn get_pcr_allocation(&mut self) -> Result)>> { - let (_more_data, mut resp) = self.get_capability(TPM_CAP_PCRS, 0, 16)?; - // Note: 16 banks is way more than any TPM has, so we ignore moreData - - // Parse capability-specific data: TPML_PCR_SELECTION - let count = resp.read_u32()?; - let mut allocation = Vec::new(); - - for _ in 0..count { - let hash_alg = resp.read_u16()?; - let select_size = resp.read_u8()? as usize; - let pcr_select = resp.read_bytes(select_size)?; - - // Parse the PCR selection bitmap to get allocated PCR indices - let mut pcr_indices = Vec::new(); - for byte_idx in 0..select_size { - for bit_idx in 0..8 { - if pcr_select[byte_idx] & (1 << bit_idx) != 0 { - let pcr_num = (byte_idx * 8 + bit_idx) as u8; - pcr_indices.push(pcr_num); - } - } - } - - // Convert to TpmAlg and add to list if valid - if let Some(alg) = TpmAlg::from_u16(hash_alg) { - allocation.push((alg, pcr_indices)); - } - } - - Ok(allocation) - } - - /// Read PCR values from a specific hash algorithm bank - fn pcr_read_bank(&mut self, pcr_indices: &[u8], alg: TpmAlg) -> Result)>> { - // PCR select bitmap (3 bytes for PCRs 0-23) - // Note: Most TPMs only support 3 bytes even though spec allows 4 - let mut pcr_select = [0u8; 3]; - for &pcr in pcr_indices { - if pcr < 24 { - pcr_select[pcr as usize / 8] |= 1 << (pcr % 8); - } - } - - let command = CommandBuffer::new() - // TPML_PCR_SELECTION count - .write_u32(1) - // TPMS_PCR_SELECTION - .write_u16(alg as u16) // hash algorithm - .write_u8(3) // sizeofSelect (3 bytes for PCRs 0-23) - // PCR select bitmap - .write_bytes(&pcr_select) - .finalize(TpmSt::NoSessions, TpmCc::PcrRead); - let mut resp = self.transmit(&command)?; - - // Parse response - // Skip pcrUpdateCounter - resp.read_u32()?; - - // Read TPML_PCR_SELECTION to see which PCRs are actually in the response - let sel_count = resp.read_u32()?; - let mut selected_pcrs = Vec::new(); - - for _ in 0..sel_count { - resp.read_u16()?; // hash alg (should match our request) - let select_size = resp.read_u8()? as usize; - let pcr_select = resp.read_bytes(select_size)?; - - // Parse bitmap to get actual PCR indices in response - for byte_idx in 0..select_size { - for bit_idx in 0..8 { - if pcr_select[byte_idx] & (1 << bit_idx) != 0 { - let pcr_num = (byte_idx * 8 + bit_idx) as u8; - selected_pcrs.push(pcr_num); - } - } - } - } - - // Read TPML_DIGEST - these correspond to selected_pcrs in order - let digest_count = resp.read_u32()?; - let mut results = Vec::new(); - - for i in 0..digest_count { - let digest = resp.read_tpm2b()?; - if (i as usize) < selected_pcrs.len() { - results.push((selected_pcrs[i as usize], digest)); - } - } - - Ok(results) - } - - /// Read PCR values from all active banks - /// Returns (pcr_index, algorithm, value) tuples - /// - /// Note: Only returns PCRs that are actually allocated in each bank. - /// A PCR might not exist in all banks. - fn pcr_read_all_banks(&mut self, pcr_indices: &[u8]) -> Result)>> { - let banks = self.get_active_pcr_banks()?; - let mut all_results = Vec::new(); - - for bank in banks { - // Try to read from this bank, but don't fail if PCR doesn't exist in this bank - match self.pcr_read_bank(pcr_indices, bank) { - Ok(results) => { - for (index, value) in results { - all_results.push((index, bank, value)); - } - } - Err(_) => { - // PCR might not be allocated in this bank, continue - continue; - } - } - } - - Ok(all_results) - } - - /// Get which banks a specific PCR is allocated in by trying to read it - fn get_pcr_allocated_banks(&mut self, pcr_index: u8) -> Result> { - let all_banks = self.get_active_pcr_banks()?; - let mut allocated = Vec::new(); - - for bank in all_banks { - if let Ok(results) = self.pcr_read_bank(&[pcr_index], bank) { - if !results.is_empty() { - allocated.push(bank); - } - } - } - - Ok(allocated) - } - - /// Extend a PCR with data using a specific hash algorithm - /// - /// This is a low-level function that extends a single bank. - /// For most use cases, use pcr_extend() instead which extends all active banks. - fn pcr_extend_bank(&mut self, pcr_index: u8, alg: TpmAlg, digest: &[u8]) -> Result<()> { - // Validate digest size - if let Some(expected_size) = alg.digest_size() { - if digest.len() != expected_size { - bail!("Digest size mismatch: expected {} bytes for {}, got {}", - expected_size, alg.name(), digest.len()); - } - } - - // For PCR_Extend, all PCRs need authorization (even 16-23) - // This is different from PCR_Read which doesn't need auth - let cmd = CommandBuffer::new() - .write_u32(pcr_index as u32) // pcrHandle - .write_auth_empty_pw() - // TPML_DIGEST_VALUES with single entry - .write_u32(1) // count = 1 - .write_u16(alg as u16) // hashAlg - .write_bytes(digest); // raw digest bytes - - let command = cmd.finalize(TpmSt::Sessions, TpmCc::PcrExtend); - self.transmit(&command)?; - - Ok(()) - } - - /// Extend a PCR with data - /// - /// This function will: - /// 1. Query which banks this PCR is allocated in - /// 2. Hash the data with each allocated algorithm - /// 3. Extend the PCR with all digests - /// - /// The TPM computes: PCR_new = Hash(PCR_old || digest) - fn pcr_extend(&mut self, pcr_index: u8, data: &[u8]) -> Result<()> { - // Get banks that are allocated for this specific PCR - let banks = self.get_pcr_allocated_banks(pcr_index)?; - - if banks.is_empty() { - bail!("No PCR banks allocated for PCR {}", pcr_index); - } - - // Build the command with authorization - // PCR_Extend always needs authorization (even for PCRs 16-23) - let mut cmd = CommandBuffer::new() - .write_u32(pcr_index as u32) // pcrHandle - // Authorization area (password session with empty password) - .write_auth_empty_pw(); - - // TPML_DIGEST_VALUES - count - cmd = cmd.write_u32(banks.len() as u32); - - // Hash data with each active algorithm and add to digest list - for bank in &banks { - cmd = cmd.write_u16(*bank as u16); // hashAlg - - let digest = match bank { - TpmAlg::Sha1 => { - let mut hasher = Sha1::new(); - hasher.update(data); - hasher.finalize().to_vec() - }, - TpmAlg::Sha256 => { - let mut hasher = Sha256::new(); - hasher.update(data); - hasher.finalize().to_vec() - }, - TpmAlg::Sha384 => { - let mut hasher = Sha384::new(); - hasher.update(data); - hasher.finalize().to_vec() - }, - TpmAlg::Sha512 => { - let mut hasher = Sha512::new(); - hasher.update(data); - hasher.finalize().to_vec() - }, - _ => { - bail!("Unsupported hash algorithm for PCR extend: {:?}", bank); - } - }; - - // Write digest as raw bytes (TPMU_HA), not TPM2B - cmd = cmd.write_bytes(&digest); - } - - let command = cmd.finalize(TpmSt::Sessions, TpmCc::PcrExtend); - self.transmit(&command)?; - - Ok(()) - } - - /// Read all PCRs from all active banks and return only non-zero ones - /// Returns (pcr_index, algorithm, value) tuples - /// - /// Note: Reads PCRs individually to handle non-contiguous PCR allocation. - /// Some TPMs have gaps in PCR allocation (e.g., PCRs 0-7,9-10,17-23 allocated but not 8,11-16). - /// - /// Queries PCRs 0-23 (standard range) and attempts 24-31 (vendor-specific). - /// Most TPMs only support PCRs 0-23 even though TPM 2.0 spec allows up to 31. - fn read_nonzero_pcrs_all_banks(&mut self) -> Result)>> { - let mut all_nonzero = Vec::new(); - - // Read each PCR individually to handle non-contiguous allocation - // Query 0-23 (standard), attempt 24-31 (most TPMs don't support these) - for pcr_idx in 0..32 { - match self.pcr_read_all_banks(&[pcr_idx]) { - Ok(values) => { - for (index, alg, value) in values { - // Only include non-zero PCRs - if !value.iter().all(|&b| b == 0) { - all_nonzero.push((index, alg, value)); - } - } - } - Err(_) => { - // PCR not allocated in any bank, skip - continue; - } - } - } - - Ok(all_nonzero) - } - - /// Get list of allocated PCR indices across all banks - /// Returns Vec of (pcr_index, Vec) showing which PCRs exist and in which banks - fn get_allocated_pcrs(&mut self) -> Result)>> { - let mut pcr_info = Vec::new(); - - for pcr_idx in 0..32 { - let banks = self.get_pcr_allocated_banks(pcr_idx)?; - if !banks.is_empty() { - pcr_info.push((pcr_idx, banks)); - } - } - - Ok(pcr_info) - } - - /// Read ALL allocated PCRs from all banks (including zero values) - /// Returns (pcr_index, algorithm, value) tuples - /// - /// Use this (not read_nonzero_pcrs_all_banks) when creating PCR policies - /// to ensure zero PCRs are included in the policy. This prevents an attacker - /// from extending a currently-zero PCR to bypass the policy. - fn read_all_allocated_pcrs(&mut self) -> Result)>> { - // Get PCR allocation from TPM (single fast query) - let allocation = self.get_pcr_allocation()?; - let mut all_results = Vec::new(); - - // Read allocated PCRs from each bank - for (bank, pcr_indices) in allocation { - if pcr_indices.is_empty() { - continue; - } - - // TPM allocation bitmap is unreliable for bulk reads - // Some TPMs claim PCRs are allocated but won't return them in bulk - // Read each PCR individually to be safe - for pcr_idx in pcr_indices { - match self.pcr_read_bank(&[pcr_idx], bank) { - Ok(results) => { - for (index, value) in results { - all_results.push((index, bank, value)); - } - } - Err(_) => continue, - } - } - } - - Ok(all_results) - } - - /// Calculate PCR policy digest for the given PCRs and their values - /// - /// The policy digest is always SHA-256 (matches key's nameAlg), but the - /// pcr_alg parameter specifies which PCR bank the policy references. - fn calculate_pcr_policy_digest(pcr_values: &[(u8, Vec)], pcr_alg: TpmAlg) -> Result> { - // Step 1: Calculate PCR digest (hash of selected PCR values) - let mut pcr_hasher = Sha256::new(); - for (_index, value) in pcr_values { - pcr_hasher.update(value); - } - let pcr_digest = pcr_hasher.finalize(); - - // Step 2: Build PCR selection structure - let mut pcr_select = [0u8; 3]; // 3 bytes for PCRs 0-23 - for (index, _value) in pcr_values { - if *index < 24 { - pcr_select[*index as usize / 8] |= 1 << (*index % 8); - } - } - - // Step 3: Calculate policy digest - // policyDigest = SHA256(previousDigest || TPM_CC_PolicyPCR || pcrSelection || pcrDigest) - let mut policy_hasher = Sha256::new(); - - // previousDigest starts as all zeros (32 bytes for SHA256) - policy_hasher.update(&[0u8; 32]); - - // TPM_CC_PolicyPCR = 0x0000017F - policy_hasher.update(&(TpmCc::PolicyPCR as u32).to_be_bytes()); - - // TPML_PCR_SELECTION structure - // count (4 bytes) - policy_hasher.update(&1u32.to_be_bytes()); - // TPMS_PCR_SELECTION: hash (2 bytes) + sizeOfSelect (1 byte) + pcrSelect (3 bytes) - policy_hasher.update(&(pcr_alg as u16).to_be_bytes()); - policy_hasher.update(&[3u8]); // sizeOfSelect - policy_hasher.update(&pcr_select); - - // PCR digest - policy_hasher.update(&pcr_digest); - - let policy_digest = policy_hasher.finalize(); - - Ok(policy_digest.to_vec()) - } -} diff --git a/crates/stage1/Cargo.toml b/crates/stage1/Cargo.toml index 18c96ec..4bc8eb7 100644 --- a/crates/stage1/Cargo.toml +++ b/crates/stage1/Cargo.toml @@ -14,5 +14,5 @@ sha2 = { workspace = true } hex = { workspace = true } anyhow = { workspace = true } base64 = { workspace = true } -rawdogtpm2 = { path = "../rawdogtpm2" } +vaportpm-attest = { workspace = true } libc = { workspace = true } diff --git a/crates/stage1/src/main.rs b/crates/stage1/src/main.rs index 2b85635..0aa296d 100644 --- a/crates/stage1/src/main.rs +++ b/crates/stage1/src/main.rs @@ -2,13 +2,13 @@ use anyhow::{anyhow, Context, Result}; use base64::{engine::general_purpose::STANDARD, Engine as _}; -use rawdogtpm2::{Tpm, PcrOps}; +use vaportpm_attest::{Tpm, PcrOps}; use reqwest::blocking::Client; use rustls::crypto::CryptoProvider; use serde::{Deserialize, Serialize}; use serde::de::Error as _; use sha2::{Digest, Sha256}; -use rawdogtpm2 as tpm; +use vaportpm_attest as tpm; use std::fs; use std::io::{self, Write}; use std::os::unix::fs::PermissionsExt; diff --git a/downloads/Makefile b/downloads/Makefile index 620ea1a..36134e8 100644 --- a/downloads/Makefile +++ b/downloads/Makefile @@ -1,25 +1,17 @@ -.PHONY: all help clean +.PHONY: all help clean check-urls # Alpine Linux version (stable) ALPINE_VERSION := v3.22 ALPINE_BASE_URL := https://dl-cdn.alpinelinux.org/alpine/$(ALPINE_VERSION)/main # Busybox (static) from Alpine Linux -BUSYBOX_VERSION := 1.37.0-r19 -BUSYBOX_SHA256_x86_64 := e16ee4ee5c8a25b6090986ec6aa36e06cb58d4432d253ad9cef1e22110092cd4 -BUSYBOX_SHA256_aarch64 := 44895a11196e6a5260e27be827522bb5f3fa3f81aa35171a780be4be0780bdb1 +BUSYBOX_VERSION := 1.37.0-r20 +BUSYBOX_SHA256_x86_64 := 488ad6efd04b5a722719e79f8e0dcc2c24afd6758867af3ce41b04839e60c74b +BUSYBOX_SHA256_aarch64 := ee469aee2958feffd7f64dd96655704025ff60af614f77a1d7323dc237c34da2 BUSYBOX_APK := busybox-static-$(BUSYBOX_VERSION).apk BUSYBOX_URL_x86_64 := $(ALPINE_BASE_URL)/x86_64/$(BUSYBOX_APK) BUSYBOX_URL_aarch64 := $(ALPINE_BASE_URL)/aarch64/$(BUSYBOX_APK) -# Bubblewrap from Alpine Linux -BUBBLEWRAP_VERSION := 0.11.0-r1 -BUBBLEWRAP_SHA256_x86_64 := 2f3307838e3a242d4e5c173210ef2b70becd0cc57f0a56d5fe17ebf0fda2dc8d -BUBBLEWRAP_SHA256_aarch64 := 8041baaf15221574d2d14fa78989ce536dd928ca297ee7555cd621b48f837e58 -BUBBLEWRAP_APK := bubblewrap-static-$(BUBBLEWRAP_VERSION).apk -BUBBLEWRAP_URL_x86_64 := $(ALPINE_BASE_URL)/x86_64/$(BUBBLEWRAP_APK) -BUBBLEWRAP_URL_aarch64 := $(ALPINE_BASE_URL)/aarch64/$(BUBBLEWRAP_APK) - # systemd-boot-unsigned from Amazon Linux (provides EFI stub) # x86_64 provides: /usr/lib/systemd/boot/efi/linuxx64.efi.stub # aarch64 provides: /usr/lib/systemd/boot/efi/linuxaa64.efi.stub @@ -47,7 +39,7 @@ AEMM_URL := https://github.com/aws/amazon-ec2-metadata-mock/releases/download/$( AEMM_SHA256 := 4f89ddc71ac53ce540bda1f9c340526d558eed8e41349761f2798acf1b254950 ARCHS = x86_64 aarch64 -TARGETS = busybox bubblewrap stub.efi kernel.rpm +TARGETS = busybox stub.efi kernel.rpm all: $(ARCHS) ec2-metadata-mock-linux-amd64 @@ -62,14 +54,6 @@ aarch64: $(addprefix aarch64/,$(TARGETS)) @tar --warning=no-unknown-keyword -xzOf $< bin/busybox.static > "$@" @chmod +x "$@" -%/$(BUBBLEWRAP_APK): - @./download-and-verify.sh "$@" "$(BUBBLEWRAP_SHA256_$*)" "$(BUBBLEWRAP_URL_$*)" - -%/bubblewrap: %/$(BUBBLEWRAP_APK) - @mkdir -p $* - @tar --warning=no-unknown-keyword -xzOf $< usr/bin/bwrap.static > "$@" - @chmod +x "$@" - # Download systemd-boot-unsigned RPM from Amazon Linux x86_64/$(SYSTEMD_BOOT_RPM_x86_64): @./download-and-verify.sh "$@" "$(SYSTEMD_BOOT_SHA256_x86_64)" "$(SYSTEMD_BOOT_URL_x86_64)" @@ -112,3 +96,14 @@ kernel-hash-%: clean: rm -rf x86_64 aarch64 ec2-metadata-mock-linux-amd64 + +# Check all download URLs are still valid +check-urls: + @echo "Checking URLs..." + @curl -sfI "$(BUSYBOX_URL_x86_64)" > /dev/null && echo "OK: busybox x86_64" || echo "FAIL: busybox x86_64" + @curl -sfI "$(BUSYBOX_URL_aarch64)" > /dev/null && echo "OK: busybox aarch64" || echo "FAIL: busybox aarch64" + @curl -sfI "$(SYSTEMD_BOOT_URL_x86_64)" > /dev/null && echo "OK: systemd-boot x86_64" || echo "FAIL: systemd-boot x86_64" + @curl -sfI "$(SYSTEMD_BOOT_URL_aarch64)" > /dev/null && echo "OK: systemd-boot aarch64" || echo "FAIL: systemd-boot aarch64" + @curl -sfI "$(AMAZON_KERNEL_URL_x86_64)" > /dev/null && echo "OK: kernel x86_64" || echo "FAIL: kernel x86_64" + @curl -sfI "$(AMAZON_KERNEL_URL_aarch64)" > /dev/null && echo "OK: kernel aarch64" || echo "FAIL: kernel aarch64" + @curl -sfI "$(AEMM_URL)" > /dev/null && echo "OK: ec2-metadata-mock" || echo "FAIL: ec2-metadata-mock" diff --git a/scripts/.dockerignore b/scripts/.dockerignore index 38c267c..789a2c1 100644 --- a/scripts/.dockerignore +++ b/scripts/.dockerignore @@ -8,6 +8,5 @@ README.md stage1 busybox ec2-metadata-mock-* -bubblewrap secure-boot-keys old diff --git a/scripts/build.sh b/scripts/build.sh index 88f7e2e..13082da 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -19,7 +19,7 @@ mkdir -p "${OUTPUT_DIR}/tmp" # Determine the correct systemd-efistub filename, PE format, and objcopy command based on architecture if [ "${ARCH}" = "x86_64" ]; then PE_FORMAT="pei-x86-64" - OBJCOPY="objcopy" + OBJCOPY="x86_64-linux-gnu-objcopy" elif [ "${ARCH}" = "aarch64" ]; then PE_FORMAT="pei-aarch64-little" OBJCOPY="aarch64-linux-gnu-objcopy" @@ -80,10 +80,6 @@ mkdir -p "${INITRAMFS_DIR}"/{bin,sbin,etc,proc,sys,dev,lib,lib64,tmp} echo "Installing busybox..." cp "${ARCH}/busybox" "${INITRAMFS_DIR}/bin/" -# Copy bubblewrap -echo "Installing bubblewrap..." -cp "${ARCH}/bubblewrap" "${INITRAMFS_DIR}/bin/bwrap" - # Copy stage1 binary echo "Installing stage1..." cp "${ARCH}/stage1" "${INITRAMFS_DIR}/bin/stage1"