diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 006c7808..58e709e2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,8 +20,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@1.86 + uses: dtolnay/rust-toolchain@master with: + toolchain: 1.92.0 components: clippy, rustfmt - name: Run Clippy diff --git a/.github/workflows/sdk.yaml b/.github/workflows/sdk.yaml index d64ad9d9..e8abf7af 100644 --- a/.github/workflows/sdk.yaml +++ b/.github/workflows/sdk.yaml @@ -23,8 +23,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@1.86 + uses: dtolnay/rust-toolchain@master with: + toolchain: 1.92.0 components: clippy, rustfmt # This additional target is needed for wasm32 compatibility check. targets: wasm32-unknown-unknown, thumbv6m-none-eabi diff --git a/Cargo.lock b/Cargo.lock index 348a9493..e03f8387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -64,23 +55,11 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -93,9 +72,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f63701831729cb154cf0b6945256af46c426074646c98b9d123148ba1d8bde" +checksum = "f07655fedc35188f3c50ff8fc6ee45703ae14ef1bc7ae7d80e23a747012184e3" dependencies = [ "alloy-core", "alloy-signer", @@ -104,9 +83,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a3bd0305a44fb457cae77de1e82856eadd42ea3cdf0dae29df32eb3b592979" +checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" dependencies = [ "alloy-eips", "alloy-primitives", @@ -115,23 +94,25 @@ dependencies = [ "alloy-trie", "alloy-tx-macros", "auto_impl", + "borsh", "c-kzg", - "derive_more 2.0.1", + "derive_more 2.1.0", "either", "k256", "once_cell", "rand 0.8.5", "secp256k1", "serde", + "serde_json", "serde_with", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-consensus-any" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a842b4023f571835e62ac39fb8d523d19fcdbacfa70bf796ff96e7e19586f50" +checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" dependencies = [ "alloy-consensus", "alloy-eips", @@ -143,9 +124,9 @@ dependencies = [ [[package]] name = "alloy-core" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe6c56d58fbfa9f0f6299376e8ce33091fc6494239466814c3f54b55743cb09" +checksum = "5ca96214615ec8cf3fa2a54b32f486eb49100ca7fe7eb0b8c1137cd316e7250a" dependencies = [ "alloy-primitives", ] @@ -160,37 +141,39 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-eip2930" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "serde", ] [[package]] name = "alloy-eip7702" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "serde", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-eips" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd749c57f38f8cbf433e651179fc5a676255e6b95044f467d49255d2b81725a" +checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -199,20 +182,21 @@ dependencies = [ "alloy-rlp", "alloy-serde", "auto_impl", + "borsh", "c-kzg", - "derive_more 2.0.1", + "derive_more 2.1.0", "either", "serde", "serde_with", "sha2 0.10.9", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-json-abi" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125a1c373261b252e53e04d6e92c37d881833afc1315fceab53fd46045695640" +checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -222,24 +206,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f614019a029c8fec14ae661aa7d4302e6e66bdbfb869dab40e78dcfba935fc97" +checksum = "f72cf87cda808e593381fb9f005ffa4d2475552b7a6c5ac33d087bf77d82abd0" dependencies = [ "alloy-primitives", "alloy-sol-types", "http", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.17", "tracing", ] [[package]] name = "alloy-network" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8b6d58e98803017bbfea01dde96c4d270a29e7aed3beb65c8d28b5ab464e0e" +checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -254,18 +238,18 @@ dependencies = [ "alloy-sol-types", "async-trait", "auto_impl", - "derive_more 2.0.1", + "derive_more 2.1.0", "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-network-primitives" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db489617bffe14847bf89f175b1c183e5dd7563ef84713936e2c34255cfbd845" +checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" dependencies = [ "alloy-consensus", "alloy-eips", @@ -276,18 +260,18 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9485c56de23438127a731a6b4c87803d49faf1a7068dcd1d8768aca3a9edb9" +checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" dependencies = [ "alloy-rlp", "bytes", "cfg-if", "const-hex", - "derive_more 2.0.1", - "foldhash", - "hashbrown 0.15.5", - "indexmap 2.10.0", + "derive_more 2.1.0", + "foldhash 0.2.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "itoa", "k256", "keccak-asm", @@ -295,7 +279,7 @@ dependencies = [ "proptest", "rand 0.9.2", "ruint", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "sha3", "tiny-keccak", @@ -320,14 +304,14 @@ checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "alloy-rpc-types-any" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f27c0c41a16cd0af4f5dbf791f7be2a60502ca8b0e840e0ad29803fac2d587" +checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -336,9 +320,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f5812f81c3131abc2cd8953dc03c41999e180cff7252abbccaba68676e15027" +checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -352,14 +336,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-serde" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dfe41a47805a34b848c83448946ca96f3d36842e8c074bcf8fa0870e337d12" +checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" dependencies = [ "alloy-primitives", "serde", @@ -368,9 +352,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79237b4c1b0934d5869deea4a54e6f0a7425a8cd943a739d6293afdf893d847" +checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" dependencies = [ "alloy-primitives", "async-trait", @@ -378,14 +362,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-signer-local" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6e90a3858da59d1941f496c17db8d505f643260f7e97cdcdd33823ddca48fc1" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" dependencies = [ "alloy-consensus", "alloy-network", @@ -394,46 +378,46 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-sol-macro" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20d867dcf42019d4779519a1ceb55eba8d7f3d0e4f0a89bcba82b8f9eb01e48" +checksum = "f3ce480400051b5217f19d6e9a82d9010cdde20f1ae9c00d53591e4a1afbb312" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "alloy-sol-macro-expander" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74e91b0b553c115d14bd0ed41898309356dc85d0e3d4b9014c4e7715e48c8ad" +checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" dependencies = [ "alloy-sol-macro-input", "const-hex", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.12.1", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84194d31220803f5f62d0a00f583fd3a062b36382e2bea446f1af96727754565" +checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" dependencies = [ "const-hex", "dunce", @@ -441,15 +425,15 @@ dependencies = [ "macro-string", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe8c27b3cf6b2bb8361904732f955bc7c05e00be5f469cec7e2280b6167f3ff0" +checksum = "954d1b2533b9b2c7959652df3076954ecb1122a28cc740aa84e7b0a49f6ac0a9" dependencies = [ "serde", "winnow", @@ -457,9 +441,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5383d34ea00079e6dd89c652bcbdb764db160cef84e6250926961a0b2295d04" +checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -476,7 +460,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "arrayvec 0.7.6", - "derive_more 2.0.1", + "derive_more 2.1.0", "nybbles", "serde", "smallvec", @@ -485,23 +469,16 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e434e0917dce890f755ea774f59d6f12557bc8c7dd9fa06456af80cfe0f0181e" +checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" dependencies = [ - "alloy-primitives", - "darling 0.21.2", + "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -513,9 +490,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -528,9 +505,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -543,29 +520,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "ark-ff" @@ -605,6 +582,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec 0.7.6", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + [[package]] name = "ark-ff-asm" version = "0.3.0" @@ -625,6 +622,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.111", +] + [[package]] name = "ark-ff-macros" version = "0.3.0" @@ -650,6 +657,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -671,6 +691,18 @@ dependencies = [ "num-bigint", ] +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec 0.7.6", + "digest 0.10.7", + "num-bigint", +] + [[package]] name = "ark-std" version = "0.3.0" @@ -691,6 +723,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -742,7 +784,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -754,7 +796,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -782,7 +824,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -793,7 +835,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -819,7 +861,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -830,9 +872,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -841,11 +883,10 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" dependencies = [ - "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -854,9 +895,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "bytes", @@ -870,8 +911,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "sync_wrapper", "tower", "tower-layer", @@ -880,9 +920,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -891,27 +931,11 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -932,9 +956,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "basic-toml" @@ -960,49 +984,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.2", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.106", - "which 4.4.2", -] - -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags 2.9.2", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.106", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -1020,15 +1001,15 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", @@ -1042,9 +1023,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitvec" @@ -1075,7 +1056,20 @@ checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", "arrayvec 0.5.2", - "constant_time_eq", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq 0.3.1", ] [[package]] @@ -1098,9 +1092,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd49896f12ac9b6dcd7a5998466b9b58263a695a3dd1ecc1aaca2e12a90b080" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" dependencies = [ "cc", "glob", @@ -1133,7 +1127,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.15", + "thiserror 2.0.17", "tokio", "tokio-util", "tower-service", @@ -1154,9 +1148,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a0c21249ad725ebcadcb1b1885f8e3d56e8e6b8924f560268aab000982d637" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" dependencies = [ "bon-macros", "rustversion", @@ -1164,24 +1158,24 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a660ebdea4d4d3ec7788cfc9c035b66efb66028b9b97bf6cde7023ccc8e77e28" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" dependencies = [ - "darling 0.21.2", + "darling", "ident_case", "prettyplease", "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", "cfg_aliases", @@ -1189,25 +1183,25 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata", "serde", ] @@ -1225,9 +1219,9 @@ checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -1237,18 +1231,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] [[package]] name = "c-kzg" -version = "2.1.1" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7318cfa722931cb5fe0838b98d3ce5621e75f6a6408abc21721d80de9223f2e4" +checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" dependencies = [ "blst", "cc", @@ -1261,10 +1255,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1275,6 +1270,8 @@ name = "cc-eventlog" version = "0.5.5" dependencies = [ "anyhow", + "digest 0.10.7", + "ez-hash", "fs-err", "hex", "insta", @@ -1334,24 +1331,15 @@ dependencies = [ "rustls", "serde", "tokio", - "toml_edit", + "toml_edit 0.22.27", "tracing-subscriber", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1361,17 +1349,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1394,22 +1381,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" -version = "4.5.45" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -1417,9 +1393,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -1429,21 +1405,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmake" @@ -1477,7 +1453,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1500,15 +1476,14 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.14.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e22e0ed40b96a48d3db274f72fd365bd78f67af39b6bbd47e8a15e1c6207ff" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", "cpufeatures", - "hex", "proptest", - "serde", + "serde_core", ] [[package]] @@ -1519,9 +1494,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -1543,6 +1518,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.6.0" @@ -1561,6 +1542,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -1609,9 +1599,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -1706,9 +1696,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", "rand_core 0.6.4", @@ -1788,48 +1778,24 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "darling" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08440b3dd222c3d0433e63e097463969485f112baff337dfdaca043a0d760570" -dependencies = [ - "darling_core 0.21.2", - "darling_macro 0.21.2", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.106", -] - -[[package]] -name = "darling_core" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25b7912bc28a04ab1b7715a68ea03aaa15662b43a1a4b2c480531fd19f8bf7e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", @@ -1837,29 +1803,18 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "darling_macro" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce154b9bea7fb0c8e8326e62d00354000c36e79770ff21b8c84e3aa267d9d531" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.21.2", + "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1873,7 +1828,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -1884,13 +1839,14 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dcap-qvl" -version = "0.3.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ef42eade99e79b9fb9d31532041a0a380a7e849d3486950a40b1afd5bf417c" +checksum = "435989ce7ba46ba3f837f9df3c8139469e72ae810e707893b19f8b6b370d14ef" dependencies = [ "anyhow", "asn1_der", "base64 0.22.1", + "borsh", "byteorder", "chrono", "const-oid", @@ -1977,17 +1933,17 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -2012,11 +1968,11 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ - "derive_more-impl 2.0.1", + "derive_more-impl 2.1.0", ] [[package]] @@ -2028,19 +1984,21 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "unicode-xid", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ + "convert_case 0.10.0", "proc-macro2", "quote", - "syn 2.0.106", + "rustc_version 0.4.1", + "syn 2.0.111", "unicode-xid", ] @@ -2070,11 +2028,11 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2116,7 +2074,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2127,7 +2085,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2149,7 +2107,7 @@ checksum = "ed6b3e31251e87acd1b74911aed84071c8364fc9087972748ade2f1094ccce34" dependencies = [ "documented-macros", "phf", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -2164,7 +2122,32 @@ dependencies = [ "proc-macro2", "quote", "strum", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "dstack-attest" +version = "0.5.5" +dependencies = [ + "anyhow", + "cc-eventlog", + "dcap-qvl", + "dstack-types", + "ez-hash", + "fs-err", + "hex", + "hex_fmt", + "or-panic", + "parity-scale-codec", + "serde", + "serde-human-bytes", + "serde_json", + "sha2 0.10.9", + "sha3", + "tdx-attest", + "tpm-attest", + "tpm-qvl", + "tpm-types", ] [[package]] @@ -2192,7 +2175,7 @@ dependencies = [ "ipnet", "jemallocator", "load_config", - "nix", + "nix 0.29.0", "or-panic", "parcelona", "pin-project", @@ -2236,11 +2219,13 @@ dependencies = [ "anyhow", "base64 0.22.1", "bollard", + "cc-eventlog", "cert-client", "chrono", "clap", "cmd_lib", "default-net", + "dstack-attest", "dstack-guest-agent-rpc", "dstack-types", "ed25519-dalek", @@ -2272,6 +2257,7 @@ dependencies = [ "tdx-attest", "tempfile", "tokio", + "tpm-attest", "tracing", "tracing-subscriber", ] @@ -2300,6 +2286,7 @@ dependencies = [ "dstack-kms-rpc", "dstack-mr", "dstack-types", + "dstack-verifier", "fs-err", "git-version", "hex", @@ -2354,7 +2341,7 @@ dependencies = [ "flate2", "fs-err", "hex", - "hex-literal 1.0.0", + "hex-literal 1.1.0", "log", "object", "parity-scale-codec", @@ -2364,7 +2351,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tar", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -2422,6 +2409,7 @@ dependencies = [ name = "dstack-types" version = "0.5.5" dependencies = [ + "parity-scale-codec", "serde", "serde-human-bytes", "sha3", @@ -2435,16 +2423,19 @@ dependencies = [ "aes-gcm", "anyhow", "bollard", + "cc-eventlog", "cert-client", "clap", "cmd_lib", "curve25519-dalek", "dcap-qvl", + "dstack-attest", "dstack-gateway-rpc", "dstack-kms-rpc", "dstack-types", + "ez-hash", "fs-err", - "getrandom 0.3.3", + "getrandom 0.3.4", "hex", "hex_fmt", "host-api", @@ -2461,17 +2452,20 @@ dependencies = [ "serde", "serde-human-bytes", "serde_json", - "serde_yaml2", "sha2 0.10.9", "sha3", "sodiumbox", "tdx-attest", + "tempfile", "tokio", "toml", + "tpm-attest", + "tpm-qvl", "tracing", "tracing-subscriber", "x25519-dalek", "x509-parser", + "yaml-rust2", ] [[package]] @@ -2487,14 +2481,18 @@ dependencies = [ "figment", "fs-err", "hex", + "hex-literal 1.1.0", "ra-tls", "reqwest", "rocket", "serde", + "serde-human-bytes", "serde_json", "sha2 0.10.9", "tempfile", "tokio", + "tpm-qvl", + "tpm-types", "tracing", "tracing-subscriber", ] @@ -2511,7 +2509,9 @@ dependencies = [ "dstack-kms-rpc", "dstack-types", "dstack-vmm-rpc", + "fatfs", "fs-err", + "fscommon", "git-version", "guest-api", "hex", @@ -2611,6 +2611,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "either" version = "1.15.0" @@ -2665,7 +2677,27 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -2677,7 +2709,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2707,12 +2739,27 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "ez-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b3b3adc5fbbc9e21416d5b721b1bccb501a87d7b32ac89f2c7cea229d40772" +dependencies = [ + "blake2", + "blake3", + "digest 0.10.7", + "md-5", + "sha1", + "sha2 0.10.9", + "sha3", ] [[package]] @@ -2754,6 +2801,18 @@ dependencies = [ "bytes", ] +[[package]] +name = "fatfs" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "chrono", + "log", +] + [[package]] name = "ff" version = "0.13.1" @@ -2797,6 +2856,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -2829,9 +2894,9 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -2849,20 +2914,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fs-err" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683" +checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" dependencies = [ "autocfg", ] @@ -2873,6 +2944,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fscommon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315ce685aca5ddcc5a3e7e436ef47d4a5d0064462849b6f0f628c28140103531" +dependencies = [ + "log", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2944,7 +3024,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2996,20 +3076,6 @@ dependencies = [ "windows 0.48.0", ] -[[package]] -name = "generator" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.61.3", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3047,15 +3113,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -3079,12 +3145,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "git-version" version = "0.3.9" @@ -3102,7 +3162,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3147,7 +3207,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.10.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -3185,30 +3245,36 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] name = "hashlink" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.5", ] [[package]] @@ -3237,15 +3303,12 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec 0.7.6", ] @@ -3258,9 +3321,9 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hex-literal" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hex_fmt" @@ -3310,7 +3373,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.15", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -3329,7 +3392,7 @@ dependencies = [ "ipconfig", "lru-cache", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "resolv-conf", "smallvec", @@ -3350,11 +3413,11 @@ dependencies = [ "ipconfig", "moka", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.15", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -3389,11 +3452,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3411,12 +3474,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", ] @@ -3501,19 +3563,20 @@ dependencies = [ [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -3521,6 +3584,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3561,9 +3625,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", @@ -3577,7 +3641,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -3600,9 +3664,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3610,7 +3674,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -3624,9 +3688,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -3637,9 +3701,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -3650,11 +3714,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -3665,42 +3728,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -3716,9 +3775,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3752,7 +3811,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3768,13 +3827,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -3789,7 +3849,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "inotify-sys", "libc", ] @@ -3814,9 +3874,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.43.1" +version = "1.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" dependencies = [ "console", "once_cell", @@ -3863,17 +3923,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.2", - "cfg-if", - "libc", -] - [[package]] name = "iohash" version = "0.5.5" @@ -3910,9 +3959,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -3920,20 +3969,20 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -3944,15 +3993,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -3999,19 +4039,19 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -4086,28 +4126,15 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +dependencies = [ + "spin", +] [[package]] name = "libc" -version = "0.2.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "libloading" -version = "0.8.8" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.3", -] +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libm" @@ -4117,13 +4144,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", ] [[package]] @@ -4140,15 +4167,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "load_config" @@ -4162,19 +4189,18 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -4183,7 +4209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" dependencies = [ "cfg-if", - "generator 0.7.5", + "generator", "scoped-tls", "serde", "serde_json", @@ -4191,19 +4217,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator 0.8.5", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru-cache" version = "0.1.2" @@ -4257,16 +4270,16 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4275,6 +4288,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memalloc" version = "0.1.0" @@ -4283,9 +4306,9 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -4337,6 +4360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -4354,14 +4378,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4375,20 +4399,19 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom 0.7.2", - "parking_lot 0.12.4", + "equivalent", + "parking_lot 0.12.5", "portable-atomic", "rustc_version 0.4.1", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] @@ -4481,7 +4504,19 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -4511,13 +4546,13 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "fsevent-sys", "inotify", "kqueue", "libc", "log", - "mio 1.0.4", + "mio 1.1.1", "notify-types", "walkdir", "windows-sys 0.60.2", @@ -4549,12 +4584,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4567,6 +4601,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -4582,6 +4632,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -4612,33 +4673,11 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "nybbles" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0418987d1aaed324d95b4beffc93635e19be965ed5d63ec07a35980fe3b71a4" +checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" dependencies = [ "alloy-rlp", "cfg-if", @@ -4650,18 +4689,18 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", ] [[package]] name = "objc2-io-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ "libc", "objc2-core-foundation", @@ -4699,9 +4738,9 @@ dependencies = [ [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -4723,7 +4762,7 @@ checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4740,20 +4779,14 @@ checksum = "596a79faf55e869e7bc0c2162cf2f18a54d4d1112876bceae587ad954fcbd574" [[package]] name = "os_pipe" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -4785,7 +4818,7 @@ checksum = "89ec0a2252bc3809594c903cc8c1b83cbccaba85b11d4728a43a681263f6c132" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4813,7 +4846,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4829,12 +4862,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -4853,15 +4886,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -4917,17 +4950,17 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", ] [[package]] @@ -4941,18 +4974,17 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", - "thiserror 2.0.15", "ucd-trie", ] @@ -4963,7 +4995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap 2.10.0", + "indexmap 2.12.1", ] [[package]] @@ -4973,7 +5005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset 0.5.7", - "indexmap 2.10.0", + "indexmap 2.12.1", ] [[package]] @@ -5006,7 +5038,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5035,7 +5067,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5050,6 +5082,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -5091,9 +5134,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -5115,12 +5158,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5145,11 +5188,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.9", ] [[package]] @@ -5195,14 +5238,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -5215,26 +5258,25 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "version_check", "yansi", ] [[package]] name = "proptest" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.2", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -5296,7 +5338,7 @@ dependencies = [ "prost 0.13.5", "prost-types 0.13.5", "regex", - "syn 2.0.106", + "syn 2.0.111", "tempfile", ] @@ -5323,7 +5365,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5382,7 +5424,7 @@ dependencies = [ "prost-build 0.9.0", "prost-types 0.13.5", "quote", - "syn 2.0.106", + "syn 2.0.111", "template-quote", ] @@ -5393,7 +5435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac0855066edbf6bdcb42beb02cd9063d12d8d6d44b9a0c2f15a30e6ddd11f5" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5404,19 +5446,19 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", - "socket2 0.5.10", - "thiserror 2.0.15", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -5424,20 +5466,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.15", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -5445,23 +5487,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -5499,13 +5541,19 @@ dependencies = [ "bon", "cc-eventlog", "dcap-qvl", + "dstack-attest", + "dstack-types", "elliptic-curve", + "ez-hash", + "flate2", "fs-err", "hex", + "hex_fmt", "hkdf", "or-panic", "p256", "parity-scale-codec", + "rand 0.8.5", "rcgen", "ring", "rustls-pki-types", @@ -5515,6 +5563,8 @@ dependencies = [ "sha2 0.10.9", "sha3", "tdx-attest", + "tpm-qvl", + "tpm-types", "tracing", "x509-parser", "yasna", @@ -5616,7 +5666,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "serde", ] @@ -5663,11 +5713,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", ] [[package]] @@ -5678,84 +5728,69 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "ref-swap" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c30c54dffee5b40af088d5d50aa3455c91a0127164b51f0215efc4cb28fb3c" - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] +checksum = "09c30c54dffee5b40af088d5d50aa3455c91a0127164b51f0215efc4cb28fb3c" [[package]] -name = "regex-automata" -version = "0.1.10" +name = "regex" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ - "regex-syntax 0.6.29", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64 0.22.1", "bytes", @@ -5797,9 +5832,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "result" @@ -5856,9 +5891,9 @@ dependencies = [ "proc-macro2", "quote", "rinja_parser", - "rustc-hash 2.1.1", + "rustc-hash", "serde", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5885,7 +5920,7 @@ dependencies = [ [[package]] name = "rocket" version = "0.6.0-dev" -source = "git+https://github.com/rwf2/Rocket?branch=master#f9de1bf4671100b2f9c9bea6ce206fc4748ca999" +source = "git+https://github.com/rwf2/Rocket?branch=master#504efef179622df82ba1dbd37f2e0d9ed2b7c9e4" dependencies = [ "async-stream", "async-trait", @@ -5898,12 +5933,12 @@ dependencies = [ "http", "hyper", "hyper-util", - "indexmap 2.10.0", + "indexmap 2.12.1", "libc", "memchr", "multer", "num_cpus", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "rand 0.8.5", "ref-cast", @@ -5945,11 +5980,11 @@ name = "rocket-vsock-listener" version = "0.5.5" dependencies = [ "anyhow", - "derive_more 2.0.1", + "derive_more 2.1.0", "pin-project", "rocket", "serde", - "thiserror 2.0.15", + "thiserror 2.0.17", "tokio", "tokio-vsock", ] @@ -5957,15 +5992,15 @@ dependencies = [ [[package]] name = "rocket_codegen" version = "0.6.0-dev" -source = "git+https://github.com/rwf2/Rocket?branch=master#f9de1bf4671100b2f9c9bea6ce206fc4748ca999" +source = "git+https://github.com/rwf2/Rocket?branch=master#504efef179622df82ba1dbd37f2e0d9ed2b7c9e4" dependencies = [ "devise", "glob", - "indexmap 2.10.0", + "indexmap 2.12.1", "proc-macro2", "quote", "rocket_http", - "syn 2.0.106", + "syn 2.0.111", "unicode-xid", "version_check", ] @@ -5973,11 +6008,11 @@ dependencies = [ [[package]] name = "rocket_http" version = "0.6.0-dev" -source = "git+https://github.com/rwf2/Rocket?branch=master#f9de1bf4671100b2f9c9bea6ce206fc4748ca999" +source = "git+https://github.com/rwf2/Rocket?branch=master#504efef179622df82ba1dbd37f2e0d9ed2b7c9e4" dependencies = [ "cookie", "either", - "indexmap 2.10.0", + "indexmap 2.12.1", "memchr", "pear", "percent-encoding", @@ -5990,15 +6025,36 @@ dependencies = [ "uncased", ] +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "ruint" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", "ark-ff 0.4.2", + "ark-ff 0.5.0", "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", @@ -6012,7 +6068,7 @@ dependencies = [ "rand 0.9.2", "rlp", "ruint-macro", - "serde", + "serde_core", "valuable", "zeroize", ] @@ -6031,22 +6087,10 @@ checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" dependencies = [ "base64 0.13.1", "blake2b_simd", - "constant_time_eq", + "constant_time_eq 0.1.5", "crossbeam-utils", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -6074,7 +6118,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.26", + "semver 1.0.27", ] [[package]] @@ -6092,7 +6136,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6101,38 +6145,38 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -6151,9 +6195,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -6172,9 +6216,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", "ring", @@ -6190,9 +6234,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -6217,9 +6261,9 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "s2n-codec" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a2a209163c3c3e68c100651706645843188015db6859236a19d63c7aa00f18" +checksum = "2ce62ab2534380ee20a32ce169c733df8462ef9821489ae8df8603c1d9e89574" dependencies = [ "byteorder", "bytes", @@ -6228,9 +6272,9 @@ dependencies = [ [[package]] name = "s2n-quic" -version = "1.63.0" +version = "1.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e462d4e16d99370a0ae1544705a1dfb1f572f3b13068af7ffdfe1f196482b06a" +checksum = "62cfd37e7de6e7d73a8c55996a1389d27d6bbe377e565440ef4980252062bf09" dependencies = [ "bytes", "cfg-if", @@ -6252,9 +6296,9 @@ dependencies = [ [[package]] name = "s2n-quic-core" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dea73b3696c383fd86dc423ba744a4ff4f6608c844832f4933366b9d9d68d57" +checksum = "d36dc08b73cea81834036fa54ba9cf4ad91ede5a31543d0ad64a63f7e99d15db" dependencies = [ "atomic-waker", "byteorder", @@ -6274,9 +6318,9 @@ dependencies = [ [[package]] name = "s2n-quic-crypto" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07254da875a37720bc944eaad8026c225d6599715ce30b0af198c039cd8046dd" +checksum = "da8e47e0db3b85012cdcb7a95490639893fa60ea0d680730424839d2c268e23c" dependencies = [ "aws-lc-rs", "cfg-if", @@ -6300,28 +6344,28 @@ dependencies = [ [[package]] name = "s2n-quic-platform" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487103480577416f5cb614d270cd65149f1cfa2adf8841b6ae64260da048d4ec" +checksum = "218ddcd7677c8dee8fc48e6f4daaeecb63c7c5b28b21d2429a53fcbf326b55ce" dependencies = [ "cfg-if", "futures", "lazy_static", "libc", "s2n-quic-core", - "socket2 0.6.0", + "socket2 0.6.1", "tokio", ] [[package]] name = "s2n-quic-rustls" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656d175084631103e3db1b888d0e1c23b7fd2f855d87c41a3ad634c0823ec5ba" +checksum = "cc59fae30cffc275a6e798c00996f3e56dc74371bd685e112e66a231ced86c4a" dependencies = [ "bytes", "rustls", - "rustls-pemfile", + "rustls-pki-types", "s2n-codec", "s2n-quic-core", "s2n-quic-crypto", @@ -6329,14 +6373,14 @@ dependencies = [ [[package]] name = "s2n-quic-transport" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33468e6ab9dd8aa389f4df02464f737fcd05c33b8fc50d0274b57613e9596a06" +checksum = "dfbd2465236f7ce448f082a33bdc2e85ef62a7ce68c7eef79e4d90dc1b3eec0c" dependencies = [ "bytes", "futures-channel", "futures-core", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "intrusive-collections", "once_cell", "s2n-codec", @@ -6394,16 +6438,16 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6420,9 +6464,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -6517,11 +6561,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -6530,9 +6574,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -6549,9 +6593,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "semver-parser" @@ -6564,9 +6608,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -6590,41 +6634,43 @@ dependencies = [ [[package]] name = "serde-human-bytes" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ef65cb41f3f9cef63c431193229067e8b98b53c4d4c4ed38a8ca87c4d07676" +checksum = "6a091af6294712930d01e375cce513e4ac416f823e033e8991ec4e5d6e6ef4c0" dependencies = [ + "base64 0.13.1", "hex", "serde", ] [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6640,15 +6686,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -6670,7 +6717,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6696,19 +6743,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.1.0", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -6716,35 +6762,35 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] -name = "serde_yaml2" -version = "0.1.3" +name = "serdect" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a7808e70b0b09116f01a4730d8976bcab457ea4a5e2e638e9b12ed3e01f27c" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ + "base16ct", "serde", - "thiserror 1.0.69", - "yaml-rust2", ] [[package]] -name = "serdect" -version = "0.2.0" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "base16ct", - "serde", + "cfg-if", + "cpufeatures", + "digest 0.10.7", ] [[package]] @@ -6851,9 +6897,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -6868,6 +6914,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "similar" version = "2.7.0" @@ -6887,7 +6939,7 @@ dependencies = [ "anyhow", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -6917,12 +6969,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6964,9 +7016,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state" @@ -6974,7 +7026,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" dependencies = [ - "loom 0.5.6", + "loom", ] [[package]] @@ -7016,7 +7068,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7037,7 +7089,7 @@ dependencies = [ "git-version", "libc", "load_config", - "nix", + "nix 0.29.0", "notify", "or-panic", "rocket", @@ -7083,9 +7135,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -7094,14 +7146,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b198d366dbec045acfcd97295eb653a7a2b40e4dc764ef1e79aafcad439d3c" +checksum = "ff790eb176cc81bb8936aed0f7b9f14fc4670069a2d371b3e3b0ecce908b2cb3" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7121,7 +7173,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7201,35 +7253,26 @@ dependencies = [ "fs-err", "hex", "insta", - "num_enum", + "log", "parity-scale-codec", "serde", "serde-human-bytes", "serde_json", "sha2 0.10.9", - "tdx-attest-sys", - "thiserror 2.0.15", -] - -[[package]] -name = "tdx-attest-sys" -version = "0.5.5" -dependencies = [ - "bindgen 0.71.1", - "cc", + "thiserror 2.0.17", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -7274,11 +7317,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.15" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.15", + "thiserror-impl 2.0.17", ] [[package]] @@ -7289,18 +7332,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "thiserror-impl" -version = "2.0.15" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7323,9 +7366,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -7338,15 +7381,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -7363,9 +7406,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -7373,9 +7416,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -7388,40 +7431,37 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", - "mio 1.0.4", - "parking_lot 0.12.4", + "mio 1.1.1", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -7440,9 +7480,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -7472,8 +7512,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -7485,20 +7525,50 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime 0.7.3", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -7522,11 +7592,11 @@ 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 2.9.2", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -7550,11 +7620,76 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tpm-attest" +version = "0.5.5" +dependencies = [ + "anyhow", + "dstack-types", + "fs-err", + "hex", + "parity-scale-codec", + "serde", + "serde-human-bytes", + "serde_json", + "sha2 0.10.9", + "tempfile", + "tpm-types", + "tpm2", + "tracing", +] + +[[package]] +name = "tpm-qvl" +version = "0.5.5" +dependencies = [ + "anyhow", + "base64 0.22.1", + "dcap-qvl-webpki", + "dstack-types", + "hex", + "nom", + "p256", + "pem", + "reqwest", + "rsa", + "rustls-pki-types", + "rustls-webpki 0.103.8", + "serde", + "serde_json", + "sha2 0.10.9", + "tpm-types", + "tracing", + "x509-parser", +] + +[[package]] +name = "tpm-types" +version = "0.5.5" +dependencies = [ + "cc-eventlog", + "dstack-types", + "parity-scale-codec", + "serde", + "serde-human-bytes", +] + +[[package]] +name = "tpm2" +version = "0.5.5" +dependencies = [ + "anyhow", + "hex", + "sha2 0.10.9", + "tempfile", + "tracing", +] + [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -7563,20 +7698,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -7595,15 +7730,15 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "parking_lot 0.12.4", - "regex", + "parking_lot 0.12.5", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -7630,9 +7765,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ubyte" @@ -7685,9 +7820,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -7725,13 +7860,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -7754,11 +7890,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -7783,12 +7919,12 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vsock" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688" +checksum = "e2da6e4ac76cd19635dce0f98985378bb62f8044ee2ff80abd2a7334b920ed63" dependencies = [ "libc", - "nix", + "nix 0.30.1", ] [[package]] @@ -7841,45 +7977,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -7890,9 +8013,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7900,31 +8023,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -7942,9 +8065,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -7969,15 +8092,15 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" dependencies = [ "either", "env_home", - "rustix 1.0.8", + "rustix 1.1.2", "winsafe", ] [[package]] name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -7997,11 +8120,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8026,9 +8149,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -8038,7 +8161,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -8049,9 +8172,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -8060,31 +8196,31 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", "windows-threading", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8093,14 +8229,20 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", ] [[package]] @@ -8109,7 +8251,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -8118,7 +8269,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -8154,7 +8314,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -8190,19 +8359,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -8211,7 +8380,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -8228,9 +8397,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -8246,9 +8415,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -8264,9 +8433,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -8276,9 +8445,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -8294,9 +8463,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -8312,9 +8481,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -8330,9 +8499,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -8348,15 +8517,15 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -8378,19 +8547,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.2", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -8449,7 +8615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.0.8", + "rustix 1.1.2", ] [[package]] @@ -8477,9 +8643,9 @@ dependencies = [ [[package]] name = "yaml-rust2" -version = "0.8.1" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", @@ -8506,11 +8672,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -8518,34 +8683,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8565,15 +8730,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -8586,14 +8751,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -8602,9 +8767,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -8613,11 +8778,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] diff --git a/Cargo.toml b/Cargo.toml index 98bc4551..6072b6c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,12 @@ members = [ "kms/rpc", "ra-rpc", "ra-tls", - "tdx-attest-sys", "tdx-attest", + "tpm-attest", + "tpm2", + "tpm-types", + "tpm-qvl", + "dstack-attest", "dstack-util", "iohash", "guest-agent", @@ -68,7 +72,11 @@ cc-eventlog = { path = "cc-eventlog" } supervisor = { path = "supervisor" } supervisor-client = { path = "supervisor/client" } tdx-attest = { path = "tdx-attest" } -tdx-attest-sys = { path = "tdx-attest-sys" } +tpm-attest = { path = "tpm-attest" } +tpm2 = { path = "tpm2" } +tpm-types = { path = "tpm-types" } +dstack-attest = { path = "dstack-attest" } +tpm-qvl = { path = "tpm-qvl" } certbot = { path = "certbot" } rocket-vsock-listener = { path = "rocket-vsock-listener" } host-api = { path = "host-api", default-features = false } @@ -82,6 +90,7 @@ lspci = { path = "lspci" } sodiumbox = { path = "sodiumbox" } serde-duration = { path = "serde-duration" } dstack-mr = { path = "dstack-mr" } +dstack-verifier = { path = "verifier", default-features = false } size-parser = { path = "size-parser" } # Core dependencies @@ -118,7 +127,7 @@ scale = { version = "3.7.4", package = "parity-scale-codec", features = [ "derive", ] } serde = { version = "1.0.219", features = ["derive"], default-features = false } -serde-human-bytes = "0.1.0" +serde-human-bytes = "0.1.2" serde_json = { version = "1.0.140", default-features = false } serde_ini = "0.2.0" toml = "0.8.20" @@ -127,6 +136,8 @@ yasna = "0.5.2" bytes = "1.10.1" figment = "0.10.19" object = "0.36.4" +fatfs = "0.3.6" +fscommon = "0.1.1" # Networking/HTTP bollard = "0.18.1" @@ -158,7 +169,7 @@ default-net = "0.22.0" # Cryptography/Security aes-gcm = "0.10.3" curve25519-dalek = "4.1.3" -dcap-qvl = "0.3.0" +dcap-qvl = "0.3.4" elliptic-curve = { version = "0.13.8", features = ["pkcs8"] } getrandom = "0.3.1" hkdf = "0.12.4" @@ -179,6 +190,7 @@ xsalsa20poly1305 = "0.9.0" salsa20 = "0.10" rand_core = "0.6.4" alloy = { version = "1.0.32", default-features = false } +ez-hash = "1.1.0" # Certificate/DNS hickory-resolver = "0.24.4" @@ -217,10 +229,11 @@ uuid = { version = "1.15.1", features = ["v4"] } which = "7.0.2" smallvec = "1.14.0" cmd_lib = "1.9.5" -serde_yaml2 = "0.1.2" +yaml-rust2 = "0.10.4" luks2 = "0.5.0" scopeguard = "1.2.0" +flate2 = "1.1" [profile.release] panic = "abort" diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt deleted file mode 100644 index ea890afb..00000000 --- a/LICENSES/BSD-3-Clause.txt +++ /dev/null @@ -1,11 +0,0 @@ -Copyright (c) . - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt deleted file mode 100644 index 17cb2864..00000000 --- a/LICENSES/GPL-2.0-only.txt +++ /dev/null @@ -1,117 +0,0 @@ -GNU GENERAL PUBLIC LICENSE -Version 2, June 1991 - -Copyright (C) 1989, 1991 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. - -To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. - -For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. - -We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. - -Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. - -Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. - -The precise terms and conditions for copying, distribution and modification follow. - -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. - -1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. - - c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. - -4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. - -6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. - -7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. - -10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author - - This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. - -signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/Linux-syscall-note.txt b/LICENSES/Linux-syscall-note.txt deleted file mode 100644 index fcd05636..00000000 --- a/LICENSES/Linux-syscall-note.txt +++ /dev/null @@ -1,12 +0,0 @@ - NOTE! This copyright does *not* cover user programs that use kernel - services by normal system calls - this is merely considered normal use - of the kernel, and does *not* fall under the heading of "derived work". - Also note that the GPL below is copyrighted by the Free Software - Foundation, but the instance of code that it refers to (the Linux - kernel) is copyrighted by me and others who actually wrote it. - - Also note that the only valid version of the GPL as far as the kernel - is concerned is _this_ particular version of the license (ie v2, not - v2.2 or v3.x or whatever), unless explicitly otherwise stated. - - Linus Torvalds diff --git a/REUSE.toml b/REUSE.toml index 8f345c27..228ffbd6 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -66,8 +66,12 @@ path = [ "docs/security/dstack-audit.pdf", "dstack_Technical_Charter_Final_10-17-2025.pdf", "sdk/simulator/quote.hex", + "sdk/simulator/attestation.bin", "ra-tls/assets/tdx_quote", "cc-eventlog/samples/ccel.bin", + "cc-eventlog/samples/tpm_eventlog.bin", + "tpm-attest/tests/tpm_quote_sample.bin", + "tpm-qvl/certs/gcp-root-ca.pem", ] SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "CC0-1.0" @@ -102,16 +106,6 @@ path = "kms/auth-eth/lib/forge-std/**" SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "Apache-2.0" -[[annotations]] -path = "tdx-attest-sys/csrc/*" -SPDX-FileCopyrightText = "Copyright (C) 2011-2021 Intel Corporation. All rights reserved." -SPDX-License-Identifier = "BSD-3-Clause" - -[[annotations]] -path = "mod-tdx-guest/Kconfig" -SPDX-FileCopyrightText = "© 2022 Intel Corporation" -SPDX-License-Identifier = "GPL-2.0-only" - # Generated files [[annotations]] @@ -173,3 +167,8 @@ SPDX-License-Identifier = "CC0-1.0" path = "verifier/builder/shared/*.txt" SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "guest-agent/fixtures/*" +SPDX-FileCopyrightText = "NONE" +SPDX-License-Identifier = "CC0-1.0" diff --git a/basefiles/app-compose.sh b/basefiles/app-compose.sh index c3768ea4..0387ed34 100644 --- a/basefiles/app-compose.sh +++ b/basefiles/app-compose.sh @@ -23,9 +23,6 @@ case "$RUNNER" in if ! [ -f docker-compose.yaml ]; then jq -r '.docker_compose_file' app-compose.json >docker-compose.yaml fi - dstack-util remove-orphans -f docker-compose.yaml || true - chmod +x /usr/bin/containerd-shim-runc-v2 - systemctl restart docker if ! docker compose up --remove-orphans -d --build; then dstack-util notify-host -e "boot.error" -d "failed to start containers" @@ -37,7 +34,6 @@ case "$RUNNER" in docker volume prune -f ;; "bash") - chmod +x /usr/bin/containerd-shim-runc-v2 echo "Running main script" dstack-util notify-host -e "boot.progress" -d "running main script" || true jq -r '.bash_script' app-compose.json | bash diff --git a/basefiles/containerd.service.d/dstack-prepare.conf b/basefiles/containerd.service.d/dstack-prepare.conf new file mode 100644 index 00000000..6bbda824 --- /dev/null +++ b/basefiles/containerd.service.d/dstack-prepare.conf @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[Unit] +Wants=dstack-prepare.service +After=dstack-prepare.service diff --git a/basefiles/docker.service.d/dstack-prepare.conf b/basefiles/docker.service.d/dstack-prepare.conf new file mode 100644 index 00000000..6bbda824 --- /dev/null +++ b/basefiles/docker.service.d/dstack-prepare.conf @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[Unit] +Wants=dstack-prepare.service +After=dstack-prepare.service diff --git a/basefiles/dstack-prepare.service b/basefiles/dstack-prepare.service index 12a47ea7..833ad409 100644 --- a/basefiles/dstack-prepare.service +++ b/basefiles/dstack-prepare.service @@ -1,6 +1,7 @@ [Unit] Description=dstack Guest Preparation Service -After=network.target chronyd.service +After=network.target network-online.target chronyd.service +Wants=network-online.target Before=app-compose.service dstack-guest-agent.service docker.service OnFailure=reboot.target diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index dfa92b9b..fc863a86 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -6,6 +6,76 @@ set -e +log() { + printf '%s\n' "$*" >&2 +} + +CMDLINE=$(cat /proc/cmdline) + +get_cmdline_value() { + local key="$1" + for param in $CMDLINE; do + case "$param" in + "$key="*) + echo "${param#*=}" + return 0 + ;; + esac + done + return 1 +} + +read_uevent_property() { + local file="$1" + local key="$2" + while IFS='=' read -r name value; do + if [ "$name" = "$key" ]; then + printf "%s" "$value" + return 0 + fi + done <"$file" + return 1 +} + +find_block_by_property() { + local key="$1" + local value="$2" + for entry in /sys/class/block/*; do + [ -e "$entry/uevent" ] || continue + local current + current=$(read_uevent_property "$entry/uevent" "$key" || true) + if [ "$current" = "$value" ]; then + printf "/dev/%s" "$(basename "$entry")" + return 0 + fi + done + return 1 +} + +resolve_block_spec() { + local spec="$1" + local device="" + case "$spec" in + "") + return 1 + ;; + PARTLABEL=*) + device=$(find_block_by_property PARTNAME "${spec#PARTLABEL=}" || true) + ;; + PARTUUID=*) + device=$(find_block_by_property PARTUUID "${spec#PARTUUID=}" || true) + ;; + /dev/*) + device="$spec" + ;; + esac + if [ -n "$device" ] && [ -b "$device" ]; then + printf "%s" "$device" + return 0 + fi + return 1 +} + WORK_DIR="/var/volatile/dstack" DATA_MNT="$WORK_DIR/persistent" @@ -19,37 +89,202 @@ mount_overlay() { mkdir -p $dst/upper $dst/work mount -t overlay overlay -o lowerdir=$src,upperdir=$dst/upper,workdir=$dst/work $src } -mount_overlay /etc/wireguard $OVERLAY_TMP -mount_overlay /etc/docker $OVERLAY_TMP -mount_overlay /usr/bin $OVERLAY_TMP -mount_overlay /home/root $OVERLAY_TMP - -# Disable the containerd-shim-runc-v2 temporarily to prevent the containers from starting -# before docker compose removal orphans. It will be enabled in app-compose.sh -chmod -x /usr/bin/containerd-shim-runc-v2 +mount_overlay /etc $OVERLAY_TMP +mount_overlay /usr $OVERLAY_TMP +mount_overlay /bin $OVERLAY_TMP +mount_overlay /home $OVERLAY_TMP # Make sure the system time is synchronized -echo "Syncing system time..." +log "Syncing system time..." # Let the chronyd correct the system time immediately chronyc makestep -modprobe tdx-guest +if ! [[ -e /dev/tdx_guest ]]; then + modprobe tdx-guest +fi + +# Mount configfs for TSM (required for TDX quote generation) +if [[ ! -d /sys/kernel/config ]]; then + mkdir -p /sys/kernel/config +fi +if ! mountpoint -q /sys/kernel/config; then + log "Mounting configfs for TSM..." + mount -t configfs none /sys/kernel/config +fi + +# Create TSM report directory for TDX attestation +if [[ -e /dev/tdx_guest ]] && [[ ! -d /sys/kernel/config/tsm/report/com.intel.dcap ]]; then + log "Creating TSM report directory..." + mkdir -p /sys/kernel/config/tsm/report/com.intel.dcap +fi # Setup dstack system -echo "Preparing dstack system..." -dstack-util setup --work-dir $WORK_DIR --device /dev/vdb --mount-point $DATA_MNT +log "Preparing dstack system..." + +has_partition_table() { + local disk="$1" + local disk_name=$(basename "$disk") + # Check sysfs for any child partitions + for entry in /sys/class/block/${disk_name}/${disk_name}*; do + [ -e "$entry/partition" ] || continue + return 0 + done + return 1 +} + +has_luks_header() { + cryptsetup isLuks "$1" 2>/dev/null && return 0 + return 1 +} + +create_data_partition() { + local disk="$1" + log "Creating GPT partition table on ${disk}..." + if ! command -v sgdisk >/dev/null 2>&1; then + log "Error: sgdisk not available, cannot create partition table" + return 1 + fi + # Create GPT with single partition filling entire disk + sgdisk -Z "$disk" >/dev/null || true # Zap any existing data + sgdisk -n 1:1MiB:0 -c 1:dstack-data -t 1:8300 "$disk" >/dev/null || return 1 + # Trigger kernel to re-read partition table + blockdev --rereadpt "$disk" >/dev/null || true + udevadm settle >/dev/null || sleep 1 + part_device=$( + lsblk -nr -o PATH "$disk" 2>/dev/null | sed -n '2p' + ) + if [ -n "$part_device" ] && [ -b "$part_device" ]; then + log "Created partition: $part_device" + echo "$part_device" + return 0 + fi + log "Failed to create partition" + return 1 +} + +choose_data_device() { + local override="$1" + local dev="" + + # 1. Check explicit override first + if [ -n "$override" ]; then + dev=$(resolve_block_spec "$override" || true) + if [ -n "$dev" ]; then + echo "$dev" + return 0 + fi + log "Warning: dstack data device override '$override' not found" + fi + + # 2. Try to find partition with PARTLABEL=dstack-data + local data_disk + data_disk=$(resolve_block_spec "PARTLABEL=dstack-data" || true) + if [ -n "$data_disk" ]; then + echo "$data_disk" + return 0 + fi + + # 3. Fallback to /dev/vdb for backward compatibility + if [ ! -b /dev/vdb ]; then + log "Error: No dstack-data partition found and /dev/vdb does not exist" + return 1 + fi + + # 3.1. Check if /dev/vdb has LUKS header (0.5.x upgrade path) + if has_luks_header /dev/vdb; then + log "Detected LUKS on /dev/vdb (dstack 0.5.x upgrade), using whole disk" + echo /dev/vdb + return 0 + fi + + # 3.2. Check if /dev/vdb has partition table + if has_partition_table /dev/vdb; then + log "Error: /dev/vdb has partition table but no 'dstack-data' partition found" + log "Please check partition labels or specify dstack.data_device kernel parameter" + return 1 + fi + + # 3.3. /dev/vdb is empty, create partition table + log "Empty disk detected at /dev/vdb, creating dstack-data partition..." + local new_partition + new_partition=$(create_data_partition /dev/vdb) + if [ -z "$new_partition" ]; then + log "Error: Failed to create partition on /dev/vdb" + return 1 + fi + echo "$new_partition" + return 0 +} -echo "Mounting docker dirs to persistent storage" +DATA_DEVICE_OVERRIDE=$(get_cmdline_value "dstack.data_device" || true) +if [ -z "$DATA_DEVICE_OVERRIDE" ] && [ -n "$DSTACK_DATA_DEVICE" ]; then + DATA_DEVICE_OVERRIDE="$DSTACK_DATA_DEVICE" +fi +DATA_DEVICE=$(choose_data_device "$DATA_DEVICE_OVERRIDE" || true) +if [ ! -b "$DATA_DEVICE" ]; then + log "Persistent data disk $DATA_DEVICE not found" + exit 1 +fi +log "Using persistent data disk $DATA_DEVICE" + +# Auto-grow partition if disk was expanded +device_name=$(basename "$DATA_DEVICE") +if [ -f "/sys/class/block/${device_name}/partition" ]; then + log "Detected partition ${DATA_DEVICE}, checking if parent disk was expanded..." + + parent_disk=$(lsblk -no PKNAME "$DATA_DEVICE" 2>/dev/null | head -n1 || true) + if [ -n "$parent_disk" ]; then + parent_disk="/dev/${parent_disk}" + fi + + if [ -n "$parent_disk" ] && [ -b "$parent_disk" ]; then + log "Parent disk: ${parent_disk}" + if command -v sgdisk >/dev/null 2>&1; then + log "Refreshing GPT on ${parent_disk}..." + sgdisk -e "$parent_disk" 2>/dev/null || true + fi + if command -v parted >/dev/null 2>&1; then + part_num=$(cat "/sys/class/block/${device_name}/partition" 2>/dev/null || echo "") + if [ -n "$part_num" ]; then + log "Growing partition ${part_num} on ${parent_disk} via parted..." + parted --script "$parent_disk" resizepart "$part_num" 100% 2>/dev/null || log "Partition already at maximum size" + fi + else + log "Warning: parted not available; unable to auto-resize ${DATA_DEVICE}" + fi + # Trigger kernel to re-read partition table + blockdev --rereadpt "$parent_disk" 2>/dev/null || true + fi +fi + +dstack-util setup --work-dir $WORK_DIR --device "$DATA_DEVICE" --mount-point $DATA_MNT + +log "Mounting docker dirs to persistent storage" # Mount docker dirs to DATA_MNT mkdir -p $DATA_MNT/var/lib/docker +mkdir -p $DATA_MNT/var/lib/containerd mount --rbind $DATA_MNT/var/lib/docker /var/lib/docker +mount --rbind $DATA_MNT/var/lib/containerd /var/lib/containerd mount --rbind $WORK_DIR /dstack -mount_overlay /etc/users $OVERLAY_PERSIST + +echo "======== Disk usage ========" +df -h +echo "============================" cd /dstack if [ $(jq 'has("init_script")' app-compose.json) == true ]; then - echo "Running init script" - dstack-util notify-host -e "boot.progress" -d "init-script" || true - source <(jq -r '.init_script' app-compose.json) + log "Running init script" + dstack-util notify-host -e "boot.progress" -d "init-script" || true + source <(jq -r '.init_script' app-compose.json) fi + +RUNNER=$(jq -r '.runner' app-compose.json) +case "$RUNNER" in +docker-compose) + if [[ ! -f docker-compose.yaml ]]; then + jq -r '.docker_compose_file' app-compose.json >docker-compose.yaml + fi + dstack-util remove-orphans --no-dockerd -f docker-compose.yaml || true + ;; +esac diff --git a/cc-eventlog/Cargo.toml b/cc-eventlog/Cargo.toml index 9ff79736..2863760f 100644 --- a/cc-eventlog/Cargo.toml +++ b/cc-eventlog/Cargo.toml @@ -12,6 +12,8 @@ license.workspace = true [dependencies] anyhow.workspace = true +digest = "0.10.7" +ez-hash.workspace = true fs-err.workspace = true hex.workspace = true scale.workspace = true diff --git a/cc-eventlog/samples/tpm_eventlog.bin b/cc-eventlog/samples/tpm_eventlog.bin new file mode 100644 index 00000000..bf8459f0 Binary files /dev/null and b/cc-eventlog/samples/tpm_eventlog.bin differ diff --git a/cc-eventlog/src/lib.rs b/cc-eventlog/src/lib.rs index 50f491cc..93bbc77f 100644 --- a/cc-eventlog/src/lib.rs +++ b/cc-eventlog/src/lib.rs @@ -2,288 +2,14 @@ // // SPDX-License-Identifier: Apache-2.0 -use crate::codecs::VecOf; -use anyhow::{Context, Result}; -use scale::Decode; -use serde::{Deserialize, Serialize}; -use tcg::{TcgDigest, TcgEfiSpecIdEvent}; +pub use runtime_events::{replay_events, RuntimeEvent}; +pub use tdx::TdxEvent; mod codecs; +mod runtime_events; mod tcg; - -/// The path to the userspace TDX event log file. -pub const RUNTIME_EVENT_LOG_FILE: &str = "/run/log/tdx_mr3/tdx_events.log"; -/// The path to boottime ccel file. -const CCEL_FILE: &str = "/sys/firmware/acpi/tables/data/CCEL"; - -/// This is the common struct for tcg event logs to be delivered in different formats. -/// Currently TCG supports several event log formats defined in TCG_PCClient Spec, -/// Canonical Eventlog Spec, etc. -/// This struct provides the functionality to convey event logs in different format -/// according to request. -#[derive(Clone, scale::Decode)] -pub struct TcgEventLog { - /// IMR index, starts from 1 - pub imr_index: u32, - /// Event type - pub event_type: u32, - /// List of digests - pub digests: VecOf, - /// Raw event data - pub event: VecOf, -} - -/// This is the TDX event log format that is used to store the event log in the TDX guest. -/// It is a simplified version of the TCG event log format, containing only a single digest -/// and the raw event data. The IMR index is zero-based, unlike the TCG event log format -/// which is one-based. -/// -/// As for RTMR3, the digest extended is calculated as `sha384(event_type.to_ne_bytes() || b":" || event || b":" || event_payload)`. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TdxEventLog { - /// IMR index, starts from 0 - pub imr: u32, - /// Event type - pub event_type: u32, - /// Digest - #[serde(with = "serde_human_bytes")] - pub digest: [u8; 48], - /// Event name - pub event: String, - /// Event payload - #[serde(with = "serde_human_bytes")] - pub event_payload: Vec, -} - -fn event_digest(ty: u32, event: &str, payload: &[u8]) -> [u8; 48] { - use sha2::Digest; - let mut hasher = sha2::Sha384::new(); - hasher.update(ty.to_ne_bytes()); - hasher.update(b":"); - hasher.update(event.as_bytes()); - hasher.update(b":"); - hasher.update(payload); - hasher.finalize().into() -} - -impl TdxEventLog { - pub fn new(imr: u32, event_type: u32, event: String, event_payload: Vec) -> Self { - let digest = event_digest(event_type, &event, &event_payload); - Self { - imr, - event_type, - digest, - event, - event_payload, - } - } - - pub fn new_str(imr: u32, event_type: u32, event: &str, event_payload: &str) -> Self { - Self::new( - imr, - event_type, - event.to_string(), - event_payload.as_bytes().to_vec(), - ) - } - - pub fn validate(&self) -> Result<()> { - if self.imr != 3 { - // TODO: validate other imrs - return Ok(()); - } - let digest = event_digest(self.event_type, &self.event, &self.event_payload); - if digest != self.digest { - return Err(anyhow::anyhow!("invalid digest")); - } - Ok(()) - } -} - -impl TryFrom for TdxEventLog { - type Error = anyhow::Error; - - fn try_from(value: TcgEventLog) -> Result { - if value.digests.len() != 1 { - return Err(anyhow::anyhow!( - "expected 1 digest, got {}", - value.digests.len() - )); - } - let digest = value - .digests - .into_inner() - .into_iter() - .next() - .context("digest not found")? - .hash - .try_into() - .ok() - .context("invalid digest size")?; - Ok(TdxEventLog { - imr: value - .imr_index - .checked_sub(1) - .context("invalid imr index")?, - event_type: value.event_type, - digest, - event: Default::default(), - event_payload: value.event.into(), - }) - } -} - -impl core::fmt::Debug for TcgEventLog { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("TcgEventLog") - .field("imr_index", &self.imr_index) - .field("event_type", &self.event_type) - .field( - "digests", - &self - .digests - .iter() - .map(|d| hex::encode(&d.hash)) - .collect::>(), - ) - .field("event", &hex::encode(&self.event)) - .finish() - } -} - -const fn alg_id_to_digest_size(alg_id: u16) -> Option { - use tcg::*; - match alg_id { - TPM_ALG_SHA1 => Some(20), - TPM_ALG_SHA256 => Some(32), - TPM_ALG_SHA384 => Some(48), - TPM_ALG_SHA512 => Some(64), - _ => None, - } -} - -#[derive(Clone, Debug)] -pub struct EventLogs { - pub spec_id_header_event: TcgEfiSpecIdEvent, - pub event_logs: Vec, -} - -impl scale::Decode for TcgDigest { - fn decode(input: &mut I) -> Result { - let algo_id = u16::decode(input)?; - let digest_size = - alg_id_to_digest_size(algo_id).ok_or(scale::Error::from("Unsupported algorithm ID"))?; - let mut digest_data = vec![0; digest_size as usize]; - input - .read(&mut digest_data) - .map_err(|_| scale::Error::from("failed to read digest_data"))?; - Ok(TcgDigest { - algo_id, - hash: digest_data, - }) - } -} - -impl EventLogs { - pub fn decode(input: &mut &[u8]) -> Result { - let (_spec_id_header, spec_id_header_event) = - parse_spec_id_event_log(input).context("Failed to parse spec id event")?; - let mut event_logs = vec![]; - loop { - // A tmp head_buffer is used to peek the imr and event type - let head_buffer = &mut &input[..]; - let imr = u32::decode(head_buffer).context("failed to decode imr")?; - if imr == 0xFFFFFFFF { - break; - } - let event_log = TcgEventLog::decode(input).context("Failed to parse event log")?; - event_logs.push(event_log); - } - Ok(EventLogs { - spec_id_header_event, - event_logs, - }) - } - - pub fn decode_from_ccel_file() -> Result { - let data = fs_err::read(CCEL_FILE).context("Failed to read CCEL")?; - Self::decode(&mut data.as_slice()) - } - - pub fn into_tdx_event_logs(self) -> Result> { - self.event_logs - .into_iter() - .map(TdxEventLog::try_from) - .collect() - } - - pub fn to_tdx_event_logs(&self) -> Result> { - self.event_logs - .iter() - .cloned() - .map(TdxEventLog::try_from) - .collect() - } -} - -fn parse_spec_id_event_log( - input: &mut I, -) -> Result<(TcgEventLog, TcgEfiSpecIdEvent)> { - #[derive(Decode)] - struct Header { - imr_index: u32, - header_event_type: u32, - digest_hash: [u8; 20], - header_event: VecOf, - } - - let decoded_header = Header::decode(input).context("failed to decode log_item")?; - // Parse EFI Spec Id Event structure - let input = &mut decoded_header.header_event.as_slice(); - let spec_id_event = - TcgEfiSpecIdEvent::decode(input).context("failed to decode TcgEfiSpecIdEvent")?; - - let digests = vec![TcgDigest { - algo_id: tcg::TPM_ALG_ERROR, - hash: decoded_header.digest_hash.to_vec(), - }]; - let spec_id_header = TcgEventLog { - imr_index: decoded_header.imr_index, - event_type: decoded_header.header_event_type, - digests: (digests.len() as u32, digests).into(), - event: decoded_header.header_event, - }; - Ok((spec_id_header, spec_id_event)) -} - -fn read_runtime_event_logs() -> Result> { - let data = match fs_err::read_to_string(RUNTIME_EVENT_LOG_FILE) { - Ok(data) => data, - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - return Ok(vec![]); - } - return Err(e).context("Failed to read user event log"); - } - }; - let mut event_logs = vec![]; - for line in data.lines() { - if line.trim().is_empty() { - continue; - } - let event_log = - serde_json::from_str::(line).context("Failed to decode user event log")?; - event_logs.push(event_log); - } - Ok(event_logs) -} - -/// Read both boottime and runtime event logs. -pub fn read_event_logs() -> Result> { - let mut event_logs = EventLogs::decode_from_ccel_file()?.to_tdx_event_logs()?; - event_logs.extend(read_runtime_event_logs()?); - Ok(event_logs) -} +pub mod tdx; +pub mod tpm; #[cfg(test)] mod tests { @@ -292,9 +18,9 @@ mod tests { #[test] fn parse_ccel() { let boot_time_data = include_bytes!("../samples/ccel.bin"); - let event_logs = EventLogs::decode(&mut boot_time_data.as_slice()).unwrap(); + let event_logs = tcg::TcgEventLog::decode(&mut boot_time_data.as_slice()).unwrap(); insta::assert_debug_snapshot!(&event_logs.event_logs); - let tdx_event_logs = event_logs.to_tdx_event_logs().unwrap(); + let tdx_event_logs = event_logs.to_cc_event_log().unwrap(); let json = serde_json::to_string_pretty(&tdx_event_logs).unwrap(); insta::assert_snapshot!(json); } diff --git a/cc-eventlog/src/runtime_events.rs b/cc-eventlog/src/runtime_events.rs new file mode 100644 index 00000000..ace6cd97 --- /dev/null +++ b/cc-eventlog/src/runtime_events.rs @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, Result}; +use fs_err as fs; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use serde_human_bytes::base64; +use std::io::Write; + +use ez_hash::{Hasher, Sha256, Sha384}; + +/// The event type for dstack runtime events. +/// This code is not defined in the TCG specification. +/// See https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf +pub const DSTACK_RUNTIME_EVENT_TYPE: u32 = 0x08000001; +/// The path to the userspace TDX event log file. +pub const RUNTIME_EVENT_LOG_FILE: &str = "/run/log/dstack/runtime_events.log"; + +/// Abstraction of cross-platform runtime events. +#[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] +pub struct RuntimeEvent { + /// Event name + pub event: String, + /// Event payload + #[serde(with = "base64")] + pub payload: Vec, +} + +impl RuntimeEvent { + pub fn new(event: String, payload: Vec) -> Self { + Self { event, payload } + } + + pub fn read_all() -> Result> { + let data = match fs_err::read_to_string(RUNTIME_EVENT_LOG_FILE) { + Ok(data) => data, + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + return Ok(vec![]); + } + return Err(e).context("Failed to read user event log"); + } + }; + let mut event_logs = vec![]; + for line in data.lines() { + if line.trim().is_empty() { + continue; + } + let event_log = serde_json::from_str::(line) + .context("Failed to decode user event log")?; + event_logs.push(event_log); + } + Ok(event_logs) + } + + pub fn emit(&self) -> Result<()> { + let logline = serde_json::to_string(self).context("failed to serialize event log")?; + + let logfile_path = std::path::Path::new(RUNTIME_EVENT_LOG_FILE); + let logfile_dir = logfile_path + .parent() + .context("failed to get event log directory")?; + fs::create_dir_all(logfile_dir).context("failed to create event log directory")?; + + let mut logfile = fs::OpenOptions::new() + .append(true) + .create(true) + .open(logfile_path) + .context("failed to open event log file")?; + + logfile + .write_all(logline.as_bytes()) + .context("failed to write to event log file")?; + logfile + .write_all(b"\n") + .context("failed to write to event log file")?; + Ok(()) + } + + pub fn sha384_digest(&self) -> [u8; 48] { + self.digest::() + } + + pub fn sha256_digest(&self) -> [u8; 32] { + self.digest::() + } + + /// Compute the digest of the event. + pub fn digest(&self) -> H::Output { + H::hash([ + &DSTACK_RUNTIME_EVENT_TYPE.to_ne_bytes()[..], + b":", + self.event.as_bytes(), + b":", + &self.payload, + ]) + } + + pub fn cc_event_type(&self) -> u32 { + DSTACK_RUNTIME_EVENT_TYPE + } +} + +/// Replay event logs +pub fn replay_events(eventlog: &[RuntimeEvent], to_event: Option<&str>) -> H::Output { + let mut mr = H::zeros(); + for event in eventlog.iter() { + mr = H::hash((mr, event.digest::())); + if let Some(to_event) = to_event { + if event.event == to_event { + break; + } + } + } + mr +} diff --git a/cc-eventlog/src/tcg.rs b/cc-eventlog/src/tcg.rs index 602ecd21..ff0aadc3 100644 --- a/cc-eventlog/src/tcg.rs +++ b/cc-eventlog/src/tcg.rs @@ -4,7 +4,12 @@ // // SPDX-License-Identifier: Apache-2.0 -use crate::codecs::VecOf; +use crate::{codecs::VecOf, tdx::TdxEvent}; +use anyhow::{Context, Result}; +use scale::Decode; + +/// The path to boottime ccel file. +const CCEL_FILE: &str = "/sys/firmware/acpi/tables/data/CCEL"; pub const TPM_ALG_ERROR: u16 = 0x0; pub const TPM_ALG_RSA: u16 = 0x1; @@ -205,3 +210,166 @@ pub struct TcgEfiSpecIdEventAlgorithmSize { pub algo_id: u16, pub digest_size: u16, } + +/// This is the common struct for tcg event logs to be delivered in different formats. +/// Currently TCG supports several event log formats defined in TCG_PCClient Spec, +/// Canonical Eventlog Spec, etc. +/// This struct provides the functionality to convey event logs in different format +/// according to request. +#[derive(Clone, scale::Decode)] +pub struct TcgEvent { + /// IMR index, starts from 1 + pub imr_index: u32, + /// Event type + pub event_type: u32, + /// List of digests + pub digests: VecOf, + /// Raw event data + pub event: VecOf, +} + +impl core::fmt::Debug for TcgEvent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TcgEventLog") + .field("imr_index", &self.imr_index) + .field("event_type", &self.event_type) + .field( + "digests", + &self + .digests + .iter() + .map(|d| hex::encode(&d.hash)) + .collect::>(), + ) + .field("event", &hex::encode(&self.event)) + .finish() + } +} + +const fn alg_id_to_digest_size(alg_id: u16) -> Option { + match alg_id { + TPM_ALG_SHA1 => Some(20), + TPM_ALG_SHA256 => Some(32), + TPM_ALG_SHA384 => Some(48), + TPM_ALG_SHA512 => Some(64), + _ => None, + } +} + +#[derive(Clone, Debug)] +pub struct TcgEventLog { + pub spec_id_header_event: TcgEfiSpecIdEvent, + pub event_logs: Vec, +} + +impl scale::Decode for TcgDigest { + fn decode(input: &mut I) -> Result { + let algo_id = u16::decode(input)?; + let digest_size = + alg_id_to_digest_size(algo_id).ok_or(scale::Error::from("Unsupported algorithm ID"))?; + let mut digest_data = vec![0; digest_size as usize]; + input + .read(&mut digest_data) + .map_err(|_| scale::Error::from("failed to read digest_data"))?; + Ok(TcgDigest { + algo_id, + hash: digest_data, + }) + } +} + +impl TcgEventLog { + pub fn decode(input: &mut &[u8]) -> Result { + let (_spec_id_header, spec_id_header_event) = + parse_spec_id_event_log(input).context("Failed to parse spec id event")?; + let mut event_logs = vec![]; + loop { + // A tmp head_buffer is used to peek the imr and event type + let head_buffer = &mut &input[..]; + let imr = u32::decode(head_buffer).context("failed to decode imr")?; + if imr == 0xFFFFFFFF { + break; + } + let event_log = TcgEvent::decode(input).context("Failed to parse event log")?; + event_logs.push(event_log); + } + Ok(TcgEventLog { + spec_id_header_event, + event_logs, + }) + } + + pub fn decode_from_ccel_file() -> Result { + let data = fs_err::read(CCEL_FILE).context("Failed to read CCEL")?; + Self::decode(&mut data.as_slice()) + } + + pub fn to_cc_event_log(&self) -> Result> { + self.event_logs + .iter() + .filter(|log| log.imr_index > 0) // GCP fills some IMRs starting from 0 + .cloned() + .map(TdxEvent::try_from) + .collect() + } +} + +fn parse_spec_id_event_log( + input: &mut I, +) -> Result<(TcgEvent, TcgEfiSpecIdEvent)> { + #[derive(Decode)] + struct Header { + imr_index: u32, + header_event_type: u32, + digest_hash: [u8; 20], + header_event: VecOf, + } + + let decoded_header = Header::decode(input).context("failed to decode log_item")?; + // Parse EFI Spec Id Event structure + let input = &mut decoded_header.header_event.as_slice(); + let spec_id_event = + TcgEfiSpecIdEvent::decode(input).context("failed to decode TcgEfiSpecIdEvent")?; + + let digests = vec![TcgDigest { + algo_id: TPM_ALG_ERROR, + hash: decoded_header.digest_hash.to_vec(), + }]; + let spec_id_header = TcgEvent { + imr_index: decoded_header.imr_index, + event_type: decoded_header.header_event_type, + digests: (digests.len() as u32, digests).into(), + event: decoded_header.header_event, + }; + Ok((spec_id_header, spec_id_event)) +} + +impl TryFrom for TdxEvent { + type Error = anyhow::Error; + + fn try_from(value: TcgEvent) -> Result { + if value.digests.len() != 1 { + return Err(anyhow::anyhow!( + "expected 1 digest, got {}", + value.digests.len() + )); + } + let digest = value + .digests + .into_inner() + .into_iter() + .next() + .context("digest not found")? + .hash; + Ok(TdxEvent { + imr: value + .imr_index + .checked_sub(1) + .context("invalid IMR index: must be >= 1")?, + event_type: value.event_type, + digest, + event: Default::default(), + event_payload: value.event.into(), + }) + } +} diff --git a/cc-eventlog/src/tdx.rs b/cc-eventlog/src/tdx.rs new file mode 100644 index 00000000..bf7d677c --- /dev/null +++ b/cc-eventlog/src/tdx.rs @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + +use crate::{ + runtime_events::{RuntimeEvent, DSTACK_RUNTIME_EVENT_TYPE}, + tcg::TcgEventLog, +}; + +/// This is the TDX event log format that is used to store the event log in the TDX guest. +/// It is a simplified version of the TCG event log format, containing only a single digest +/// and the raw event data. The IMR index is zero-based, unlike the TCG event log format +/// which is one-based. +/// +/// As for RTMR3, the digest extended is calculated as `sha384(event_type.to_ne_bytes() || b":" || event || b":" || event_payload)`. +#[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] +pub struct TdxEvent { + /// IMR index, starts from 0 + pub imr: u32, + /// Event type + pub event_type: u32, + /// Digest + #[serde(with = "serde_human_bytes", default)] + pub digest: Vec, + /// Event name + pub event: String, + /// Event payload + #[serde(with = "serde_human_bytes")] + pub event_payload: Vec, +} + +impl TdxEvent { + pub fn new(imr: u32, event_type: u32, event: String, event_payload: Vec) -> Self { + Self { + imr, + event_type, + digest: vec![], + event, + event_payload, + } + } + + /// Create a version of this event with payload stripped (for size reduction). + /// Only call this on events where can_strip_payload() returns true. + pub fn stripped(&self) -> Self { + if self.is_runtime_event() { + Self { + imr: self.imr, + event_type: self.event_type, + digest: Vec::new(), + event: self.event.clone(), + event_payload: self.event_payload.clone(), + } + } else { + Self { + imr: self.imr, + event_type: self.event_type, + digest: self.digest.clone(), + event: self.event.clone(), + event_payload: Vec::new(), + } + } + } + + pub fn digest(&self) -> Vec { + if let Some(runtime_event) = self.to_runtime_event() { + return runtime_event.sha384_digest().to_vec(); + } + self.digest.clone() + } + + pub fn is_runtime_event(&self) -> bool { + self.event_type == DSTACK_RUNTIME_EVENT_TYPE + } + + pub fn to_runtime_event(&self) -> Option { + self.is_runtime_event().then_some(RuntimeEvent { + event: self.event.clone(), + payload: self.event_payload.clone(), + }) + } +} + +impl From for TdxEvent { + fn from(value: RuntimeEvent) -> Self { + TdxEvent { + imr: 3, + event_type: DSTACK_RUNTIME_EVENT_TYPE, + digest: value.sha384_digest().to_vec(), + event: value.event, + event_payload: value.payload, + } + } +} + +/// Read both boottime and runtime event logs. +pub fn read_event_log() -> Result> { + let mut event_logs = TcgEventLog::decode_from_ccel_file()?.to_cc_event_log()?; + event_logs.extend(RuntimeEvent::read_all()?.into_iter().map(Into::into)); + Ok(event_logs) +} diff --git a/cc-eventlog/src/tpm.rs b/cc-eventlog/src/tpm.rs new file mode 100644 index 00000000..2d70bd01 --- /dev/null +++ b/cc-eventlog/src/tpm.rs @@ -0,0 +1,245 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM Event Log parsing (binary_bios_measurements format) + +use crate::codecs::VecOf; +use crate::tcg::{TcgDigest, TcgEfiSpecIdEvent}; +use anyhow::{Context, Result}; +use scale::Decode; +use serde::{Deserialize, Serialize}; + +/// Simplified TPM event for PCR replay +#[derive(Clone, Debug, Serialize, Deserialize, scale::Encode, scale::Decode)] +pub struct TpmEvent { + /// PCR index this event was extended to + pub pcr_index: u32, + /// SHA-256 digest of the event data + #[serde(with = "serde_human_bytes")] + pub digest: Vec, +} + +/// TCG_PCR_EVENT2 format +/// +/// See TCG PC Client Platform Firmware Profile spec section 9.2.2 +#[derive(Clone, Decode)] +struct TpmRawEvent { + pcr_index: u32, + event_type: u32, + digests: VecOf, + event: VecOf, +} + +impl core::fmt::Debug for TpmRawEvent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TpmRawEvent") + .field("pcr_index", &self.pcr_index) + .field("event_type", &self.event_type) + .field( + "digests", + &self + .digests + .iter() + .map(|d| hex::encode(&d.hash)) + .collect::>(), + ) + .field("event", &hex::encode(&self.event)) + .finish() + } +} + +impl TpmRawEvent { + fn sha256_digest(&self) -> Option> { + self.digests + .iter() + .find(|d| d.algo_id == crate::tcg::TPM_ALG_SHA256) + .map(|d| d.hash.clone()) + } + + fn is_extended_to_pcr(&self) -> bool { + self.event_type != crate::tcg::EV_NO_ACTION + } + + fn to_simple_event(&self) -> Option { + if !self.is_extended_to_pcr() { + return None; + } + self.sha256_digest().map(|digest| TpmEvent { + pcr_index: self.pcr_index, + digest, + }) + } +} + +#[derive(Clone, Debug)] +pub struct TpmEventLog { + pub spec_id_header_event: TcgEfiSpecIdEvent, + pub events: Vec, +} + +impl TpmEventLog { + /// Decode from binary_bios_measurements format + /// + /// First event is TCG_PCClientPCREvent (legacy format with SHA-1). + /// Subsequent events are TCG_PCR_EVENT2 (crypto-agile format). + pub fn decode(input: &mut &[u8]) -> Result { + let (_spec_id_header, spec_id_header_event) = + parse_spec_id_event(input).context("Failed to parse spec id event")?; + + let mut events = vec![]; + loop { + let head_buffer = &mut &input[..]; + let pcr_index = match u32::decode(head_buffer) { + Ok(idx) => idx, + Err(_) => break, + }; + + if pcr_index == 0xFFFFFFFF { + break; + } + + let raw_event = TpmRawEvent::decode(input).context("Failed to decode TPM event")?; + + if let Some(event) = raw_event.to_simple_event() { + events.push(event); + } + } + + Ok(TpmEventLog { + spec_id_header_event, + events, + }) + } + + /// Read and decode TPM Event Log from kernel sysfs + pub fn from_kernel_file() -> Result { + const TPM_BINARY_BIOS_MEASUREMENTS: &str = + "/sys/kernel/security/tpm0/binary_bios_measurements"; + + let data = fs_err::read(TPM_BINARY_BIOS_MEASUREMENTS) + .context("Failed to read TPM binary_bios_measurements")?; + + Self::decode(&mut data.as_slice()) + } + + /// Filter events by PCR index + pub fn filter_by_pcr(&self, pcr_index: u32) -> Vec { + self.events + .iter() + .filter(|e| e.pcr_index == pcr_index) + .cloned() + .collect() + } + + /// Get all PCR 2 events (boot loader and OS measurements) + pub fn pcr2_events(&self) -> Vec { + self.filter_by_pcr(2) + } +} + +/// Parse Spec ID Event in legacy TCG_PCClientPCREvent format +fn parse_spec_id_event(input: &mut I) -> Result<(TpmRawEvent, TcgEfiSpecIdEvent)> { + #[derive(Decode)] + struct SpecIdHeader { + pcr_index: u32, + event_type: u32, + digest_sha1: [u8; 20], + event: VecOf, + } + + let header = SpecIdHeader::decode(input).context("failed to decode spec id header")?; + + let spec_id_event = TcgEfiSpecIdEvent::decode(&mut header.event.as_slice()) + .context("failed to decode TcgEfiSpecIdEvent")?; + + let digests = vec![TcgDigest { + algo_id: crate::tcg::TPM_ALG_SHA1, + hash: header.digest_sha1.to_vec(), + }]; + + let raw_event = TpmRawEvent { + pcr_index: header.pcr_index, + event_type: header.event_type, + digests: (digests.len() as u32, digests).into(), + event: header.event, + }; + + Ok((raw_event, spec_id_event)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_empty() { + let result = TpmEventLog::decode(&mut &[][..]); + assert!(result.is_err()); + } + + #[test] + fn test_decode_gcp_tpm_eventlog() { + let data = include_bytes!("../samples/tpm_eventlog.bin"); + let event_log = TpmEventLog::decode(&mut data.as_slice()).unwrap(); + + assert!(!event_log.events.is_empty()); + assert_eq!(event_log.spec_id_header_event.platform_class, 0); + + let pcr2_events = event_log.pcr2_events(); + assert_eq!(pcr2_events.len(), 4); + + assert_eq!( + hex::encode(&pcr2_events[0].digest), + "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119" + ); + + assert_eq!( + hex::encode(&pcr2_events[1].digest), + "00b8a357e652623798d1bbd16c375ec90fbed802b4269affa3e78e6eb19386cf" + ); + + // Event 28: UKI Authenticode hash + assert_eq!( + hex::encode(&pcr2_events[2].digest), + "9ab14a46f858662a89adc102d2a57a13f52f75c1769d65a4c34edbbfc8855f0f" + ); + + // Event 41: Linux kernel Authenticode hash + assert_eq!( + hex::encode(&pcr2_events[3].digest), + "ade943a0a7a3189a3201ba17d7df778eb380cbd33ce5e361176e974ccf7cdedb" + ); + } + + #[test] + fn test_filter_by_pcr() { + let data = include_bytes!("../samples/tpm_eventlog.bin"); + let event_log = TpmEventLog::decode(&mut data.as_slice()).unwrap(); + + let pcr0_events = event_log.filter_by_pcr(0); + assert!(!pcr0_events.is_empty()); + + let pcr2_events = event_log.filter_by_pcr(2); + assert_eq!(pcr2_events.len(), 4); + + let pcr99_events = event_log.filter_by_pcr(99); + assert_eq!(pcr99_events.len(), 0); + } + + #[test] + fn test_pcr2_uki_hash_extraction() { + let data = include_bytes!("../samples/tpm_eventlog.bin"); + let event_log = TpmEventLog::decode(&mut data.as_slice()).unwrap(); + + let pcr2_events = event_log.pcr2_events(); + assert!(pcr2_events.len() >= 3); + + let uki_hash = &pcr2_events[2].digest; + let expected_uki_hash = + hex::decode("9ab14a46f858662a89adc102d2a57a13f52f75c1769d65a4c34edbbfc8855f0f") + .unwrap(); + + assert_eq!(uki_hash, &expected_uki_hash); + } +} diff --git a/cert-client/src/lib.rs b/cert-client/src/lib.rs index 3be6bb47..a41fa3bf 100644 --- a/cert-client/src/lib.rs +++ b/cert-client/src/lib.rs @@ -7,15 +7,14 @@ use dstack_kms_rpc::{kms_client::KmsClient, SignCertRequest}; use dstack_types::{AppKeys, KeyProvider}; use ra_rpc::client::{RaClient, RaClientConfig}; use ra_tls::{ - attestation::QuoteContentType, - cert::{generate_ra_cert, CaCert, CertConfig, CertSigningRequest}, + attestation::{QuoteContentType, VersionedAttestation}, + cert::{generate_ra_cert, CaCert, CertConfig, CertSigningRequestV2, Csr}, rcgen::KeyPair, }; -use tdx_attest::{eventlog::read_event_logs, get_quote}; pub enum CertRequestClient { Local { - ca: CaCert, + ca: Box, }, Kms { client: KmsClient, @@ -26,7 +25,7 @@ pub enum CertRequestClient { impl CertRequestClient { pub async fn sign_csr( &self, - csr: &CertSigningRequest, + csr: &CertSigningRequestV2, signature: &[u8], ) -> Result> { match self { @@ -39,7 +38,7 @@ impl CertRequestClient { CertRequestClient::Kms { client, vm_config } => { let response = client .sign_cert(SignCertRequest { - api_version: 1, + api_version: 2, csr: csr.to_vec(), signature: signature.to_vec(), vm_config: vm_config.clone(), @@ -63,9 +62,12 @@ impl CertRequestClient { vm_config: String, ) -> Result { match &keys.key_provider { - KeyProvider::None { key } | KeyProvider::Local { key, .. } => { + KeyProvider::None { key } + | KeyProvider::Local { key, .. } + | KeyProvider::Tpm { key, .. } => { let ca = CaCert::new(keys.ca_cert.clone(), key.clone()) .context("Failed to create CA")?; + let ca = Box::new(ca); Ok(CertRequestClient::Local { ca }) } KeyProvider::Kms { @@ -96,26 +98,22 @@ impl CertRequestClient { &self, key: &KeyPair, config: CertConfig, - no_ra: bool, + attestation_override: Option, ) -> Result> { let pubkey = key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let (quote, event_log) = if !no_ra { - let (_, quote) = get_quote(&report_data, None).context("Failed to get quote")?; - let event_log = read_event_logs().context("Failed to decode event log")?; - let event_log = - serde_json::to_vec(&event_log).context("Failed to serialize event log")?; - (quote, event_log) - } else { - (vec![], vec![]) + let attestation = match attestation_override { + Some(attestation) => attestation, + None => ra_rpc::Attestation::quote(&report_data) + .context("Failed to get quote for cert pubkey")? + .into_versioned(), }; - let csr = CertSigningRequest { + let csr = CertSigningRequestV2 { confirm: "please sign cert:".to_string(), pubkey, config, - quote, - event_log, + attestation, }; let signature = csr.signed_by(key).context("Failed to sign the CSR")?; self.sign_csr(&csr, &signature) diff --git a/docs/security-guide/cvm-boundaries.md b/docs/security-guide/cvm-boundaries.md index b15c844c..b658ffae 100644 --- a/docs/security-guide/cvm-boundaries.md +++ b/docs/security-guide/cvm-boundaries.md @@ -33,6 +33,7 @@ This is the main configuration file for the application in JSON format: | kms_enabled | 0.3.1 | boolean | Enable/disable KMS | | gateway_enabled | 0.3.1 | boolean | Enable/disable gateway | | local_key_provider_enabled | 0.3.1 | boolean | Use a local key provider | +| key_provider_id | 0.5.1 | string | Key provider ID. | | public_logs | 0.3.3 | boolean | Whether logs are publicly visible | | public_sysinfo | 0.3.3 | boolean | Whether system info is public | | public_tcbinfo | 0.5.1 | boolean | Whether TCB info is public | @@ -43,6 +44,7 @@ This is the main configuration file for the application in JSON format: | init_script | 0.5.5 | string | Bash script that executed prior to dockerd startup | | storage_fs | 0.5.5 | string | Filesystem type for the data disk of the CVM. Supported values: "zfs", "ext4". default to "zfs". **ZFS:** Ensures filesystem integrity with built-in data protection features. **ext4:** Provides better performance for database applications with lower overhead and faster I/O operations, but no strong integrity protection. | | swap_size | 0.5.5 | string/integer | The linux swap size. default to 0. Can be in byte or human-readable format (e.g., "1G", "256M"). | +| key_provider | 0.5.6 | string | Key provider type. Supported values: "none", "kms", "local", "tpm". | The hash of this file content is extended to RTMR3 as event name `compose-hash`. Remote verifier can extract the compose-hash during remote attestation. @@ -120,7 +122,7 @@ dstack uses encrypted environment variables to allow app developers to securely This file is not measured to RTMRs. But it is highly recommended to add application-specific integrity checks on encrypted environment variables at the application layer. See [security-guide.md](security-guide.md) for more details. ### .user-config -This is an optional application-specific configuration file that applications inside the CVM can access. dstack OS simply stores it at /dstack/user-config without any measurement or additional processing. +This is an optional application-specific configuration file that applications inside the CVM can access. dstack OS simply stores it at /dstack/.host-shared/.user-config without any measurement or additional processing. Application developers should perform integrity checks on user_config at the application layer if necessary. diff --git a/docs/vmm-cli-user-guide.md b/docs/vmm-cli-user-guide.md index 242d024b..5befa43a 100644 --- a/docs/vmm-cli-user-guide.md +++ b/docs/vmm-cli-user-guide.md @@ -250,7 +250,7 @@ Deploy your application with the compose file: - `--gpu`: GPU assignments - `--ppcie`: Enable PPCIE mode (attach ALL available GPUs and NVSwitches) - `--env-file`: Environment variables file -- `--user-config`: Path to user config file (will be placed at `/dstack/.user-config` in the CVM) +- `--user-config`: Path to user config file (will be placed at `/dstack/.host-shared/.user-config` in the CVM) - `--kms-url`: KMS server URL - `--gateway-url`: Gateway server URL - `--stopped`: Create VM in stopped state (requires dstack-vmm >= 0.5.4) diff --git a/dstack-attest/Cargo.toml b/dstack-attest/Cargo.toml new file mode 100644 index 00000000..7ccfca47 --- /dev/null +++ b/dstack-attest/Cargo.toml @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "dstack-attest" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +cc-eventlog.workspace = true +dcap-qvl.workspace = true +dstack-types.workspace = true +ez-hash.workspace = true +fs-err.workspace = true +hex.workspace = true +hex_fmt.workspace = true +or-panic.workspace = true +scale = { workspace = true, features = ["derive"] } +serde.workspace = true +serde-human-bytes.workspace = true +serde_json.workspace = true +sha2.workspace = true +sha3.workspace = true +tdx-attest.workspace = true +tpm-attest.workspace = true +tpm-qvl.workspace = true +tpm-types.workspace = true + +[features] +quote = [] \ No newline at end of file diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs new file mode 100644 index 00000000..a031efe1 --- /dev/null +++ b/dstack-attest/src/attestation.rs @@ -0,0 +1,817 @@ +// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Attestation functions + +use std::{borrow::Cow, str::FromStr}; + +use anyhow::{anyhow, bail, Context, Result}; +use cc_eventlog::{RuntimeEvent, TdxEvent}; +use dcap_qvl::{ + quote::{EnclaveReport, Quote, Report, TDReport10, TDReport15}, + verify::VerifiedReport as TdxVerifiedReport, +}; +use dstack_types::Platform; +use ez_hash::{sha256, Hasher, Sha256, Sha384}; +use or_panic::ResultOrPanic; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use serde_human_bytes as hex_bytes; +use sha2::Digest as _; +use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; + +// Re-export TpmQuote from tpm-types +pub use tpm_types::TpmQuote; + +/// Attestation mode +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] +pub enum AttestationMode { + /// Intel TDX with DCAP quote only + #[serde(rename = "dstack-tdx")] + #[default] + DstackTdx, + /// GCP TDX with DCAP quote only + #[serde(rename = "gcp-tdx")] + GcpTdx, + /// Dstack attestation SDK in AWS Nitro Enclave + #[serde(rename = "dstack-nitro")] + DstackNitro, +} + +impl FromStr for AttestationMode { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "dstack-tdx" => Ok(Self::DstackTdx), + "gcp-tdx" => Ok(Self::GcpTdx), + "dstack-nitro" => Ok(Self::DstackNitro), + _ => bail!("Invalid attestation mode: {s}"), + } + } +} + +impl AttestationMode { + /// Get string representation + pub fn as_str(&self) -> &str { + match self { + Self::DstackTdx => "dstack-tdx", + Self::GcpTdx => "gcp-tdx", + Self::DstackNitro => "dstack-nitro", + } + } + + /// Detect attestation mode from system + pub fn detect() -> Result { + let has_tdx = std::path::Path::new("/dev/tdx_guest").exists(); + + // First, try to detect platform from DMI product name + let platform = Platform::detect_or_dstack(); + match platform { + Platform::Dstack => { + if has_tdx { + return Ok(Self::DstackTdx); + } + bail!("Unsupported platform: Dstack(-tdx)"); + } + Platform::Gcp => { + // GCP platform: TDX + TPM dual mode + if has_tdx { + return Ok(Self::GcpTdx); + } + bail!("Unsupported platform: GCP(-tdx)"); + } + } + } + + /// Check if TDX quote should be included + pub fn has_tdx(&self) -> bool { + match self { + Self::DstackTdx => true, + Self::GcpTdx => true, + Self::DstackNitro => false, + } + } + + /// Check if TPM quote should be included + pub fn has_tpm(&self) -> bool { + match self { + Self::DstackTdx => false, + Self::GcpTdx => true, + Self::DstackNitro => true, + } + } + + /// Get TPM runtime event PCR index + pub fn tpm_runtime_pcr(&self) -> Option { + match self { + Self::GcpTdx => Some(14), + Self::DstackTdx => None, + Self::DstackNitro => None, + } + } +} + +/// The content type of a quote. A CVM should only generate quotes for these types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QuoteContentType<'a> { + /// The public key of KMS root CA + KmsRootCa, + /// The public key of the RA-TLS certificate + RaTlsCert, + /// App defined data + AppData, + /// The custom content type + Custom(&'a str), +} + +/// The default hash algorithm used to hash the report data. +pub const DEFAULT_HASH_ALGORITHM: &str = "sha512"; + +impl QuoteContentType<'_> { + /// The tag of the content type used in the report data. + pub fn tag(&self) -> &str { + match self { + Self::KmsRootCa => "kms-root-ca", + Self::RaTlsCert => "ratls-cert", + Self::AppData => "app-data", + Self::Custom(tag) => tag, + } + } + + /// Convert the content to the report data. + pub fn to_report_data(&self, content: &[u8]) -> [u8; 64] { + self.to_report_data_with_hash(content, "") + .or_panic("sha512 hash should not fail") + } + + /// Convert the content to the report data with a specific hash algorithm. + pub fn to_report_data_with_hash(&self, content: &[u8], hash: &str) -> Result<[u8; 64]> { + macro_rules! do_hash { + ($hash: ty) => {{ + // The format is: + // hash(:) + let mut hasher = <$hash>::new(); + hasher.update(self.tag().as_bytes()); + hasher.update(b":"); + hasher.update(content); + let output = hasher.finalize(); + + let mut padded = [0u8; 64]; + padded[..output.len()].copy_from_slice(&output); + padded + }}; + } + let hash = if hash.is_empty() { + DEFAULT_HASH_ALGORITHM + } else { + hash + }; + let output = match hash { + "sha256" => do_hash!(sha2::Sha256), + "sha384" => do_hash!(sha2::Sha384), + "sha512" => do_hash!(sha2::Sha512), + "sha3-256" => do_hash!(sha3::Sha3_256), + "sha3-384" => do_hash!(sha3::Sha3_384), + "sha3-512" => do_hash!(sha3::Sha3_512), + "keccak256" => do_hash!(sha3::Keccak256), + "keccak384" => do_hash!(sha3::Keccak384), + "keccak512" => do_hash!(sha3::Keccak512), + "raw" => content.try_into().ok().context("invalid content length")?, + _ => bail!("invalid hash algorithm"), + }; + Ok(output) + } +} + +/// Represents a verified attestation +#[derive(Clone)] +pub struct DstackVerifiedReport { + /// The attestation mode + pub mode: AttestationMode, + /// The verified TDX report + pub tdx_report: Option, + /// The verified TPM report + pub tpm_report: Option, +} + +fn get_report_data(report: &TdxVerifiedReport) -> [u8; 64] { + match report.report { + Report::SgxEnclave(enclave_report) => enclave_report.report_data, + Report::TD10(tdreport10) => tdreport10.report_data, + Report::TD15(tdreport15) => tdreport15.base.report_data, + } +} + +impl DstackVerifiedReport { + /// Check if the report is empty + pub fn is_empty(&self) -> bool { + self.tdx_report.is_none() && self.tpm_report.is_none() + } +} + +/// Represents a verified attestation +pub type VerifiedAttestation = Attestation; + +/// Represents a TDX quote +#[derive(Clone, Encode, Decode)] +pub struct TdxQuote { + /// The quote gererated by Intel QE + pub quote: Vec, + /// The event log + pub event_log: Vec, +} + +/// Represents a versioned attestation +#[derive(Clone, Encode, Decode)] +pub enum VersionedAttestation { + /// Version 0 + V0 { + /// The attestation report + attestation: Attestation, + }, +} + +impl VersionedAttestation { + /// Decode VerifiedAttestation from scale encoded bytes + pub fn from_scale(scale: &[u8]) -> Result { + Self::decode(&mut &scale[..]).context("Failed to decode VersionedAttestation") + } + + /// Encode to scale encoded bytes + pub fn to_scale(&self) -> Vec { + self.encode() + } + + /// Turn into latest version of attestation + pub fn into_inner(self) -> Attestation { + match self { + Self::V0 { attestation } => attestation, + } + } + + /// Strip data for certificate embedding (e.g. keep RTMR3 event logs only). + pub fn into_stripped(mut self) -> Self { + let VersionedAttestation::V0 { attestation } = &mut self; + if let Some(tdx_quote) = &mut attestation.tdx_quote { + tdx_quote.event_log = tdx_quote + .event_log + .iter() + .filter(|e| e.imr == 3) + .map(|e| e.stripped()) + .collect(); + } + self + } +} + +/// Attestation data +#[derive(Clone, Encode, Decode)] +pub struct Attestation { + /// Attestation mode + pub mode: AttestationMode, + + /// TDX quote (only for TDX mode) + pub tdx_quote: Option, + + /// TPM quote (only for TPM mode) + pub tpm_quote: Option, + + /// Runtime events (only for TDX mode) + pub runtime_events: Vec, + + /// The report data + pub report_data: [u8; 64], + + /// The configuration of the VM + pub config: String, + + /// Verified report + pub report: R, +} + +impl Attestation { + /// Get TDX quote bytes + pub fn get_tdx_quote_bytes(&self) -> Option> { + self.tdx_quote.as_ref().map(|q| q.quote.clone()) + } + + /// Get TDX event log bytes + pub fn get_tdx_event_log_bytes(&self) -> Option> { + self.tdx_quote + .as_ref() + .map(|q| serde_json::to_vec(&q.event_log).unwrap_or_default()) + } + + /// Get TDX event log string + pub fn get_tdx_event_log_string(&self) -> Option { + self.tdx_quote + .as_ref() + .map(|q| serde_json::to_string(&q.event_log).unwrap_or_default()) + } + + pub fn get_td10_report(&self) -> Option { + self.tdx_quote + .as_ref() + .and_then(|q| Quote::parse(&q.quote).ok()) + .and_then(|quote| quote.report.as_td10().cloned()) + } +} + +pub trait GetPpid { + fn get_ppid(&self) -> Vec; +} + +impl GetPpid for () { + fn get_ppid(&self) -> Vec { + Vec::new() + } +} + +impl GetPpid for DstackVerifiedReport { + fn get_ppid(&self) -> Vec { + let Some(tdx_report) = &self.tdx_report else { + return Vec::new(); + }; + tdx_report.ppid.clone() + } +} + +struct Mrs { + mr_system: [u8; 32], + mr_aggregated: [u8; 32], +} + +impl Attestation { + fn decode_mr_tpm(&self, boottime_mr: bool, mr_key_provider: &[u8]) -> Result { + let os_image_hash = self.find_event_payload("os-image-hash").unwrap_or_default(); + let mr_system = sha256([&os_image_hash, mr_key_provider]); + let tpm_quote = self.tpm_quote.as_ref().context("TPM quote not found")?; + let pcr0 = tpm_quote + .pcr_values + .iter() + .find(|p| p.index == 0) + .context("PCR 0 not found")?; + let pcr2 = tpm_quote + .pcr_values + .iter() + .find(|p| p.index == 2) + .context("PCR 2 not found")?; + let runtime_pcr = + self.replay_runtime_events::(boottime_mr.then_some("boot-mr-done")); + let mr_aggregated = sha256([&pcr0.value[..], &pcr2.value, &runtime_pcr]); + Ok(Mrs { + mr_system, + mr_aggregated, + }) + } + + fn decode_mr_tdx(&self, boottime_mr: bool, mr_key_provider: &[u8]) -> Result { + let quote = self.decode_tdx_quote()?; + let rtmr3 = self.replay_runtime_events::(boottime_mr.then_some("boot-mr-done")); + let td_report = quote.report.as_td10().context("TDX report not found")?; + let mr_system = sha256([ + &td_report.mr_td[..], + &td_report.rt_mr0, + &td_report.rt_mr1, + &td_report.rt_mr2, + mr_key_provider, + ]); + let mr_aggregated = { + let mut hasher = sha2::Sha256::new(); + for d in [ + &td_report.mr_td, + &td_report.rt_mr0, + &td_report.rt_mr1, + &td_report.rt_mr2, + &rtmr3, + ] { + hasher.update(d); + } + // For backward compatibility. Don't include mr_config_id, mr_owner, mr_owner_config if they are all 0. + if td_report.mr_config_id != [0u8; 48] + || td_report.mr_owner != [0u8; 48] + || td_report.mr_owner_config != [0u8; 48] + { + hasher.update(td_report.mr_config_id); + hasher.update(td_report.mr_owner); + hasher.update(td_report.mr_owner_config); + } + hasher.finalize().into() + }; + Ok(Mrs { + mr_system, + mr_aggregated, + }) + } + + /// Decode the app info from the event log + pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + let key_provider_info = if boottime_mr { + vec![] + } else { + self.find_event_payload("key-provider").unwrap_or_default() + }; + let mr_key_provider = if key_provider_info.is_empty() { + [0u8; 32] + } else { + sha256(&key_provider_info) + }; + let mrs = match self.mode { + AttestationMode::DstackTdx => self.decode_mr_tdx(boottime_mr, &mr_key_provider)?, + AttestationMode::GcpTdx => self.decode_mr_tpm(boottime_mr, &mr_key_provider)?, + AttestationMode::DstackNitro => bail!("Nitro attestation is not supported"), + }; + Ok(AppInfo { + app_id: self.find_event_payload("app-id").unwrap_or_default(), + compose_hash: self.find_event_payload("compose-hash").unwrap_or_default(), + instance_id: self.find_event_payload("instance-id").unwrap_or_default(), + os_image_hash: self.find_event_payload("os-image-hash").unwrap_or_default(), + device_id: sha256(self.report.get_ppid()).to_vec(), + mr_system: mrs.mr_system, + mr_aggregated: mrs.mr_aggregated, + key_provider_info, + }) + } +} + +impl Attestation { + /// Decode the quote + pub fn decode_tdx_quote(&self) -> Result { + let Some(tdx_quote) = &self.tdx_quote else { + bail!("tdx_quote not found"); + }; + Quote::parse(&tdx_quote.quote) + } + + fn find_event(&self, name: &str) -> Result { + for event in &self.runtime_events { + if event.event == "system-ready" { + break; + } + if event.event == name { + return Ok(event.clone()); + } + } + Err(anyhow!("event {name} not found")) + } + + /// Replay event logs + pub fn replay_runtime_events(&self, to_event: Option<&str>) -> H::Output { + cc_eventlog::replay_events::(&self.runtime_events, to_event) + } + + fn find_event_payload(&self, event: &str) -> Result> { + self.find_event(event).map(|event| event.payload) + } + + fn find_event_hex_payload(&self, event: &str) -> Result { + self.find_event(event) + .map(|event| hex::encode(&event.payload)) + } + + /// Decode the app-id from the event log + pub fn decode_app_id(&self) -> Result { + self.find_event_hex_payload("app-id") + } + + /// Decode the instance-id from the event log + pub fn decode_instance_id(&self) -> Result { + self.find_event_hex_payload("instance-id") + } + + /// Decode the upgraded app-id from the event log + pub fn decode_compose_hash(&self) -> Result { + self.find_event_hex_payload("compose-hash") + } + + /// Decode the rootfs hash from the event log + pub fn decode_rootfs_hash(&self) -> Result { + self.find_event_hex_payload("rootfs-hash") + } +} + +#[cfg(feature = "quote")] +impl Attestation { + /// Create an attestation for local machine (auto-detect mode) + pub fn local() -> Result { + Self::quote(&[0u8; 64]) + } + + /// Reconstruct from tdx quote and event log, for backward compatibility + pub fn from_tdx_quote(quote: Vec, event_log: &[u8]) -> Result { + let tdx_eventlog: Vec = + serde_json::from_slice(event_log).context("Failed to parse tdx_event_log")?; + let runtime_events = tdx_eventlog + .iter() + .flat_map(|event| event.to_runtime_event()) + .collect(); + let report_data = { + let quote = Quote::parse("e).context("Invalid TDX quote")?; + let report = quote.report.as_td10().context("Invalid TDX report")?; + report.report_data + }; + Ok(Attestation { + mode: AttestationMode::DstackTdx, + tpm_quote: None, + tdx_quote: Some(TdxQuote { + quote, + event_log: tdx_eventlog, + }), + runtime_events, + report_data, + config: "".into(), + report: (), + }) + } + + /// Create an attestation from a report data + pub fn quote(report_data: &[u8; 64]) -> Result { + let mode = AttestationMode::detect()?; + let runtime_events = RuntimeEvent::read_all().context("Failed to read runtime events")?; + let tpm_qualifying_data; + let tdx_quote; + if mode.has_tdx() { + let quote = tdx_attest::get_quote(report_data).context("Failed to get quote")?; + let event_log = + cc_eventlog::tdx::read_event_log().context("Failed to read event log")?; + tpm_qualifying_data = sha256("e); + tdx_quote = Some(TdxQuote { quote, event_log }); + } else { + tpm_qualifying_data = sha256(report_data); + tdx_quote = None; + }; + let tpm_quote = if mode.has_tpm() { + let tpm_ctx = tpm_attest::TpmContext::detect().context("Failed to open TPM context")?; + let quote = tpm_ctx + .create_quote(&tpm_qualifying_data, &tpm_attest::dstack_pcr_policy()) + .context("Failed to create TPM quote")?; + Some(quote) + } else { + None + }; + // TODO: Find a better way handling this hardcode path + let config = + fs_err::read_to_string("/dstack/.host-shared/.sys-config.json").unwrap_or_default(); + Ok(Self { + mode, + tdx_quote, + tpm_quote, + runtime_events, + report_data: *report_data, + config, + report: (), + }) + } +} + +impl Attestation { + /// Wrap into a versioned attestation for encoding + pub fn into_versioned(self) -> VersionedAttestation { + VersionedAttestation::V0 { attestation: self } + } + + /// Verify the quote + pub async fn verify_with_ra_pubkey( + self, + ra_pubkey_der: &[u8], + pccs_url: Option<&str>, + ) -> Result { + let expected_report_data = QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der); + if self.report_data != expected_report_data { + bail!("report data mismatch"); + } + self.verify(pccs_url).await + } + + /// Verify the quote + pub async fn verify(self, pccs_url: Option<&str>) -> Result { + let tpm_report = if self.mode.has_tpm() { + let report = self + .verify_tpm() + .await + .context("Failed to verify TPM quote")?; + let pcr_ind = self + .mode + .tpm_runtime_pcr() + .context("Failed to get runtime PCR no")?; + let replayed_rt_pcr = self.replay_runtime_events::(None); + let quoted_rt_pcr = report + .get_pcr(pcr_ind) + .context("No runtime PCR in TPM report")?; + if replayed_rt_pcr != quoted_rt_pcr[..] { + bail!( + "PCR{pcr_ind} mismatch, quoted: {}, replayed: {}", + hex::encode(quoted_rt_pcr), + hex::encode(replayed_rt_pcr), + ); + } + Some(report) + } else { + None + }; + let tdx_report = if self.mode.has_tdx() { + let report = self.verify_tdx(pccs_url).await?; + let td_report = report.report.as_td10().context("no td report")?; + let replayed_rtmr = self.replay_runtime_events::(None); + if replayed_rtmr != td_report.rt_mr3 { + bail!( + "RTMR3 mismatch, quoted: {}, replayed: {}", + hex::encode(td_report.rt_mr3), + hex::encode(replayed_rtmr) + ); + } + Some(report) + } else { + None + }; + + match self.mode { + AttestationMode::DstackTdx => { + // For bare dstack TDX machine, we only make sure the report data is correct + let Some(tdx_report) = &tdx_report else { + bail!("TDX report is missing in dstack-tdx mode"); + }; + let td_report_data = get_report_data(tdx_report); + if td_report_data != self.report_data[..] { + bail!("tdx report_data mismatch"); + } + } + AttestationMode::GcpTdx => { + let Some(tdx_report) = &tdx_report else { + bail!("TDX report is missing in gcp-tdx mode"); + }; + let Some(td10_report) = tdx_report.report.as_td10() else { + bail!("TD10 report is missing in gcp-tdx mode"); + }; + if td10_report.report_data != self.report_data[..] { + bail!("tdx report_data mismatch"); + } + let tdx_quote = &self.tdx_quote.as_ref().context("TDX quote missing")?.quote; + let Some(tpm_report) = &tpm_report else { + bail!("TPM report is missing in gcp-tdx mode"); + }; + // TPM quote the TDX quote + let qualifying_data = sha256(tdx_quote); + if qualifying_data != tpm_report.attest.qualified_data[..] { + bail!("tpm qualified_data mismatch"); + } + } + AttestationMode::DstackNitro => { + bail!("Nitro not supported"); + } + } + let report = DstackVerifiedReport { + mode: self.mode, + tdx_report, + tpm_report, + }; + if report.is_empty() { + bail!("nothing verified"); + } + Ok(VerifiedAttestation { + mode: self.mode, + tdx_quote: self.tdx_quote, + tpm_quote: self.tpm_quote, + runtime_events: self.runtime_events, + report_data: self.report_data, + config: self.config, + report, + }) + } + + async fn verify_tpm(&self) -> Result { + let tpm_quote = self.tpm_quote.as_ref().context("TPM quote missing")?; + tpm_qvl::get_collateral_and_verify(tpm_quote).await + } + + async fn verify_tdx(&self, pccs_url: Option<&str>) -> Result { + let quote = &self.tdx_quote.as_ref().context("TDX quote missing")?.quote; + let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default()); + if pccs_url.is_empty() { + // try to read from PCCS_URL env var + pccs_url = match std::env::var("PCCS_URL") { + Ok(url) => Cow::Owned(url), + Err(_) => Cow::Borrowed(""), + }; + } + let tdx_report = + dcap_qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref())) + .await + .context("Failed to get collateral")?; + validate_tcb(&tdx_report)?; + Ok(tdx_report) + } +} + +/// Validate the TCB attributes +pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> { + fn validate_td10(report: &TDReport10) -> Result<()> { + let is_debug = report.td_attributes[0] & 0x01 != 0; + if is_debug { + bail!("Debug mode is not allowed"); + } + if report.mr_signer_seam != [0u8; 48] { + bail!("Invalid mr signer seam"); + } + Ok(()) + } + fn validate_td15(report: &TDReport15) -> Result<()> { + if report.mr_service_td != [0u8; 48] { + bail!("Invalid mr service td"); + } + validate_td10(&report.base) + } + fn validate_sgx(report: &EnclaveReport) -> Result<()> { + let is_debug = report.attributes[0] & 0x02 != 0; + if is_debug { + bail!("Debug mode is not allowed"); + } + Ok(()) + } + match &report.report { + Report::TD15(report) => validate_td15(report), + Report::TD10(report) => validate_td10(report), + Report::SgxEnclave(report) => validate_sgx(report), + } +} + +/// Information about the app extracted from event log +#[derive(Debug, Clone, Serialize)] +pub struct AppInfo { + /// App ID + #[serde(with = "hex_bytes")] + pub app_id: Vec, + /// SHA256 of the app compose file + #[serde(with = "hex_bytes")] + pub compose_hash: Vec, + /// ID of the CVM instance + #[serde(with = "hex_bytes")] + pub instance_id: Vec, + /// ID of the device + #[serde(with = "hex_bytes")] + pub device_id: Vec, + /// Measurement of everything except the app info + #[serde(with = "hex_bytes")] + pub mr_system: [u8; 32], + /// Measurement of the entire vm execution environment + #[serde(with = "hex_bytes")] + pub mr_aggregated: [u8; 32], + /// Measurement of the app image + #[serde(with = "hex_bytes")] + pub os_image_hash: Vec, + /// Key provider info + #[serde(with = "hex_bytes")] + pub key_provider_info: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_report_data_with_hash() { + let content_type = QuoteContentType::AppData; + let content = b"test content"; + + let report_data = content_type.to_report_data(content); + assert_eq!(hex::encode(report_data), "7ea0b744ed5e9c0c83ff9f575668e1697652cd349f2027cdf26f918d4c53e8cd50b5ea9b449b4c3d50e20ae00ec29688d5a214e8daff8a10041f5d624dae8a01"); + + // Test SHA-256 + let result = content_type + .to_report_data_with_hash(content, "sha256") + .unwrap(); + assert_eq!(result[32..], [0u8; 32]); // Check padding + assert_ne!(result[..32], [0u8; 32]); // Check hash is non-zero + + // Test SHA-384 + let result = content_type + .to_report_data_with_hash(content, "sha384") + .unwrap(); + assert_eq!(result[48..], [0u8; 16]); // Check padding + assert_ne!(result[..48], [0u8; 48]); // Check hash is non-zero + + // Test default + let result = content_type.to_report_data_with_hash(content, "").unwrap(); + assert_ne!(result, [0u8; 64]); // Should fill entire buffer + + // Test raw content + let exact_content = [42u8; 64]; + let result = content_type + .to_report_data_with_hash(&exact_content, "raw") + .unwrap(); + assert_eq!(result, exact_content); + + // Test invalid raw content length + let invalid_content = [42u8; 65]; + assert!(content_type + .to_report_data_with_hash(&invalid_content, "raw") + .is_err()); + + // Test invalid hash algorithm + assert!(content_type + .to_report_data_with_hash(content, "invalid") + .is_err()); + } +} diff --git a/dstack-attest/src/lib.rs b/dstack-attest/src/lib.rs new file mode 100644 index 00000000..b8577d00 --- /dev/null +++ b/dstack-attest/src/lib.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context; +use cc_eventlog::RuntimeEvent; + +pub use cc_eventlog as ccel; +pub use tdx_attest as tdx; +pub use tpm_attest as tpm; + +use crate::attestation::AttestationMode; + +pub mod attestation; + +/// Emit a runtime event that extends RTMR3 and logs the event. +pub fn emit_runtime_event(event: &str, payload: &[u8]) -> anyhow::Result<()> { + let event = RuntimeEvent::new(event.to_string(), payload.to_vec()); + + let mode = AttestationMode::detect()?; + + event.emit().context("Failed to emit runtime event")?; + + if mode.has_tdx() { + let digest = event.sha384_digest(); + let event_type = event.cc_event_type(); + tdx_attest::extend_rtmr(3, event_type, digest).context("Failed to extend TDX RTMR")?; + } + if let Some(pcr) = mode.tpm_runtime_pcr() { + let digest = event.sha256_digest(); + let tpm = tpm_attest::TpmContext::detect().context("Failed to detect TPM device")?; + tpm.pcr_extend_sha256(pcr, &digest) + .context("Failed to extend TPM RTMR")?; + } + Ok(()) +} diff --git a/dstack-mr/src/acpi.rs b/dstack-mr/src/acpi.rs index 5976c10f..a39759e8 100644 --- a/dstack-mr/src/acpi.rs +++ b/dstack-mr/src/acpi.rs @@ -63,12 +63,33 @@ impl Machine<'_> { "tdx-guest,id=tdx", "-device", "vhost-vsock-pci,guest-cid=3", - "-virtfs", - &format!( - "local,path={shared_dir},mount_tag=host-shared,readonly=on,security_model=none,id=virtfs0", - ), ]); + // Configure shared files delivery: either via disk or 9p + match self.host_share_mode.as_str() { + "" | "9p" => { + // Use 9p virtfs (default) + cmd.args([ + "-virtfs", + &format!( + "local,path={shared_dir},mount_tag=host-shared,readonly=on,security_model=none,id=virtfs0", + ), + ]); + } + "vvfat" | "vhd" => { + // Use a second virtual disk (hd2) to share files + cmd.args([ + "-drive", + &format!("file={dummy_disk},if=none,id=hd2,format=raw,readonly=on"), + "-device", + "virtio-blk-pci,drive=hd2", + ]); + } + _ => { + bail!("Invalid shared disk mode: {}", self.host_share_mode); + } + } + if self.root_verity { cmd.args([ "-drive", diff --git a/dstack-mr/src/machine.rs b/dstack-mr/src/machine.rs index c08e6cdf..b664d2c4 100644 --- a/dstack-mr/src/machine.rs +++ b/dstack-mr/src/machine.rs @@ -30,6 +30,8 @@ pub struct Machine<'a> { pub num_nvswitches: u32, pub hotplug_off: bool, pub root_verity: bool, + #[builder(default)] + pub host_share_mode: String, } fn parse_version_tuple(v: &str) -> Result<(u32, u32, u32)> { diff --git a/dstack-types/Cargo.toml b/dstack-types/Cargo.toml index d0b0ae64..997b0e43 100644 --- a/dstack-types/Cargo.toml +++ b/dstack-types/Cargo.toml @@ -10,6 +10,7 @@ edition.workspace = true license.workspace = true [dependencies] +scale = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] } serde-human-bytes.workspace = true sha3.workspace = true diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index 1188a1f0..a2dc1047 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 +use scale::{Decode, Encode}; use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; use size_parser::human_size; @@ -69,6 +70,7 @@ pub enum KeyProviderKind { None, Kms, Local, + Tpm, } impl KeyProviderKind { @@ -79,6 +81,10 @@ impl KeyProviderKind { pub fn is_kms(&self) -> bool { matches!(self, KeyProviderKind::Kms) } + + pub fn is_tpm(&self) -> bool { + matches!(self, KeyProviderKind::Tpm) + } } #[derive(Deserialize, Serialize, Debug, Default, Clone)] @@ -101,7 +107,7 @@ impl AppCompose { } pub fn kms_enabled(&self) -> bool { - self.kms_enabled || self.feature_enabled("kms") + self.key_provider().is_kms() } pub fn key_provider(&self) -> KeyProviderKind { @@ -128,7 +134,7 @@ pub struct SysConfig { pub gateway_urls: Vec, pub pccs_url: Option, pub docker_registry: Option, - pub host_api_url: String, + pub host_api_url: Option, // JSON serialized VmConfig pub vm_config: String, } @@ -138,7 +144,9 @@ pub struct VmConfig { pub spec_version: u32, #[serde(with = "hex_bytes")] pub os_image_hash: Vec, + #[serde(default)] pub cpu_count: u32, + #[serde(default)] pub memory_size: u64, // https://github.com/intel-staging/qemu-tdx/issues/1 #[serde(default, skip_serializing_if = "Option::is_none")] @@ -159,6 +167,10 @@ pub struct VmConfig { pub hotplug_off: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub image: Option, + /// If true, shared files are provided via a second virtual disk (hd2) + /// If false (default), shared files are provided via 9p virtfs + #[serde(default)] + pub host_share_mode: String, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -186,6 +198,11 @@ pub enum KeyProvider { #[serde(with = "hex_bytes")] mr: Vec, }, + Tpm { + key: String, + #[serde(with = "hex_bytes")] + pubkey: Vec, + }, Kms { url: String, #[serde(with = "hex_bytes")] @@ -200,6 +217,7 @@ impl KeyProvider { match self { KeyProvider::None { .. } => KeyProviderKind::None, KeyProvider::Local { .. } => KeyProviderKind::Local, + KeyProvider::Tpm { .. } => KeyProviderKind::Tpm, KeyProvider::Kms { .. } => KeyProviderKind::Kms, } } @@ -208,6 +226,7 @@ impl KeyProvider { match self { KeyProvider::None { .. } => &[], KeyProvider::Local { mr, .. } => mr, + KeyProvider::Tpm { pubkey, .. } => pubkey, KeyProvider::Kms { pubkey, .. } => pubkey, } } @@ -244,3 +263,40 @@ pub fn dstack_agent_address() -> String { } "unix:/var/run/dstack.sock".into() } + +/// Hardware/Cloud Platform +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] +#[serde(rename_all = "lowercase")] +pub enum Platform { + /// dstack bare platform + Dstack, + /// Google Cloud Platform + Gcp, +} + +impl Platform { + /// Detect platform from system DMI information + pub fn detect() -> Option { + if let Ok(board_name) = std::fs::read_to_string("/sys/class/dmi/id/product_name") { + match board_name.trim() { + "dstack" | "qemu" => return Some(Self::Dstack), + "Google Compute Engine" => return Some(Self::Gcp), + _ => {} + } + } + None + } + + /// Detect platform from system DMI information, default to Dstack if cannot detect + pub fn detect_or_dstack() -> Self { + Self::detect().unwrap_or(Self::Dstack) + } + + /// Get platform name as string + pub fn as_str(&self) -> &'static str { + match self { + Self::Dstack => "dstack", + Self::Gcp => "gcp", + } + } +} diff --git a/dstack-types/src/mr_config.rs b/dstack-types/src/mr_config.rs index 6546a552..b4766ecb 100644 --- a/dstack-types/src/mr_config.rs +++ b/dstack-types/src/mr_config.rs @@ -37,6 +37,7 @@ impl MrConfig<'_> { KeyProviderKind::None => 0_u8, KeyProviderKind::Local => 1, KeyProviderKind::Kms => 2, + KeyProviderKind::Tpm => 3, }; let mut hasher = Keccak256::new(); hasher.update(compose_hash); diff --git a/dstack-types/src/shared_filenames.rs b/dstack-types/src/shared_filenames.rs index 2588ae78..5c3ef828 100644 --- a/dstack-types/src/shared_filenames.rs +++ b/dstack-types/src/shared_filenames.rs @@ -12,6 +12,7 @@ pub const DECRYPTED_ENV_JSON: &str = ".decrypted-env.json"; pub const INSTANCE_INFO: &str = ".instance_info"; pub const HOST_SHARED_DIR: &str = "/dstack/.host-shared"; pub const HOST_SHARED_DIR_NAME: &str = ".host-shared"; +pub const HOST_SHARED_DISK_LABEL: &str = "DSTACKSHR"; pub mod compat_v3 { pub const SYS_CONFIG: &str = "config.json"; diff --git a/dstack-util/Cargo.toml b/dstack-util/Cargo.toml index f0266487..71bf295f 100644 --- a/dstack-util/Cargo.toml +++ b/dstack-util/Cargo.toml @@ -32,9 +32,11 @@ x25519-dalek.workspace = true dstack-kms-rpc.workspace = true ra-rpc = { workspace = true, features = ["client"] } -ra-tls.workspace = true +ra-tls = { workspace = true, features = ["quote"] } dstack-gateway-rpc.workspace = true tdx-attest.workspace = true +tpm-attest.workspace = true +tpm-qvl = { workspace = true, features = ["crl-download"] } host-api = { workspace = true, features = ["client"] } cmd_lib.workspace = true toml.workspace = true @@ -43,14 +45,18 @@ k256 = { workspace = true, features = ["ecdsa"] } dstack-types.workspace = true rand.workspace = true sha3.workspace = true +dstack-attest.workspace = true cert-client.workspace = true x509-parser.workspace = true -serde_yaml2.workspace = true +yaml-rust2.workspace = true bollard.workspace = true sodiumbox.workspace = true libc.workspace = true luks2.workspace = true scopeguard.workspace = true +tempfile.workspace = true +ez-hash.workspace = true +cc-eventlog.workspace = true [dev-dependencies] rand.workspace = true diff --git a/dstack-util/src/docker_compose.rs b/dstack-util/src/docker_compose.rs new file mode 100644 index 00000000..b5f02eb7 --- /dev/null +++ b/dstack-util/src/docker_compose.rs @@ -0,0 +1,413 @@ +// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, Result}; +use bollard::container::{ListContainersOptions, RemoveContainerOptions}; +use bollard::Docker; +use fs_err as fs; +use serde::Deserialize; +use std::collections::HashMap; +use std::path::Path; +use yaml_rust2::{Yaml, YamlLoader}; + +/// Holds parsed information from a docker-compose file +#[derive(Debug)] +pub struct ComposeInfo { + pub project_name: String, + pub service_names: std::collections::HashSet, +} + +/// Parse a docker-compose file and extract project name and service names +pub fn parse_docker_compose_file(compose_file: impl AsRef) -> Result { + let compose_content = + fs::read_to_string(compose_file.as_ref()).context("failed to read docker-compose file")?; + + let yaml_docs = YamlLoader::load_from_str(&compose_content).context("failed to parse YAML")?; + let yaml_doc = yaml_docs.first().context("empty YAML document")?; + + // Extract project name + let project_name = if let Some(name) = yaml_doc["name"].as_str() { + name.to_string() + } else { + get_project_name(compose_file.as_ref())? + }; + + // Extract service names + let services = match &yaml_doc["services"] { + Yaml::Hash(m) => m, + _ => anyhow::bail!("missing or invalid 'services' field"), + }; + + let service_names = services + .keys() + .filter_map(|k| k.as_str().map(|s| s.to_string())) + .collect(); + + Ok(ComposeInfo { + project_name, + service_names, + }) +} + +fn get_project_name(compose_file: impl AsRef) -> Result { + let project_name = fs::canonicalize(compose_file) + .context("failed to canonicalize compose file")? + .parent() + .context("failed to get parent directory of compose file")? + .file_name() + .context("failed to get file name of compose file")? + .to_string_lossy() + .into_owned(); + Ok(project_name) +} + +/// Remove orphaned containers using Docker daemon API +pub async fn remove_orphans(compose_file: impl AsRef, dry_run: bool) -> Result<()> { + // Connect to Docker daemon + let docker = + Docker::connect_with_local_defaults().context("Failed to connect to Docker daemon")?; + + // Parse compose file to extract project name and service names + let compose_info = parse_docker_compose_file(&compose_file)?; + let project_name = compose_info.project_name; + let service_names = compose_info.service_names; + + // List all containers + let options = ListContainersOptions:: { + all: true, + ..Default::default() + }; + + let containers = docker + .list_containers(Some(options)) + .await + .context("Failed to list containers")?; + + // Find and remove orphaned containers + for container in containers { + let Some(labels) = container.labels else { + continue; + }; + + // Check if container belongs to current project + let Some(container_project) = labels.get("com.docker.compose.project") else { + continue; + }; + + if container_project != &project_name { + continue; + } + // Check if service still exists in compose file + let Some(service_name) = labels.get("com.docker.compose.service") else { + continue; + }; + if service_names.contains(service_name) { + continue; + } + // Service no longer exists in compose file, remove the container + let Some(container_id) = container.id else { + continue; + }; + + if dry_run { + println!("would remove orphaned container {service_name} {container_id}"); + } else { + println!("removing orphaned container {service_name} {container_id}"); + docker + .remove_container( + &container_id, + Some(RemoveContainerOptions { + v: true, + force: true, + ..Default::default() + }), + ) + .await + .with_context(|| format!("Failed to remove container {}", container_id))?; + } + } + + Ok(()) +} + +/// Docker container config.v2.json structure +#[derive(Deserialize)] +struct ContainerConfig { + #[serde(rename = "Config")] + config: Option, +} + +#[derive(Deserialize)] +struct ContainerConfigInner { + #[serde(rename = "Labels")] + labels: Option>, +} + +/// Remove orphaned containers without requiring Docker daemon (offline mode) +/// +/// This function directly reads Docker's data directory to find and remove +/// orphaned containers. It should be run BEFORE dockerd starts to prevent +/// orphaned containers from starting. +pub fn remove_orphans_direct( + compose_file: impl AsRef, + docker_root: impl AsRef, + dry_run: bool, +) -> Result<()> { + // Parse compose file to extract project name and service names + let compose_info = parse_docker_compose_file(&compose_file)?; + let project_name = &compose_info.project_name; + let service_names = &compose_info.service_names; + + let containers_dir = docker_root.as_ref().join("containers"); + if !containers_dir.exists() { + return Ok(()); + } + + // Iterate through all container directories + let entries = fs::read_dir(&containers_dir).with_context(|| { + format!( + "Failed to read containers directory: {}", + containers_dir.display() + ) + })?; + + for entry in entries { + let entry = entry.context("Failed to read directory entry")?; + let container_dir = entry.path(); + + if !container_dir.is_dir() { + continue; + } + + let container_id = container_dir + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("") + .to_string(); + + // Read config.v2.json + let config_path = container_dir.join("config.v2.json"); + if !config_path.exists() { + continue; + } + + let config_content = match fs::read_to_string(&config_path) { + Ok(content) => content, + Err(e) => { + eprintln!("Warning: Failed to read {}: {}", config_path.display(), e); + continue; + } + }; + + let config: ContainerConfig = match serde_json::from_str(&config_content) { + Ok(config) => config, + Err(e) => { + eprintln!("Warning: Failed to parse {}: {}", config_path.display(), e); + continue; + } + }; + + let Some(inner_config) = config.config else { + continue; + }; + + let Some(labels) = inner_config.labels else { + continue; + }; + + // Check if container belongs to current project + let Some(container_project) = labels.get("com.docker.compose.project") else { + continue; + }; + + if container_project != project_name { + continue; + } + + // Check if service still exists in compose file + let Some(service_name) = labels.get("com.docker.compose.service") else { + continue; + }; + + if service_names.contains(service_name) { + continue; + } + + // Service no longer exists in compose file, remove the container directory + let short_id = &container_id[..12.min(container_id.len())]; + + if dry_run { + println!("would remove orphaned container {service_name} {short_id}"); + } else { + println!("removing orphaned container {service_name} {short_id}"); + fs::remove_dir_all(&container_dir).with_context(|| { + format!( + "Failed to remove container directory: {}", + container_dir.display() + ) + })?; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_yaml_anchor_parsing() { + // Test that yaml-rust2 can parse YAML anchors and aliases + let yaml_with_anchors = r#" +name: test-project +services: + common: &common-config + image: ubuntu:latest + restart: unless-stopped + + service1: + <<: *common-config + container_name: service1 + + service2: + <<: *common-config + container_name: service2 + + service3: + image: nginx:latest +"#; + + let yaml_docs = YamlLoader::load_from_str(yaml_with_anchors).unwrap(); + let yaml_doc = yaml_docs.first().unwrap(); + + // Extract project name + let project_name = yaml_doc["name"].as_str().unwrap(); + assert_eq!(project_name, "test-project"); + + // Extract service names + let services = match &yaml_doc["services"] { + Yaml::Hash(m) => m, + _ => panic!("services should be a hash"), + }; + + let service_names: std::collections::HashSet = services + .keys() + .filter_map(|k| k.as_str().map(|s| s.to_string())) + .collect(); + + // Verify all services are parsed including the anchor definition + assert_eq!(service_names.len(), 4); + assert!(service_names.contains("common")); + assert!(service_names.contains("service1")); + assert!(service_names.contains("service2")); + assert!(service_names.contains("service3")); + + // Verify that anchors are resolved + // Note: yaml-rust2 parses anchors but doesn't auto-expand merge keys + // The merge key "<<" will contain the referenced hash + let service1 = &yaml_doc["services"]["service1"]; + assert_eq!(service1["container_name"].as_str().unwrap(), "service1"); + + // Verify the merge key contains the anchor content + if let Yaml::Hash(merge_content) = &service1["<<"] { + assert_eq!( + merge_content[&Yaml::String("image".to_string())] + .as_str() + .unwrap(), + "ubuntu:latest" + ); + assert_eq!( + merge_content[&Yaml::String("restart".to_string())] + .as_str() + .unwrap(), + "unless-stopped" + ); + } else { + panic!("merge key should contain hash"); + } + } + + #[test] + fn test_yaml_simple_anchor_alias() { + // Test simple anchor and alias without merge keys + let yaml_simple_anchor = r#" +defaults: &defaults + timeout: 30 + retries: 3 + +service1: + name: web + config: *defaults + +service2: + name: api + config: *defaults +"#; + + let yaml_docs = YamlLoader::load_from_str(yaml_simple_anchor).unwrap(); + let yaml_doc = yaml_docs.first().unwrap(); + + // Verify alias points to the same content + let service1_config = &yaml_doc["service1"]["config"]; + let service2_config = &yaml_doc["service2"]["config"]; + + assert_eq!(service1_config["timeout"].as_i64().unwrap(), 30); + assert_eq!(service1_config["retries"].as_i64().unwrap(), 3); + assert_eq!(service2_config["timeout"].as_i64().unwrap(), 30); + assert_eq!(service2_config["retries"].as_i64().unwrap(), 3); + } + + #[test] + fn test_yaml_without_anchors() { + let yaml_simple = r#" +services: + web: + image: nginx:latest + db: + image: postgres:14 +"#; + + let yaml_docs = YamlLoader::load_from_str(yaml_simple).unwrap(); + let yaml_doc = yaml_docs.first().unwrap(); + + let services = match &yaml_doc["services"] { + Yaml::Hash(m) => m, + _ => panic!("services should be a hash"), + }; + + let service_names: std::collections::HashSet = services + .keys() + .filter_map(|k| k.as_str().map(|s| s.to_string())) + .collect(); + + assert_eq!(service_names.len(), 2); + assert!(service_names.contains("web")); + assert!(service_names.contains("db")); + } + + #[test] + fn test_parse_real_compose_file() { + // Test with real docker-compose.yaml from key-provider-build + let compose_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/key-provider-docker-compose.yaml" + ); + + let compose_info = parse_docker_compose_file(compose_path).unwrap(); + + // Verify service names are correctly extracted + assert_eq!(compose_info.service_names.len(), 2); + assert!(compose_info.service_names.contains("aesmd")); + assert!(compose_info + .service_names + .contains("gramine-sealing-key-provider")); + + // Note: x-common is an anchor definition, not a service, so it should not be in service_names + assert!(!compose_info.service_names.contains("x-common")); + + // Project name should be "fixtures" (the parent directory name) + assert_eq!(compose_info.project_name, "fixtures"); + } +} diff --git a/dstack-util/src/host_api.rs b/dstack-util/src/host_api.rs index cc40581a..95eb59d7 100644 --- a/dstack-util/src/host_api.rs +++ b/dstack-util/src/host_api.rs @@ -4,7 +4,10 @@ use crate::utils::{deserialize_json_file, sha256, SysConfig}; use anyhow::{anyhow, bail, Context, Result}; -use dstack_types::shared_filenames::{HOST_SHARED_DIR, SYS_CONFIG}; +use dstack_types::{ + shared_filenames::{HOST_SHARED_DIR, SYS_CONFIG}, + Platform, +}; use host_api::{ client::{new_client, DefaultClient}, Notification, @@ -19,41 +22,48 @@ pub(crate) struct KeyProvision { } pub(crate) struct HostApi { - client: DefaultClient, + client: Option, pccs_url: Option, } impl Default for HostApi { fn default() -> Self { - Self::new("".into(), None) + Self::new(None, None) } } impl HostApi { - pub fn new(base_url: String, pccs_url: Option) -> Self { + pub fn new(base_url: Option, pccs_url: Option) -> Self { Self { - client: new_client(base_url), + client: base_url.map(new_client), pccs_url, } } pub fn load_or_default(url: Option) -> Result { let api = match url { - Some(url) => Self::new(url, None), + Some(url) => Self::new(Some(url), None), None => { let local_config: SysConfig = deserialize_json_file(format!("{HOST_SHARED_DIR}/{SYS_CONFIG}"))?; - Self::new( - local_config.host_api_url.clone(), - local_config.pccs_url.clone(), - ) + Self::new(local_config.host_api_url, local_config.pccs_url) } }; Ok(api) } pub async fn notify(&self, event: &str, payload: &str) -> Result<()> { - self.client + match Platform::detect_or_dstack() { + Platform::Dstack => {} + Platform::Gcp => { + // Skip notify on GCP as no host dstack-vmm there. + return Ok(()); + } + } + let Some(client) = &self.client else { + return Ok(()); + }; + client .notify(Notification { event: event.to_string(), payload: payload.to_string(), @@ -72,11 +82,11 @@ impl HostApi { let (pk, sk) = generate_keypair(); let mut report_data = [0u8; 64]; report_data[..PUBLICKEYBYTES].copy_from_slice(pk.as_bytes()); - let (_, quote) = - tdx_attest::get_quote(&report_data, None).context("Failed to get quote")?; - - let provision = self - .client + let quote = tdx_attest::get_quote(&report_data).context("Failed to get quote")?; + let Some(client) = &self.client else { + return Err(anyhow!("Host API client not initialized")); + }; + let provision = client .get_sealing_key(host_api::GetSealingKeyRequest { quote: quote.to_vec(), }) diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index f5f2f730..40840ecd 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -3,23 +3,22 @@ // SPDX-License-Identifier: Apache-2.0 use anyhow::{Context, Result}; -use bollard::container::{ListContainersOptions, RemoveContainerOptions}; -use bollard::Docker; use clap::{Parser, Subcommand}; -use dstack_types::KeyProvider; +use dstack_attest::emit_runtime_event; +use dstack_types::{KeyProvider, KeyProviderKind}; use fs_err as fs; use getrandom::fill as getrandom; use host_api::HostApi; use k256::schnorr::SigningKey; +use ra_rpc::Attestation; use ra_tls::{ - attestation::QuoteContentType, + attestation::{QuoteContentType, VersionedAttestation}, cert::generate_ra_cert, kdf::{derive_ecdsa_key, derive_ecdsa_key_pair_from_bytes}, rcgen::KeyPair, }; -use scale::Decode; -use serde::Deserialize; -use std::{collections::HashMap, path::Path}; +use scale::Encode; +use std::path::Path; use std::{ io::{self, Read, Write}, path::PathBuf, @@ -29,6 +28,7 @@ use tdx_attest as att; use utils::AppKeys; mod crypto; +mod docker_compose; mod host_api; mod parse_env_file; mod system_setup; @@ -44,14 +44,16 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Get TDX report given report data from stdin - Report, /// Generate a TDX quote given report data from stdin Quote, + /// Get TDX event logs + Eventlog, /// Extend RTMRs Extend(ExtendArgs), /// Show the current RTMR state Show, + /// Replay event log and show calculated IMR/RTMR values + ReplayImr, /// Hex encode data Hex(HexCommand), /// Generate a RA-TLS certificate @@ -70,6 +72,21 @@ enum Commands { NotifyHost(HostNotifyArgs), /// Remove orphaned containers RemoveOrphans(RemoveOrphansArgs), + /// Perform vTPM attestation (for GCP TEE instances) + VtpmAttest(VtpmAttestArgs), + /// Generate a TPM quote + TpmQuote(TpmQuoteArgs), + /// Verify a TPM quote + TpmVerify(TpmVerifyArgs), + QuoteReport(QuoteReportArgs), + /// Generate a versioned attestation for simulator use + Attest(AttestArgs), + /// Show size breakdown for a versioned attestation file + AttestInfo(AttestInfoArgs), + /// Dump a versioned attestation as JSON + AttestJson(AttestJsonArgs), + /// Strip attestation for certificate embedding + AttestStrip(AttestStripArgs), } #[derive(Parser)] @@ -185,44 +202,321 @@ struct RemoveOrphansArgs { /// path to the docker-compose.yaml file #[arg(short = 'f', long)] compose: String, + + /// show what would be removed without actually removing + #[arg(short = 'n', long)] + dry_run: bool, + + /// Offline mode: operate without Docker daemon by directly reading Docker data directory + #[arg(long)] + no_dockerd: bool, + + /// Docker data root directory for offline mode (default: /var/lib/docker) + #[arg(short = 'd', long, default_value = "/var/lib/docker")] + docker_root: String, +} + +#[derive(Parser)] +/// Perform vTPM attestation +struct VtpmAttestArgs { + /// path to Root CA certificate (PEM format) + #[arg(long)] + root_ca: PathBuf, + + /// nonce for replay protection + #[arg(long)] + nonce: String, + + /// expected OS image SHA256 hash (optional) + #[arg(long)] + expected_os_hash: Option, + + /// key algorithm (rsa or ecc, default: rsa) + #[arg(long, default_value = "rsa")] + key_algo: String, + + /// output format (json or text, default: text) + #[arg(long, default_value = "text")] + format: String, +} + +#[derive(Parser)] +/// Generate a TPM quote +struct TpmQuoteArgs { + /// qualifying data (hex encoded, default: 32 zeros) + #[arg(short, long)] + data: Option, + + /// output file (default: stdout) + #[arg(short, long)] + output: Option, + + /// key algorithm (auto, ecc, or rsa; default: auto) + #[arg(short = 'k', long, default_value = "auto")] + key_algo: String, + + /// The hash algorithm to use (default: none) + #[arg(short = 'H', long, default_value = "none")] + hash_algo: String, +} + +#[derive(Parser)] +/// Verify a TPM quote +struct TpmVerifyArgs { + /// path to Root CA certificate (PEM format) + #[arg(long)] + root_ca: PathBuf, + + /// path to TPM quote JSON file + #[arg(short, long)] + quote: PathBuf, +} + +#[derive(Parser)] +struct QuoteReportArgs { + #[arg(long)] + report_data: Option, + + #[arg(long, default_value = "/dstack/.host-shared/.sys-config.json")] + sys_config: PathBuf, + + #[arg(short, long)] + output: Option, + + #[arg(long, default_value_t = false)] + debug: bool, +} + +#[derive(Parser)] +struct AttestArgs { + /// report data in hex (max 64 bytes) + #[arg(long)] + report_data: Option, + + /// output file (default: attestation.bin) + #[arg(short, long)] + output: Option, + + /// hex encode output + #[arg(long, default_value_t = false)] + hex: bool, +} + +#[derive(Parser)] +struct AttestInfoArgs { + /// input file (default: attestation.bin) + #[arg(short, long)] + input: Option, +} + +#[derive(Parser)] +struct AttestJsonArgs { + /// input file (default: attestation.bin) + #[arg(short, long)] + input: Option, + + /// output file (default: stdout) + #[arg(short, long)] + output: Option, +} + +#[derive(Parser)] +struct AttestStripArgs { + /// input file (default: attestation.bin) + #[arg(short, long)] + input: Option, + + /// output file (default: attestation.strip.bin) + #[arg(short, long)] + output: Option, +} + +fn pad64(data: &[u8]) -> Result<[u8; 64]> { + if data.len() > 64 { + anyhow::bail!("report_data must be at most 64 bytes"); + } + let mut out = [0u8; 64]; + out[..data.len()].copy_from_slice(data); + Ok(out) +} + +fn cmd_quote_report(args: QuoteReportArgs) -> Result<()> { + #[derive(serde::Serialize)] + struct VerificationRequestJson { + pub attestation: String, + } + + let report_data = match args.report_data { + Some(hex_data) => { + pad64(&hex::decode(hex_data).context("Failed to decode report_data hex")?)? + } + None => [0u8; 64], + }; + let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; + let request = VerificationRequestJson { + attestation: hex::encode(attestation.into_versioned().to_scale()), + }; + + let json = + serde_json::to_string_pretty(&request).context("Failed to serialize request JSON")?; + if let Some(output_path) = args.output { + fs::write(&output_path, json).context("Failed to write quote report")?; + } else { + println!("{json}"); + } + Ok(()) } -#[derive(Debug, Deserialize)] -struct ComposeConfig { - name: Option, - services: HashMap, +fn cmd_attest(args: AttestArgs) -> Result<()> { + let report_data = match args.report_data { + Some(hex_data) => { + pad64(&hex::decode(hex_data).context("Failed to decode report_data hex")?)? + } + None => [0u8; 64], + }; + let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; + let attestation = attestation.into_versioned().to_scale(); + + if args.hex { + let encoded = hex::encode(attestation); + if let Some(output) = args.output { + fs::write(&output, encoded).context("Failed to write attestation hex")?; + } else { + println!("{encoded}"); + } + return Ok(()); + } + + let output = args + .output + .unwrap_or_else(|| PathBuf::from("attestation.bin")); + fs::write(&output, &attestation).context("Failed to write attestation sample")?; + Ok(()) } -#[derive(Debug, Deserialize)] -struct ComposeService {} +fn cmd_attest_info(args: AttestInfoArgs) -> Result<()> { + let input = args + .input + .unwrap_or_else(|| PathBuf::from("attestation.bin")); + let data = fs::read(&input).context("Failed to read attestation file")?; + let attestation = + VersionedAttestation::from_scale(&data).context("Failed to decode attestation")?; + + println!("file: {}", input.display()); + println!("total_bytes: {}", data.len()); + + match attestation { + VersionedAttestation::V0 { attestation } => { + println!("version: V0"); + println!("mode: {:?}", attestation.mode); + println!("config_bytes: {}", attestation.config.len()); + match attestation.tdx_quote { + Some(tdx) => { + let event_log_json = serde_json::to_vec(&tdx.event_log) + .context("Failed to serialize event log")?; + println!("tdx_quote_bytes: {}", tdx.quote.len()); + println!("event_log_entries: {}", tdx.event_log.len()); + println!("event_log_json_bytes: {}", event_log_json.len()); + } + None => { + println!("tdx_quote_bytes: 0"); + println!("event_log_entries: 0"); + println!("event_log_json_bytes: 0"); + } + } + match attestation.tpm_quote { + Some(tpm) => { + let tpm_bytes = tpm.encode(); + println!("tpm_quote_bytes: {}", tpm_bytes.len()); + } + None => println!("tpm_quote_bytes: 0"), + } + } + } + + Ok(()) +} + +fn cmd_attest_json(args: AttestJsonArgs) -> Result<()> { + let input = args + .input + .unwrap_or_else(|| PathBuf::from("attestation.bin")); + let data = fs::read(&input).context("Failed to read attestation file")?; + let attestation = + VersionedAttestation::from_scale(&data).context("Failed to decode attestation")?; + + let json = match attestation { + VersionedAttestation::V0 { attestation } => { + let mode = serde_json::to_value(attestation.mode) + .context("Failed to serialize attestation mode")?; + let tdx_quote = match attestation.tdx_quote { + Some(tdx) => serde_json::json!({ + "quote": hex::encode(tdx.quote), + "event_log": tdx.event_log, + }), + None => serde_json::Value::Null, + }; + let tpm_quote = match attestation.tpm_quote { + Some(tpm) => serde_json::to_value(tpm).context("Failed to serialize TPM quote")?, + None => serde_json::Value::Null, + }; + + serde_json::json!({ + "version": "V0", + "mode": mode, + "config": attestation.config, + "tdx_quote": tdx_quote, + "tpm_quote": tpm_quote, + }) + } + }; + + let output = serde_json::to_string_pretty(&json).context("Failed to serialize JSON")?; + if let Some(path) = args.output { + fs::write(&path, output).context("Failed to write JSON output")?; + } else { + println!("{output}"); + } + Ok(()) +} + +fn cmd_attest_strip(args: AttestStripArgs) -> Result<()> { + let input = args + .input + .unwrap_or_else(|| PathBuf::from("attestation.bin")); + let data = fs::read(&input).context("Failed to read attestation file")?; + let attestation = + VersionedAttestation::from_scale(&data).context("Failed to decode attestation")?; + let stripped = attestation.into_stripped(); + let output = args + .output + .unwrap_or_else(|| PathBuf::from("attestation.strip.bin")); + fs::write(&output, stripped.to_scale()).context("Failed to write stripped attestation")?; + Ok(()) +} fn cmd_quote() -> Result<()> { let mut report_data = [0; 64]; io::stdin() .read_exact(&mut report_data) .context("Failed to read report data")?; - let (_key_id, quote) = att::get_quote(&report_data, None).context("Failed to get quote")?; + let quote = att::get_quote(&report_data).context("Failed to get quote")?; io::stdout() .write_all("e) .context("Failed to write quote")?; Ok(()) } -fn cmd_extend(extend_args: ExtendArgs) -> Result<()> { - let payload = hex::decode(&extend_args.payload).context("Failed to decode payload")?; - att::extend_rtmr3(&extend_args.event, &payload).context("Failed to extend RTMR") +fn cmd_eventlog() -> Result<()> { + let event_logs = cc_eventlog::tdx::read_event_log().context("Failed to read event logs")?; + serde_json::to_writer_pretty(io::stdout(), &event_logs) + .context("Failed to write event logs")?; + Ok(()) } -fn cmd_report() -> Result<()> { - let mut report_data = [0; 64]; - io::stdin() - .read_exact(&mut report_data) - .context("Failed to read report data")?; - let report = att::get_report(&report_data).context("Failed to get report")?; - io::stdout() - .write_all(&report.0) - .context("Failed to write report")?; - Ok(()) +fn cmd_extend(extend_args: ExtendArgs) -> Result<()> { + let payload = hex::decode(&extend_args.payload).context("Failed to decode payload")?; + emit_runtime_event(&extend_args.event, &payload).context("Failed to extend RTMR") } fn cmd_rand(rand_args: RandArgs) -> Result<()> { @@ -237,41 +531,6 @@ fn cmd_rand(rand_args: RandArgs) -> Result<()> { Ok(()) } -#[derive(Decode)] -struct ParsedReport { - attributes: [u8; 8], - xfam: [u8; 8], - mrtd: [u8; 48], - mrconfigid: [u8; 48], - mrowner: [u8; 48], - mrownerconfig: [u8; 48], - rtmr0: [u8; 48], - rtmr1: [u8; 48], - rtmr2: [u8; 48], - rtmr3: [u8; 48], - servtd_hash: [u8; 48], -} - -impl core::fmt::Debug for ParsedReport { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - use hex_fmt::HexFmt as HF; - - f.debug_struct("ParsedReport") - .field("attributes", &HF(&self.attributes)) - .field("xfam", &HF(&self.xfam)) - .field("mrtd", &HF(&self.mrtd)) - .field("mrconfigid", &HF(&self.mrconfigid)) - .field("mrowner", &HF(&self.mrowner)) - .field("mrownerconfig", &HF(&self.mrownerconfig)) - .field("rtmr0", &HF(&self.rtmr0)) - .field("rtmr1", &HF(&self.rtmr1)) - .field("rtmr2", &HF(&self.rtmr2)) - .field("rtmr3", &HF(&self.rtmr3)) - .field("servtd_hash", &HF(&self.servtd_hash)) - .finish() - } -} - fn cmd_show_mrs() -> Result<()> { let attestation = ra_tls::attestation::Attestation::local().context("Failed to get attestation")?; @@ -283,6 +542,57 @@ fn cmd_show_mrs() -> Result<()> { Ok(()) } +fn cmd_replay_imr() -> Result<()> { + use sha2::Digest; + + println!("=== Event Log Replay: Calculated IMR/RTMR Values ===\n"); + + // Read and replay event logs + let event_logs = att::eventlog::tdx::read_event_log().context("Failed to read event logs")?; + + println!("Total events: {}", event_logs.len()); + + // Count events per IMR + let mut imr_counts = [0u32; 4]; + for event in &event_logs { + if event.imr < 4 { + imr_counts[event.imr as usize] += 1; + } + } + + println!("Event distribution:"); + for (idx, count) in imr_counts.iter().enumerate() { + println!(" IMR {}: {} events", idx, count); + } + println!(); + + // Replay event logs to calculate IMR/RTMR values + println!("Replaying event log..."); + let mut rtmrs: [[u8; 48]; 4] = [[0u8; 48]; 4]; + + for event in &event_logs { + if event.imr < 4 { + let mut hasher = sha2::Sha384::new(); + hasher.update(rtmrs[event.imr as usize]); + hasher.update(event.digest()); + rtmrs[event.imr as usize] = hasher.finalize().into(); + } + } + + println!("\nCalculated IMR/RTMR values from event log replay:\n"); + println!("IMR 0 (CCEL) → {}", hex::encode(rtmrs[0])); + println!("IMR 1 (CCEL) → {}", hex::encode(rtmrs[1])); + println!("IMR 2 (CCEL) → {}", hex::encode(rtmrs[2])); + println!("IMR 3 (CCEL) → {}", hex::encode(rtmrs[3])); + + println!("\n========================================"); + println!("Note: These are the calculated values from replaying the CCEL event log."); + println!("The mapping between CCEL IMR indices and TDX RTMR indices may vary"); + println!("depending on the platform implementation."); + + Ok(()) +} + fn cmd_hex(hex_args: HexCommand) -> Result<()> { fn hex_encode_io(io: &mut impl Read) -> Result<()> { loop { @@ -321,14 +631,13 @@ fn cmd_gen_ca_cert(args: GenCaCertArgs) -> Result<()> { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let pubkey = key.public_key_der(); let report_data = QuoteContentType::KmsRootCa.to_report_data(&pubkey); - let (_, quote) = att::get_quote(&report_data, None).context("Failed to get quote")?; - let event_logs = att::eventlog::read_event_logs().context("Failed to read event logs")?; - let event_log = serde_json::to_vec(&event_logs).context("Failed to serialize event logs")?; + let attestation = Attestation::quote(&report_data) + .context("Failed to get attestation")? + .into_versioned(); let req = CertRequest::builder() .subject("App Root CA") - .quote("e) - .event_log(&event_log) + .attestation(&attestation) .key(&key) .ca_level(args.ca_level) .build(); @@ -347,38 +656,60 @@ fn cmd_gen_app_keys(args: GenAppKeysArgs) -> Result<()> { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let disk_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let k256_key = SigningKey::random(&mut rand::thread_rng()); - let app_keys = make_app_keys(key, disk_key, k256_key, args.ca_level, None)?; + let key_provider = KeyProvider::None { + key: key.serialize_pem(), + }; + let app_keys = make_app_keys(&key, &disk_key, &k256_key, args.ca_level, key_provider)?; let app_keys = serde_json::to_string(&app_keys).context("Failed to serialize app keys")?; fs::write(&args.output, app_keys).context("Failed to write app keys")?; Ok(()) } -fn gen_app_keys_from_seed(seed: &[u8], mr: Option>) -> Result { +fn gen_app_keys_from_seed( + seed: &[u8], + provider: KeyProviderKind, + mr: Option>, +) -> Result { let key = derive_ecdsa_key_pair_from_bytes(seed, &["app-key".as_bytes()])?; let disk_key = derive_ecdsa_key_pair_from_bytes(seed, &["app-disk-key".as_bytes()])?; let k256_key = derive_ecdsa_key(seed, &["app-k256-key".as_bytes()], 32)?; let k256_key = SigningKey::from_bytes(&k256_key).context("Failed to parse k256 key")?; - make_app_keys(key, disk_key, k256_key, 1, mr) + let key_provider = match provider { + KeyProviderKind::None => KeyProvider::None { + key: key.serialize_pem(), + }, + KeyProviderKind::Local => KeyProvider::Local { + mr: mr.context("Missing MR for local key provider")?, + key: key.serialize_pem(), + }, + KeyProviderKind::Tpm => KeyProvider::Tpm { + key: key.serialize_pem(), + pubkey: key.public_key_der(), + }, + KeyProviderKind::Kms => { + anyhow::bail!("KMS keys must be fetched from the KMS server") + } + }; + make_app_keys(&key, &disk_key, &k256_key, 1, key_provider) } fn make_app_keys( - app_key: KeyPair, - disk_key: KeyPair, - k256_key: SigningKey, + app_key: &KeyPair, + disk_key: &KeyPair, + k256_key: &SigningKey, ca_level: u8, - mr: Option>, + key_provider: KeyProvider, ) -> Result { use ra_tls::cert::CertRequest; let pubkey = app_key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let (_, quote) = att::get_quote(&report_data, None).context("Failed to get quote")?; - let event_logs = att::eventlog::read_event_logs().context("Failed to read event logs")?; - let event_log = serde_json::to_vec(&event_logs).context("Failed to serialize event logs")?; + let attestation = Attestation::quote(&report_data) + .context("Failed to get attestation")? + .into_versioned(); let req = CertRequest::builder() .subject("App Root Cert") - .quote("e) - .event_log(&event_log) - .key(&app_key) + .attestation(&attestation) + .key(app_key) .ca_level(ca_level) .build(); let cert = req @@ -392,15 +723,7 @@ fn make_app_keys( k256_signature: vec![], gateway_app_id: "".to_string(), ca_cert: cert.pem(), - key_provider: match mr { - Some(mr) => KeyProvider::Local { - mr, - key: app_key.serialize_pem(), - }, - None => KeyProvider::None { - key: app_key.serialize_pem(), - }, - }, + key_provider, }) } @@ -417,89 +740,351 @@ fn sha256(data: &[u8]) -> [u8; 32] { sha256.finalize().into() } -fn get_project_name(compose_file: impl AsRef) -> Result { - let project_name = fs::canonicalize(compose_file) - .context("Failed to canonicalize compose file")? - .parent() - .context("Failed to get parent directory of compose file")? - .file_name() - .context("Failed to get file name of compose file")? - .to_string_lossy() - .into_owned(); - Ok(project_name) -} - -async fn cmd_remove_orphans(compose_file: impl AsRef) -> Result<()> { - // Connect to Docker daemon - let docker = - Docker::connect_with_local_defaults().context("Failed to connect to Docker daemon")?; - - // Read and parse docker-compose.yaml to get project name - let compose_content = - fs::read_to_string(compose_file.as_ref()).context("Failed to read docker-compose.yaml")?; - let docker_compose: ComposeConfig = - serde_yaml2::from_str(&compose_content).context("Failed to parse docker-compose.yaml")?; - - // Get current project name from compose file or directory name - let project_name = match docker_compose.name { - Some(name) => name, - None => get_project_name(compose_file)?, +fn cmd_vtpm_attest(args: VtpmAttestArgs) -> Result<()> { + use cmd_lib::run_cmd; + use serde::Serialize; + + #[derive(Serialize)] + struct AttestationResult { + success: bool, + ek_cert_verified: bool, + quote_verified: bool, + os_image_verified: Option, + nonce: String, + key_algorithm: String, + error: Option, + } + + // verify root CA file exists + if !args.root_ca.exists() { + anyhow::bail!("root CA file not found: {:?}", args.root_ca); + } + + // verify key algorithm + let (ek_algo, ak_algo, ak_scheme, algo_name) = match args.key_algo.to_lowercase().as_str() { + "rsa" => ("rsa", "rsa", "rsassa", "RSA-2048"), + "ecc" | "ecdsa" => ("ecc", "ecc", "ecdsa", "ECC P-256"), + _ => anyhow::bail!( + "invalid key algorithm: {}. Use 'rsa' or 'ecc'", + args.key_algo + ), }; - // List all containers - let options = ListContainersOptions:: { - all: true, - ..Default::default() + let mut result = AttestationResult { + success: false, + ek_cert_verified: false, + quote_verified: false, + os_image_verified: None, + nonce: args.nonce.clone(), + key_algorithm: algo_name.to_string(), + error: None, }; - let containers = docker - .list_containers(Some(options)) - .await - .context("Failed to list containers")?; + let attestation_result = (|| -> Result<()> { + if args.format == "text" { + println!("=== vTPM Attestation ==="); + println!("Root CA: {:?}", args.root_ca); + println!("Nonce: {}", args.nonce); + println!("Key Algorithm: {}", algo_name); + println!(); + } - // Find and remove orphaned containers - for container in containers { - let Some(labels) = container.labels else { - continue; - }; + // step 1: extract EK certificate + if args.format == "text" { + println!("[1/7] extracting EK certificate..."); + } + run_cmd! { + tpm2_nvread -o /tmp/ek_cert.der 0x1c00002 2>/dev/null; + openssl x509 -inform DER -in /tmp/ek_cert.der -out /tmp/ek_cert.pem 2>/dev/null; + } + .context("failed to extract EK certificate")?; - // Check if container belongs to current project - let Some(container_project) = labels.get("com.docker.compose.project") else { - continue; - }; + // step 2: extract intermediate CA URL + if args.format == "text" { + println!("[2/7] downloading intermediate CA..."); + } + let ica_url_output = std::process::Command::new("openssl") + .args(["x509", "-in", "/tmp/ek_cert.pem", "-noout", "-text"]) + .output() + .context("failed to read EK cert")?; + let ica_text = String::from_utf8_lossy(&ica_url_output.stdout); + let ica_url = ica_text + .lines() + .find(|l| l.contains("CA Issuers") && l.contains("URI:")) + .and_then(|l| l.split("URI:").nth(1)) + .map(|s| s.trim()) + .context("failed to find Intermediate CA URL")?; - if container_project != &project_name { - continue; + run_cmd! { + curl -s -o /tmp/intermediate_ca.crt $ica_url; } - // Check if service still exists in compose file - let Some(service_name) = labels.get("com.docker.compose.service") else { - continue; + .context("failed to download intermediate CA")?; + + // try DER first, then PEM + let convert_result = run_cmd! { + openssl x509 -inform DER -in /tmp/intermediate_ca.crt -outform PEM -out /tmp/intermediate_ca.pem 2>/dev/null; }; - if docker_compose.services.contains_key(service_name) { - continue; + if convert_result.is_err() { + run_cmd! { + openssl x509 -inform PEM -in /tmp/intermediate_ca.crt -outform PEM -out /tmp/intermediate_ca.pem 2>/dev/null; + } + .context("failed to convert intermediate CA")?; } - // Service no longer exists in compose file, remove the container - let Some(container_id) = container.id else { - continue; - }; - println!("Removing orphaned container {service_name} {container_id}"); - docker - .remove_container( - &container_id, - Some(RemoveContainerOptions { - v: true, - force: true, - ..Default::default() - }), - ) - .await - .with_context(|| format!("Failed to remove container {}", container_id))?; + // step 3: verify intermediate CA + if args.format == "text" { + println!("[3/7] verifying certificate chain..."); + } + let root_ca_path = args.root_ca.to_str().context("invalid root CA path")?; + run_cmd! { + openssl verify -CAfile $root_ca_path /tmp/intermediate_ca.pem >/dev/null 2>&1; + } + .context("intermediate CA verification failed")?; + + // step 4: verify EK certificate + run_cmd! { + cat /tmp/intermediate_ca.pem $root_ca_path > /tmp/ca_chain.pem; + openssl verify -CAfile /tmp/ca_chain.pem /tmp/ek_cert.pem >/dev/null 2>&1; + } + .context("EK certificate verification failed")?; + result.ek_cert_verified = true; + + // step 5: create AK + if args.format == "text" { + println!("[4/7] creating attestation key ({})...", algo_name); + } + run_cmd! { + tpm2_createek -c /tmp/ek.ctx -G $ek_algo -u /tmp/ek.pub >/dev/null 2>&1; + tpm2_createak -C /tmp/ek.ctx -c /tmp/ak.ctx -G $ak_algo -g sha256 -s $ak_scheme -u /tmp/ak.pub -n /tmp/ak.name >/dev/null 2>&1; + } + .context("failed to create attestation key")?; + + // step 6: generate quote + if args.format == "text" { + println!("[5/7] generating TPM quote..."); + } + let nonce = &args.nonce; + run_cmd! { + echo -n $nonce > /tmp/nonce.bin; + tpm2_quote -c /tmp/ak.ctx -l sha256:0,1,2,3,4,5,6,7,8,9,10,14 -q /tmp/nonce.bin -m /tmp/quote.msg -s /tmp/quote.sig -o /tmp/quote.pcr -g sha256 >/dev/null 2>&1; + } + .context("failed to generate quote")?; + + // step 7: verify quote + if args.format == "text" { + println!("[6/7] verifying quote signature..."); + } + run_cmd! { + tpm2_checkquote -u /tmp/ak.pub -m /tmp/quote.msg -s /tmp/quote.sig -f /tmp/quote.pcr -g sha256 -q /tmp/nonce.bin >/dev/null 2>&1; + } + .context("quote verification failed")?; + result.quote_verified = true; + + // step 8: verify OS image (optional) + if let Some(expected_hash) = &args.expected_os_hash { + if args.format == "text" { + println!("[7/7] verifying OS image..."); + } + let tpm_eventlog_path = "/sys/kernel/security/tpm0/binary_bios_measurements"; + if Path::new(tpm_eventlog_path).exists() { + let _ = run_cmd! { + tpm2_eventlog $tpm_eventlog_path > /tmp/eventlog.yaml 2>/dev/null; + }; + + let eventlog = fs::read_to_string("/tmp/eventlog.yaml").unwrap_or_default(); + if eventlog.contains(expected_hash) { + result.os_image_verified = Some(true); + } else { + result.os_image_verified = Some(false); + anyhow::bail!("OS image hash mismatch"); + } + } + } + + result.success = true; + Ok(()) + })(); + + if let Err(e) = attestation_result { + result.error = Some(format!("{:#}", e)); + } + + if args.format == "json" { + println!("{}", serde_json::to_string_pretty(&result)?); + } else { + println!(); + println!("=== Attestation Result ==="); + println!( + " EK Certificate Chain: {}", + if result.ek_cert_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + println!( + " TPM Quote: {}", + if result.quote_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + if let Some(os_verified) = result.os_image_verified { + println!( + " OS Image: {}", + if os_verified { + "✓ VERIFIED" + } else { + "✗ MISMATCH" + } + ); + } + println!(); + if result.success { + println!("🎉 ATTESTATION PASSED"); + } else { + println!("❌ ATTESTATION FAILED"); + if let Some(error) = &result.error { + println!("Error: {}", error); + } + anyhow::bail!("attestation failed"); + } } Ok(()) } +fn cmd_tpm_quote(args: TpmQuoteArgs) -> Result<()> { + let data = if let Some(hex_data) = args.data { + let decoded = hex::decode(&hex_data).context("Failed to decode hex data")?; + if decoded.len() > 64 { + anyhow::bail!("Qualifying data must be at most 64 bytes"); + } + decoded + } else { + vec![0u8; 32] // TPM 2.0 max qualifying data is 32 bytes + }; + + // Parse key algorithm + let key_algo = args + .key_algo + .parse::() + .context("Failed to parse key algorithm")?; + + let qualifying_data: [u8; 32] = match args.hash_algo.as_str() { + "none" => data + .try_into() + .ok() + .context("qualifying data must be 32 bytes")?, + "sha256" => ez_hash::sha256(&data), + _ => { + anyhow::bail!("Unsupported hash algorithm"); + } + }; + + let tpm = tpm_attest::TpmContext::open(None).context("Failed to open TPM context")?; + let pcr_selection = tpm_attest::dstack_pcr_policy(); + let tpm_quote = tpm + .create_quote_with_algo(&qualifying_data, &pcr_selection, key_algo) + .context("Failed to create TPM quote")?; + + let quote_json = + serde_json::to_string_pretty(&tpm_quote).context("Failed to serialize TPM quote")?; + + if let Some(output_path) = args.output { + fs::write(&output_path, quote_json).context("Failed to write quote to file")?; + eprintln!("TPM quote written to: {:?}", output_path); + } else { + println!("{}", quote_json); + } + + Ok(()) +} + +async fn cmd_tpm_verify(args: TpmVerifyArgs) -> Result<()> { + let root_ca_pem = fs::read_to_string(&args.root_ca).context("Failed to read root CA")?; + let quote_json = fs::read_to_string(&args.quote).context("Failed to read quote file")?; + let tpm_quote: tpm_attest::TpmQuote = + serde_json::from_str("e_json).context("Failed to parse quote JSON")?; + + println!("=== TPM Quote Verification (dcap-qvl architecture) ==="); + println!("Root CA: {:?}", args.root_ca); + println!("Quote file: {:?}", args.quote); + println!(); + + // Step 1: Get collateral (certificates + CRLs) + println!("[Step 1] Fetching quote collateral (certificates + CRLs)..."); + let collateral = tpm_qvl::get_collateral(&tpm_quote, &root_ca_pem) + .await + .context("Failed to get collateral")?; + let crl_count = collateral.crls.len() + + if collateral.root_ca_crl.is_some() { + 1 + } else { + 0 + }; + println!(" ✓ Collateral fetched: {} CRLs downloaded", crl_count); + println!(); + + // Step 2: Verify quote with conditional CRL checking + println!("[Step 2] Verifying quote (CRL verification if CRL DP present)..."); + + match tpm_qvl::verify::verify_quote_with_ca(&tpm_quote, &collateral, &root_ca_pem) { + Ok(_) => { + // Success - print simple success message + println!(); + let crl_count = collateral.crls.len() + + if collateral.root_ca_crl.is_some() { + 1 + } else { + 0 + }; + if crl_count == 0 { + println!("🎉 VERIFICATION PASSED (no CRLs available)"); + } else { + println!( + "🎉 VERIFICATION PASSED (with {} CRL(s) verified)", + crl_count + ); + } + Ok(()) + } + Err(verification_result) => { + // Failure - print detailed status + println!(); + println!("=== Verification Result ==="); + println!( + " AK Certificate Chain (webpki + CRL): {}", + if verification_result.status.ak_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + println!( + " Quote Signature: {}", + if verification_result.status.signature_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + println!( + " PCR Values: {}", + if verification_result.status.pcr_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + println!(" Error: {}", verification_result.error); + println!(); + anyhow::bail!("Verification failed") + } + } +} + #[tokio::main] async fn main() -> Result<()> { { @@ -511,9 +1096,10 @@ async fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Report => cmd_report()?, Commands::Quote => cmd_quote()?, + Commands::Eventlog => cmd_eventlog()?, Commands::Show => cmd_show_mrs()?, + Commands::ReplayImr => cmd_replay_imr()?, Commands::Extend(extend_args) => { cmd_extend(extend_args)?; } @@ -542,7 +1128,39 @@ async fn main() -> Result<()> { cmd_notify_host(args).await?; } Commands::RemoveOrphans(args) => { - cmd_remove_orphans(args.compose).await?; + if args.no_dockerd { + docker_compose::remove_orphans_direct( + args.compose, + args.docker_root, + args.dry_run, + )?; + } else { + docker_compose::remove_orphans(args.compose, args.dry_run).await?; + } + } + Commands::VtpmAttest(args) => { + cmd_vtpm_attest(args)?; + } + Commands::TpmQuote(args) => { + cmd_tpm_quote(args)?; + } + Commands::TpmVerify(args) => { + cmd_tpm_verify(args).await?; + } + Commands::QuoteReport(args) => { + cmd_quote_report(args)?; + } + Commands::Attest(args) => { + cmd_attest(args)?; + } + Commands::AttestInfo(args) => { + cmd_attest_info(args)?; + } + Commands::AttestJson(args) => { + cmd_attest_json(args)?; + } + Commands::AttestStrip(args) => { + cmd_attest_strip(args)?; } } diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index 6e568702..748444a7 100644 --- a/dstack-util/src/system_setup.rs +++ b/dstack-util/src/system_setup.rs @@ -13,11 +13,12 @@ use std::{ }; use anyhow::{anyhow, bail, Context, Result}; +use dstack_attest::emit_runtime_event; use dstack_kms_rpc as rpc; use dstack_types::{ shared_filenames::{ APP_COMPOSE, APP_KEYS, DECRYPTED_ENV, DECRYPTED_ENV_JSON, ENCRYPTED_ENV, - HOST_SHARED_DIR_NAME, INSTANCE_INFO, SYS_CONFIG, USER_CONFIG, + HOST_SHARED_DIR_NAME, HOST_SHARED_DISK_LABEL, INSTANCE_INFO, SYS_CONFIG, USER_CONFIG, }, KeyProvider, KeyProviderInfo, }; @@ -31,7 +32,6 @@ use ra_tls::cert::generate_ra_cert; use rand::Rng as _; use scopeguard::defer; use serde::{Deserialize, Serialize}; -use tdx_attest::extend_rtmr3; use tracing::{info, warn}; use crate::{ @@ -54,6 +54,7 @@ use ra_tls::{ }; use serde_human_bytes as hex_bytes; use serde_json::Value; +use tpm_attest::{self as tpm, TpmContext}; mod config_id_verifier; @@ -150,12 +151,6 @@ fn parse_dstack_options(shared: &HostShared) -> Result { Ok(options) } -impl InstanceInfo { - fn is_initialized(&self) -> bool { - !self.instance_id_seed.is_empty() - } -} - #[derive(Clone)] pub struct HostShareDir { base_dir: PathBuf, @@ -207,6 +202,42 @@ struct HostShared { } impl HostShared { + /// Find block device by volume label + fn find_disk_by_label(label: &str) -> Option { + let label_path = format!("/dev/disk/by-label/{}", label); + if Path::new(&label_path).exists() { + return Some(label_path); + } + + // Fallback: scan /sys/block for devices and check their labels with blkid + if let Ok(entries) = fs::read_dir("/sys/block") { + for entry in entries.flatten() { + let dev_name = entry.file_name(); + let dev_path = format!("/dev/{}", dev_name.to_string_lossy()); + + // Use blkid to check the label + if let Ok(output) = Command::new("blkid") + .arg("-s") + .arg("LABEL") + .arg("-o") + .arg("value") + .arg(&dev_path) + .output() + { + if output.status.success() { + let found_label = + String::from_utf8_lossy(&output.stdout).trim().to_string(); + if found_label == label { + return Some(dev_path); + } + } + } + } + } + + None + } + fn load(host_shared_dir: impl Into) -> Result { let host_shared_dir = host_shared_dir.into(); let sys_config = deserialize_json_file(host_shared_dir.sys_config_file())?; @@ -259,7 +290,31 @@ impl HostShared { cmd! { info "Mounting host-shared"; mkdir -p $host_shared_dir; - mount -t 9p -o trans=virtio,version=9p2000.L,ro host-shared $host_shared_dir; + }?; + + // Try to detect and mount shared disk by label first, fallback to 9p + let disk_device = Self::find_disk_by_label(HOST_SHARED_DISK_LABEL); + let mounted_via_disk = if let Some(dev) = disk_device { + info!("Found shared disk at {}", dev); + let mount_result = cmd! { + info "Attempting to mount shared disk"; + mount -o ro $dev $host_shared_dir; + }; + mount_result.is_ok() + } else { + false + }; + + if !mounted_via_disk { + info!("Shared disk not found, trying 9p virtfs"); + cmd! { + mount -t 9p -o trans=virtio,version=9p2000.L,ro host-shared $host_shared_dir; + }?; + } else { + info!("Successfully mounted shared disk"); + } + + cmd! { mkdir -p $host_shared_copy_dir; info "Copying host-shared files"; }?; @@ -352,7 +407,7 @@ impl<'a> GatewayContext<'a> { let client_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).context("Failed to generate key")?; let client_certs = cert_client - .request_cert(&client_key, config, false) + .request_cert(&client_key, config, None) .await .context("Failed to request cert")?; let client_cert = client_certs.join("\n"); @@ -452,7 +507,7 @@ fn truncate(s: &[u8], len: usize) -> &[u8] { fn emit_key_provider_info(provider_info: &KeyProviderInfo) -> Result<()> { info!("Key provider info: {provider_info:?}"); let provider_info_json = serde_json::to_vec(&provider_info)?; - extend_rtmr3("key-provider", &provider_info_json)?; + emit_runtime_event("key-provider", &provider_info_json)?; Ok(()) } @@ -584,7 +639,7 @@ impl<'a> Stage0<'a> { let kms_info = att .decode_app_info(false) .context("Failed to decode app_info")?; - extend_rtmr3("mr-kms", &kms_info.mr_aggregated) + emit_runtime_event("mr-kms", &kms_info.mr_aggregated) .context("Failed to extend mr-kms to RTMR3")?; } Ok(()) @@ -601,7 +656,7 @@ impl<'a> Stage0<'a> { .await .context("Failed to get app key")?; - extend_rtmr3("os-image-hash", &response.os_image_hash) + emit_runtime_event("os-image-hash", &response.os_image_hash) .context("Failed to extend os-image-hash to RTMR3")?; let (_, ca_pem) = x509_parser::pem::parse_x509_pem(tmp_ca.ca_cert.as_bytes()) @@ -670,11 +725,50 @@ impl<'a> Stage0<'a> { .await .context("Failed to get sealing key")?; // write to fs - let app_keys = gen_app_keys_from_seed(&provision.sk, Some(provision.mr.to_vec())) - .context("Failed to generate app keys")?; + let app_keys = gen_app_keys_from_seed( + &provision.sk, + KeyProviderKind::Local, + Some(provision.mr.to_vec()), + ) + .context("Failed to generate app keys")?; Ok(app_keys) } + fn generate_tpm_app_keys(&self) -> Result { + let tpm = TpmContext::detect().context("failed to detect TPM context")?; + + // Get PCR policy for sealing (boot chain + app PCR) + let pcr_policy = tpm::dstack_pcr_policy(); + + // Try to read sealed seed (bound to PCR values including app PCR) + if let Some(seed) = tpm + .unseal::<32>(tpm::SEALED_NV_INDEX, tpm::PRIMARY_KEY_HANDLE, &pcr_policy) + .context("failed to unseal from TPM")? + { + info!( + "unsealed root key seed from TPM (PCR policy: {})", + pcr_policy.to_arg() + ); + return gen_app_keys_from_seed(&seed, KeyProviderKind::Tpm, None) + .context("failed to generate TPM app keys"); + } + + // No sealed seed exists, generate new one + info!("no sealed seed found, generating new seed..."); + let seed: [u8; 32] = tpm.get_random().context("TPM RNG unavailable")?; + // Seal the new seed to TPM with PCR policy (including app PCR) + tpm.seal( + &seed, + tpm::SEALED_NV_INDEX, + tpm::PRIMARY_KEY_HANDLE, + &pcr_policy, + ) + .context("failed to seal seed to TPM")?; + + gen_app_keys_from_seed(&seed, KeyProviderKind::Tpm, None) + .context("failed to generate TPM app keys") + } + async fn request_app_keys(&self) -> Result { let key_provider = self.shared.app_compose.key_provider(); match key_provider { @@ -683,7 +777,12 @@ impl<'a> Stage0<'a> { KeyProviderKind::None => { info!("No key provider is enabled, generating temporary app keys"); let seed: [u8; 32] = rand::thread_rng().gen(); - gen_app_keys_from_seed(&seed, None).context("Failed to generate app keys") + gen_app_keys_from_seed(&seed, KeyProviderKind::None, None) + .context("Failed to generate app keys") + } + KeyProviderKind::Tpm => { + info!("Generating app keys from TPM"); + self.generate_tpm_app_keys() } } } @@ -767,12 +866,66 @@ impl<'a> Stage0<'a> { Ok(()) } - async fn mount_data_disk( - &self, - initialized: bool, - disk_crypt_key: &str, - opts: &DstackOptions, - ) -> Result<()> { + fn is_disk_initialized(&self, opts: &DstackOptions) -> bool { + let device = &self.args.device; + + // For encrypted storage, just check if LUKS header exists + // The filesystem check happens after the LUKS device is opened + let has_luks = if opts.storage_encrypted { + let result = cmd!(cryptsetup isLuks $device).is_ok(); + if result { + info!("LUKS header detected on {}", device.display()); + } + result + } else { + false + }; + + // Check if filesystem exists + let has_fs = match opts.storage_fs { + FsType::Zfs => { + // Check if zpool exists by trying to import it in readonly mode + if cmd!(zpool import -N -o readonly=on dstack).is_ok() { + cmd!(zpool export dstack).ok(); + info!("ZFS pool 'dstack' detected"); + true + } else { + false + } + } + FsType::Ext4 if !opts.storage_encrypted => { + // For unencrypted ext4, check the device directly + if cmd!(blkid -s TYPE -o value $device) + .map(|out| out.trim() == "ext4") + .unwrap_or(false) + { + info!("ext4 filesystem detected on {}", device.display()); + true + } else { + false + } + } + FsType::Ext4 => { + // For encrypted ext4, we can only check after LUKS is opened + // So we rely on LUKS header presence as indicator + has_luks + } + }; + + // For encrypted ZFS, need both LUKS header AND zpool to exist + let initialized = if opts.storage_encrypted && opts.storage_fs == FsType::Zfs { + has_luks && has_fs + } else { + has_luks || has_fs + }; + + if !initialized { + info!("No existing filesystem detected on {}", device.display()); + } + initialized + } + + async fn mount_data_disk(&self, disk_crypt_key: &str, opts: &DstackOptions) -> Result<()> { let name = "dstack_data_disk"; let mount_point = &self.args.mount_point; @@ -785,7 +938,9 @@ impl<'a> Stage0<'a> { cmd!(mkdir -p $mount_point).context("Failed to create mount point")?; - if !initialized { + let disk_initialized = self.is_disk_initialized(opts); + + if !disk_initialized { self.vmm .notify_q("boot.progress", "initializing data disk") .await; @@ -937,6 +1092,20 @@ impl<'a> Stage0<'a> { echo -n $disk_crypt_key | cryptsetup luksOpen --type luks2 --header $in_mem_hdr -d- $root_hd $name; } .or(Err(anyhow!("Failed to open encrypted data disk")))?; + + // Wait for device mapper to create the device + let dm_path = format!("/dev/mapper/{name}"); + for i in 0..10 { + if std::path::Path::new(&dm_path).exists() { + info!("Device mapper {} is ready", dm_path); + break; + } + if i == 9 { + bail!("Timed out waiting for device mapper {}", dm_path); + } + info!("Waiting for device mapper {}...", dm_path); + std::thread::sleep(std::time::Duration::from_millis(500)); + } Ok(()) } @@ -967,15 +1136,17 @@ impl<'a> Stage0<'a> { sha256(&id_path)[..20].to_vec() }; instance_info.instance_id = instance_id.clone(); - if !kms_enabled && instance_info.app_id != truncated_compose_hash { - bail!("App upgrade is not supported without KMS"); - } + let app_id = if kms_enabled { + instance_info.app_id.clone() + } else { + truncated_compose_hash.to_vec() + }; - extend_rtmr3("system-preparing", &[])?; - extend_rtmr3("app-id", &instance_info.app_id)?; - extend_rtmr3("compose-hash", &compose_hash)?; - extend_rtmr3("instance-id", &instance_id)?; - extend_rtmr3("boot-mr-done", &[])?; + emit_runtime_event("system-preparing", &[])?; + emit_runtime_event("app-id", &app_id)?; + emit_runtime_event("compose-hash", &compose_hash)?; + emit_runtime_event("instance-id", &instance_id)?; + emit_runtime_event("boot-mr-done", &[])?; Ok(AppInfo { instance_info, compose_hash, @@ -1001,6 +1172,9 @@ impl<'a> Stage0<'a> { KeyProvider::Local { mr, .. } => { KeyProviderInfo::new("local-sgx".into(), hex::encode(mr)) } + KeyProvider::Tpm { pubkey, .. } => { + KeyProviderInfo::new("tpm".into(), hex::encode(pubkey)) + } KeyProvider::Kms { pubkey, .. } => { KeyProviderInfo::new("kms".into(), hex::encode(pubkey)) } @@ -1010,7 +1184,6 @@ impl<'a> Stage0<'a> { } async fn setup_fs(self) -> Result> { - let is_initialized = self.shared.instance_info.is_initialized(); let app_info = self .measure_app_info() .context("Failed to measure app info")?; @@ -1034,18 +1207,14 @@ impl<'a> Stage0<'a> { // Parse kernel command line options let opts = parse_dstack_options(&self.shared).context("Failed to parse kernel cmdline")?; - extend_rtmr3("storage-fs", opts.storage_fs.to_string().as_bytes())?; + emit_runtime_event("storage-fs", opts.storage_fs.to_string().as_bytes())?; info!( "Filesystem options: encryption={}, filesystem={:?}", opts.storage_encrypted, opts.storage_fs ); - self.mount_data_disk( - is_initialized, - &hex::encode(&app_keys.disk_crypt_key), - &opts, - ) - .await?; + self.mount_data_disk(&hex::encode(&app_keys.disk_crypt_key), &opts) + .await?; self.setup_swap(self.shared.app_compose.swap_size, &opts) .await?; self.vmm @@ -1054,7 +1223,7 @@ impl<'a> Stage0<'a> { &serde_json::to_string(&app_info.instance_info)?, ) .await; - extend_rtmr3("system-ready", &[])?; + emit_runtime_event("system-ready", &[])?; self.vmm.notify_q("boot.progress", "data disk ready").await; if !self.shared.app_compose.key_provider().is_kms() { diff --git a/dstack-util/src/system_setup/config_id_verifier.rs b/dstack-util/src/system_setup/config_id_verifier.rs index 4e69a465..c62f665c 100644 --- a/dstack-util/src/system_setup/config_id_verifier.rs +++ b/dstack-util/src/system_setup/config_id_verifier.rs @@ -7,7 +7,7 @@ use dstack_types::{mr_config::MrConfig, KeyProviderKind}; use tracing::info; fn read_mr_config_id() -> Result<[u8; 48]> { - let (_, quote) = tdx_attest::get_quote(&[0u8; 64], None).context("Failed to get quote")?; + let quote = tdx_attest::get_quote(&[0u8; 64]).context("Failed to get quote")?; let quote = dcap_qvl::quote::Quote::parse("e).context("Failed to parse quote")?; let configid = quote .report @@ -27,7 +27,7 @@ fn read_mr_config_id() -> Result<[u8; 48]> { /// Where the instance info is a concatenated bytes of the following fields: /// - compose_hash: [u8; 32] /// - app_id: [u8; 20] -/// - key_provider_type: u8 // 0: none, 1: local, 2: kms +/// - key_provider_type: u8 // 0: none, 1: local, 2: kms, 3: tpm /// - key_provider_id: [u8] // the ca pubkey for KMS or the MR enclave for local-sgx provider, empty for none pub fn verify_mr_config_id( compose_hash: &[u8; 32], diff --git a/dstack-util/tests/fixtures/key-provider-docker-compose.yaml b/dstack-util/tests/fixtures/key-provider-docker-compose.yaml new file mode 100644 index 00000000..25e7e4ef --- /dev/null +++ b/dstack-util/tests/fixtures/key-provider-docker-compose.yaml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: © 2024-2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +x-common: &common-config + restart: always + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + +services: + aesmd: + <<: *common-config + container_name: aesmd + build: + context: . + dockerfile: Dockerfile.aesmd + privileged: true + devices: + - "/dev/sgx_enclave:/dev/sgx_enclave" + - "/dev/sgx_provision:/dev/sgx_provision" + volumes: + - "./sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf" + - "aesmd:/var/run/aesmd/" + network_mode: "host" + + gramine-sealing-key-provider: + <<: *common-config + container_name: gramine-sealing-key-provider + build: + context: . + dockerfile: Dockerfile.key-provider + privileged: true + devices: + - "/dev/sgx_enclave:/dev/sgx_enclave" + - "/dev/sgx_provision:/dev/sgx_provision" + depends_on: + - aesmd + volumes: + - "aesmd:/var/run/aesmd/" + ports: + - "127.0.0.1:3443:3443" + +volumes: + aesmd: diff --git a/dstack-util/tests/test_remove_orphans.sh b/dstack-util/tests/test_remove_orphans.sh new file mode 100755 index 00000000..0400693b --- /dev/null +++ b/dstack-util/tests/test_remove_orphans.sh @@ -0,0 +1,236 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: © 2025 Phala Network +# SPDX-License-Identifier: Apache-2.0 + +# Test script for remove-orphans command (both online and offline modes) +# Uses real docker compose to create containers for accurate testing + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +DSTACK_UTIL="$PROJECT_ROOT/target/release/dstack-util" +TEST_DIR=$(mktemp -d) +DOCKER_ROOT="/var/lib/docker" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Project name for tests +PROJECT_NAME="test-orphan-$$" + +cleanup() { + echo -e "${YELLOW}Cleaning up...${NC}" + rm -rf "$TEST_DIR" + # Clean up test containers + docker compose -f "$TEST_DIR/docker-compose.yaml" down -v 2>/dev/null || true + docker rm -f "${PROJECT_NAME}-old" 2>/dev/null || true +} + +trap cleanup EXIT + +echo -e "${YELLOW}=== Test remove-orphans commands ===${NC}" +echo "Test directory: $TEST_DIR" +echo "Project root: $PROJECT_ROOT" +echo "Project name: $PROJECT_NAME" + +# Check if Docker is available +if ! docker info >/dev/null 2>&1; then + echo -e "${RED}ERROR: Docker daemon not available${NC}" + exit 1 +fi + +# Build dstack-util in release mode +echo -e "\n${YELLOW}Building dstack-util...${NC}" +cargo build --release --package dstack-util --manifest-path "$PROJECT_ROOT/Cargo.toml" + +if [ ! -f "$DSTACK_UTIL" ]; then + echo -e "${RED}ERROR: dstack-util binary not found at $DSTACK_UTIL${NC}" + exit 1 +fi + +# ============================================ +# Setup: Create containers using docker compose +# ============================================ +echo -e "\n${YELLOW}=== Setup: Creating test containers with docker compose ===${NC}" + +# Create compose file with web, db, and old-service +cat >"$TEST_DIR/docker-compose-full.yaml" <"$TEST_DIR/docker-compose.yaml" <&1) +echo "$OUTPUT" + +if echo "$OUTPUT" | grep -q "would remove orphaned container old-service"; then + echo -e "${GREEN}✓ Dry-run correctly identified orphaned container${NC}" +else + echo -e "${RED}✗ Dry-run failed to identify orphaned container${NC}" + sudo systemctl start docker + exit 1 +fi + +# Test actual removal +echo -e "\n${YELLOW}Testing offline actual removal...${NC}" +OUTPUT=$(sudo "$DSTACK_UTIL" remove-orphans --no-dockerd -f "$TEST_DIR/docker-compose.yaml" -d "$DOCKER_ROOT" 2>&1) +echo "$OUTPUT" + +if echo "$OUTPUT" | grep -q "removing orphaned container old-service"; then + echo -e "${GREEN}✓ Removal correctly identified orphaned container${NC}" +else + echo -e "${RED}✗ Removal failed to identify orphaned container${NC}" + sudo systemctl start docker + exit 1 +fi + +# ============================================ +# Restart Docker and verify +# ============================================ +echo -e "\n${YELLOW}Restarting Docker daemon...${NC}" +sudo systemctl start docker + +# Wait for docker to start +sleep 3 + +# Verify old-service container is gone +echo -e "\n${YELLOW}Verifying results after Docker restart...${NC}" +echo "Remaining containers:" +docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "table {{.Names}}\t{{.Status}}" + +if docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "{{.Names}}" | grep -q "old-service"; then + echo -e "${RED}✗ old-service container still exists${NC}" + exit 1 +else + echo -e "${GREEN}✓ old-service container was removed${NC}" +fi + +# Verify web and db containers still exist +if docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "{{.Names}}" | grep -q "web"; then + echo -e "${GREEN}✓ web container still exists${NC}" +else + echo -e "${RED}✗ web container was incorrectly removed${NC}" + exit 1 +fi + +if docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "{{.Names}}" | grep -q "db"; then + echo -e "${GREEN}✓ db container still exists${NC}" +else + echo -e "${RED}✗ db container was incorrectly removed${NC}" + exit 1 +fi + +# ============================================ +# Test 2: Online mode (with Docker daemon) +# ============================================ +echo -e "\n${YELLOW}=== Test 2: Online mode (remove-orphans) ===${NC}" + +# Create another orphan container using docker run +echo "Creating orphan container for online test..." +docker run -d --name "${PROJECT_NAME}-old" \ + --label "com.docker.compose.project=${PROJECT_NAME}" \ + --label "com.docker.compose.service=another-old-service" \ + alpine:latest sleep infinity + +echo "Containers before online removal:" +docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "table {{.Names}}\t{{.Status}}" + +# Test dry-run +echo -e "\n${YELLOW}Testing online dry-run mode...${NC}" +OUTPUT=$("$DSTACK_UTIL" remove-orphans -f "$TEST_DIR/docker-compose.yaml" -n 2>&1) +echo "$OUTPUT" + +if echo "$OUTPUT" | grep -q "would remove orphaned container another-old-service"; then + echo -e "${GREEN}✓ Online dry-run correctly identified orphaned container${NC}" +else + echo -e "${RED}✗ Online dry-run failed to identify orphaned container${NC}" + exit 1 +fi + +# Verify orphan still exists after dry-run +if docker ps -a --format "{{.Names}}" | grep -q "${PROJECT_NAME}-old"; then + echo -e "${GREEN}✓ Online dry-run did not remove container${NC}" +else + echo -e "${RED}✗ Online dry-run incorrectly removed container${NC}" + exit 1 +fi + +# Test actual removal +echo -e "\n${YELLOW}Testing online actual removal...${NC}" +OUTPUT=$("$DSTACK_UTIL" remove-orphans -f "$TEST_DIR/docker-compose.yaml" 2>&1) +echo "$OUTPUT" + +if echo "$OUTPUT" | grep -q "removing orphaned container another-old-service"; then + echo -e "${GREEN}✓ Online removal correctly identified orphaned container${NC}" +else + echo -e "${RED}✗ Online removal failed to identify orphaned container${NC}" + exit 1 +fi + +# Verify orphan was removed +if ! docker ps -a --format "{{.Names}}" | grep -q "${PROJECT_NAME}-old"; then + echo -e "${GREEN}✓ Orphaned container was removed${NC}" +else + echo -e "${RED}✗ Orphaned container was NOT removed${NC}" + exit 1 +fi + +# Verify other containers still exist +echo "Containers after online removal:" +docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "table {{.Names}}\t{{.Status}}" + +# Final cleanup +echo -e "\n${YELLOW}Final cleanup...${NC}" +docker compose -f "$TEST_DIR/docker-compose.yaml" down -v 2>/dev/null || true + +echo -e "\n${GREEN}=== All tests passed! ===${NC}" diff --git a/gateway/dstack-app/builder/Dockerfile b/gateway/dstack-app/builder/Dockerfile index ba5bedb0..3f007642 100644 --- a/gateway/dstack-app/builder/Dockerfile +++ b/gateway/dstack-app/builder/Dockerfile @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -FROM rust:1.86.0@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 AS gateway-builder +FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS gateway-builder COPY ./shared /build ARG DSTACK_REV WORKDIR /build diff --git a/gateway/rpc/proto/gateway_rpc.proto b/gateway/rpc/proto/gateway_rpc.proto index 890e87f7..2c7aa552 100644 --- a/gateway/rpc/proto/gateway_rpc.proto +++ b/gateway/rpc/proto/gateway_rpc.proto @@ -87,7 +87,10 @@ message HostInfo { message QuotedPublicKey { bytes public_key = 1; + // The TDX quote of the public_key string quote = 2; + // The dstack attestation of the public key. + string attestation = 3; } // AcmeInfoResponse is the response for AcmeInfo. @@ -104,6 +107,8 @@ message AcmeInfoResponse { string active_cert = 5; // The domain that serves ZT-HTTPS string base_domain = 6; + // The attestation of the ACME account URI. + string account_attestation = 7; } // Get HostInfo for associated instance id. diff --git a/gateway/src/main.rs b/gateway/src/main.rs index d4544ffd..5d86e84f 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -106,7 +106,7 @@ async fn maybe_gen_certs(config: &Config, tls_config: &TlsConfig) -> Result<()> let cert = ra_tls::cert::CertRequest::builder() .key(&key) .subject("dstack-gateway") - .alt_names(&[config.rpc_domain.clone()]) + .alt_names(std::slice::from_ref(&config.rpc_domain)) .usage_server_auth(true) .build() .self_signed() diff --git a/gateway/src/main_service.rs b/gateway/src/main_service.rs index c85d501c..e6bc6775 100644 --- a/gateway/src/main_service.rs +++ b/gateway/src/main_service.rs @@ -100,7 +100,7 @@ impl Proxy { } impl ProxyInner { - pub(crate) fn lock(&self) -> MutexGuard { + pub(crate) fn lock(&self) -> MutexGuard<'_, ProxyState> { self.state.lock().or_panic("Failed to lock AppState") } @@ -202,6 +202,14 @@ impl Proxy { ) .await .unwrap_or_default(); + let account_attestation = get_or_generate_attestation( + &agent, + QuoteContentType::Custom("acme-account"), + account_uri.as_bytes(), + workdir.acme_account_quote_path(), + ) + .await + .unwrap_or_default(); let mut quoted_hist_keys = vec![]; for cert_path in workdir.list_certs().unwrap_or_default() { @@ -215,9 +223,18 @@ impl Proxy { ) .await .unwrap_or_default(); + let attestation = get_or_generate_attestation( + &agent, + QuoteContentType::Custom("zt-cert"), + &pubkey, + cert_path.display().to_string() + ".quote", + ) + .await + .unwrap_or_default(); quoted_hist_keys.push(QuotedPublicKey { public_key: pubkey, quote, + attestation, }); } let active_cert = @@ -227,6 +244,7 @@ impl Proxy { account_uri, hist_keys: keys.into_iter().collect(), account_quote, + account_attestation, quoted_hist_keys, active_cert, base_domain: config.proxy.base_domain.clone(), @@ -825,6 +843,26 @@ async fn get_or_generate_quote( Ok(quote) } +async fn get_or_generate_attestation( + agent: &DstackGuestClient, + content_type: QuoteContentType<'_>, + payload: &[u8], + quote_path: impl AsRef, +) -> Result { + let quote_path = quote_path.as_ref(); + if fs::metadata(quote_path).is_ok() { + return fs::read_to_string(quote_path).context("Failed to read quote"); + } + let report_data = content_type.to_report_data(payload).to_vec(); + let response = agent + .attest(RawQuoteArgs { report_data }) + .await + .context("Failed to get quote")?; + let attestation = serde_json::to_string(&response).context("Failed to serialize quote")?; + safe_write(quote_path, &attestation).context("Failed to write quote")?; + Ok(attestation) +} + impl RpcCall for RpcHandler { type PrpcService = GatewayServer; diff --git a/gateway/src/proxy/sni.rs b/gateway/src/proxy/sni.rs index 5235765b..4901cc45 100644 --- a/gateway/src/proxy/sni.rs +++ b/gateway/src/proxy/sni.rs @@ -10,7 +10,7 @@ pub fn extract_sni(b: &[u8]) -> Option<&[u8]> { extract_sni_inner(b).ok().map(|r| r.1) } -fn extract_sni_inner(b: &[u8]) -> Result<(usize, &[u8]), PErr> { +fn extract_sni_inner(b: &[u8]) -> Result<(usize, &[u8]), PErr<'_, u8>> { const HANDSHAKE_TYPE_CLIENT_HELLO: usize = 1; const EXTENSION_TYPE_SNI: usize = 0; const NAME_TYPE_HOST_NAME: usize = 0; diff --git a/guest-agent/Cargo.toml b/guest-agent/Cargo.toml index b8e0c881..fe8813ba 100644 --- a/guest-agent/Cargo.toml +++ b/guest-agent/Cargo.toml @@ -29,8 +29,9 @@ rinja.workspace = true git-version.workspace = true ra-rpc = { workspace = true, features = ["client", "rocket"] } dstack-guest-agent-rpc.workspace = true -ra-tls.workspace = true +ra-tls = { workspace = true, features = ["quote"] } tdx-attest.workspace = true +tpm-attest.workspace = true guest-api = { workspace = true, features = ["client"] } host-api = { workspace = true, features = ["client"] } sysinfo.workspace = true @@ -46,8 +47,10 @@ dstack-types.workspace = true sha3.workspace = true strip-ansi-escapes.workspace = true cert-client.workspace = true +dstack-attest.workspace = true ring.workspace = true ed25519-dalek.workspace = true tempfile.workspace = true rand.workspace = true or-panic.workspace = true +cc-eventlog.workspace = true diff --git a/guest-agent/dstack.toml b/guest-agent/dstack.toml index 28722bf0..63b71bc9 100644 --- a/guest-agent/dstack.toml +++ b/guest-agent/dstack.toml @@ -18,9 +18,7 @@ data_disks = ["/"] [default.core.simulator] enabled = false -quote_file = "quote.hex" -event_log_file = "eventlog.json" -sys_config_file = "sys-config.json" +attestation_file = "attestation.bin" [internal-v0] address = "unix:/var/run/tappd.sock" diff --git a/guest-agent/fixtures/attestation.bin b/guest-agent/fixtures/attestation.bin new file mode 100644 index 00000000..2f7a9c35 Binary files /dev/null and b/guest-agent/fixtures/attestation.bin differ diff --git a/guest-agent/rpc/proto/agent_rpc.proto b/guest-agent/rpc/proto/agent_rpc.proto index 4d728dc3..f12b161d 100644 --- a/guest-agent/rpc/proto/agent_rpc.proto +++ b/guest-agent/rpc/proto/agent_rpc.proto @@ -43,6 +43,10 @@ service DstackGuest { // Generates a TDX quote with given report data. rpc GetQuote(RawQuoteArgs) returns (GetQuoteResponse) {} + // Generates a versioned attestation with the given report data. + // Returns a dstack-defined attestation format that supports different attestation modes across platforms. + rpc Attest(RawQuoteArgs) returns (AttestResponse) {} + // Emit an event. This extends the event to RTMR3 on TDX platform. rpc EmitEvent(EmitEventArgs) returns (google.protobuf.Empty) {} @@ -169,6 +173,11 @@ message TdxQuoteResponse { string prefix = 4; } +message AttestResponse { + // The attestation + bytes attestation = 1; +} + message GetQuoteResponse { // TDX quote bytes quote = 1; @@ -211,6 +220,10 @@ message AppInfo { bytes compose_hash = 13; // VM config string vm_config = 14; + // Cloud provider sys_vendor (e.g. "Google") + string cloud_vendor = 15; + // Cloud provider product_name (e.g. "Google Compute Engine") + string cloud_product = 16; } // The response to a Version request diff --git a/guest-agent/src/config.rs b/guest-agent/src/config.rs index c8b75ed0..6276a4da 100644 --- a/guest-agent/src/config.rs +++ b/guest-agent/src/config.rs @@ -71,6 +71,5 @@ where #[derive(Debug, Clone, Deserialize)] pub struct Simulator { pub enabled: bool, - pub quote_file: String, - pub event_log_file: String, + pub attestation_file: String, } diff --git a/guest-agent/src/guest_api_service.rs b/guest-agent/src/guest_api_service.rs index 1376cb25..2f905778 100644 --- a/guest-agent/src/guest_api_service.rs +++ b/guest-agent/src/guest_api_service.rs @@ -208,7 +208,10 @@ pub async fn notify_host(event: &str, payload: &str) -> Result<()> { let local_config: SysConfig = serde_json::from_str(&fs::read_to_string(format!( "{HOST_SHARED_DIR}/{SYS_CONFIG}" ))?)?; - let nc = host_api::client::new_client(local_config.host_api_url); + let Some(host_api_url) = local_config.host_api_url else { + anyhow::bail!("host_api_url not configured"); + }; + let nc = host_api::client::new_client(host_api_url); nc.notify(Notification { event: event.to_string(), payload: payload.to_string(), diff --git a/guest-agent/src/http_routes.rs b/guest-agent/src/http_routes.rs index 4e0c2fac..c8fa44df 100644 --- a/guest-agent/src/http_routes.rs +++ b/guest-agent/src/http_routes.rs @@ -48,6 +48,8 @@ async fn index(state: &State) -> Result, String> { tcb_info, app_cert: _, vm_config: _, + cloud_vendor, + cloud_product, } = handler .info() .await @@ -70,6 +72,8 @@ async fn index(state: &State) -> Result, String> { public_sysinfo, public_logs, public_tcbinfo, + cloud_vendor, + cloud_product, }; match model.render() { Ok(html) => Ok(RawHtml(html)), diff --git a/guest-agent/src/models.rs b/guest-agent/src/models.rs index 12a0f887..a50cf340 100644 --- a/guest-agent/src/models.rs +++ b/guest-agent/src/models.rs @@ -48,6 +48,8 @@ pub struct Dashboard { pub public_sysinfo: bool, pub public_logs: bool, pub public_tcbinfo: bool, + pub cloud_vendor: String, + pub cloud_product: String, } #[derive(Template)] diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 16fe61a4..14f24ddf 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -6,15 +6,17 @@ use std::sync::{Arc, RwLock}; use anyhow::{Context, Result}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use cc_eventlog::tdx::read_event_log; use cert_client::CertRequestClient; +use dstack_attest::emit_runtime_event; use dstack_guest_agent_rpc::{ dstack_guest_server::{DstackGuestRpc, DstackGuestServer}, tappd_server::{TappdRpc, TappdServer}, worker_server::{WorkerRpc, WorkerServer}, - AppInfo, DeriveK256KeyResponse, DeriveKeyArgs, EmitEventArgs, GetAttestationForAppKeyRequest, - GetKeyArgs, GetKeyResponse, GetQuoteResponse, GetTlsKeyArgs, GetTlsKeyResponse, RawQuoteArgs, - SignRequest, SignResponse, TdxQuoteArgs, TdxQuoteResponse, VerifyRequest, VerifyResponse, - WorkerVersion, + AppInfo, AttestResponse, DeriveK256KeyResponse, DeriveKeyArgs, EmitEventArgs, + GetAttestationForAppKeyRequest, GetKeyArgs, GetKeyResponse, GetQuoteResponse, GetTlsKeyArgs, + GetTlsKeyResponse, RawQuoteArgs, SignRequest, SignResponse, TdxQuoteArgs, TdxQuoteResponse, + VerifyRequest, VerifyResponse, WorkerVersion, }; use dstack_types::{AppKeys, SysConfig}; use ed25519_dalek::ed25519::signature::hazmat::{PrehashSigner, PrehashVerifier}; @@ -26,7 +28,7 @@ use k256::ecdsa::SigningKey; use or_panic::ResultOrPanic; use ra_rpc::{Attestation, CallContext, RpcCall}; use ra_tls::{ - attestation::{QuoteContentType, DEFAULT_HASH_ALGORITHM}, + attestation::{QuoteContentType, VersionedAttestation, DEFAULT_HASH_ALGORITHM}, cert::CertConfig, kdf::{derive_ecdsa_key, derive_ecdsa_key_pair_from_bytes}, }; @@ -34,11 +36,16 @@ use rcgen::KeyPair; use ring::rand::{SecureRandom, SystemRandom}; use serde_json::json; use sha3::{Digest, Keccak256}; -use tdx_attest::eventlog::read_event_logs; use tracing::error; use crate::config::Config; +fn read_dmi_file(name: &str) -> String { + fs::read_to_string(format!("/sys/class/dmi/id/{name}")) + .map(|s| s.trim().to_string()) + .unwrap_or_default() +} + #[derive(Clone)] pub struct AppState { inner: Arc, @@ -53,8 +60,20 @@ struct AppStateInner { } impl AppStateInner { + fn simulator_attestation(&self) -> Result> { + if !self.config.simulator.enabled { + return Ok(None); + } + let attestation_bytes = fs::read(&self.config.simulator.attestation_file) + .context("Failed to read simulator attestation file")?; + let attestation = VersionedAttestation::from_scale(&attestation_bytes) + .context("Failed to decode simulator attestation")?; + Ok(Some(attestation)) + } + async fn request_demo_cert(&self) -> Result { let key = KeyPair::generate().context("Failed to generate demo key")?; + let attestation_override = self.simulator_attestation()?; let demo_cert = self .cert_client .request_cert( @@ -67,7 +86,7 @@ impl AppStateInner { usage_client_auth: true, ext_quote: true, }, - self.config.simulator.enabled, + attestation_override, ) .await .context("Failed to get app cert")? @@ -134,33 +153,42 @@ pub struct InternalRpcHandler { pub async fn get_info(state: &AppState, external: bool) -> Result { let hide_tcb_info = external && !state.config().app_compose.public_tcbinfo; - let response = InternalRpcHandler { - state: state.clone(), - } - .get_quote(RawQuoteArgs { - report_data: [0; 64].to_vec(), - }) - .await; - let Ok(response) = response else { - return Ok(AppInfo::default()); - }; - let Ok(attestation) = Attestation::new(response.quote, response.event_log.into()) else { - return Ok(AppInfo::default()); + let attestation = if let Some(attestation) = state.inner.simulator_attestation()? { + attestation.into_inner() + } else { + let Ok(attestation) = Attestation::local() else { + return Ok(AppInfo::default()); + }; + attestation }; let app_info = attestation .decode_app_info(false) .context("Failed to decode app info")?; - let event_log = &attestation.event_log; + let event_log = attestation + .tdx_quote + .as_ref() + .map(|q| &q.event_log[..]) + .unwrap_or_default(); let tcb_info = if hide_tcb_info { "".to_string() } else { let app_compose = state.config().app_compose.raw.clone(); + let td_report = match attestation.get_td10_report() { + Some(report) => json!({ + "mrtd": hex::encode(report.mr_td), + "rtmr0": hex::encode(report.rt_mr0), + "rtmr1": hex::encode(report.rt_mr1), + "rtmr2": hex::encode(report.rt_mr2), + "rtmr3": hex::encode(report.rt_mr3), + }), + None => json!({}), + }; serde_json::to_string_pretty(&json!({ - "mrtd": hex::encode(app_info.mrtd), - "rtmr0": hex::encode(app_info.rtmr0), - "rtmr1": hex::encode(app_info.rtmr1), - "rtmr2": hex::encode(app_info.rtmr2), - "rtmr3": hex::encode(app_info.rtmr3), + "mrtd": td_report["mrtd"], + "rtmr0": td_report["rtmr0"], + "rtmr1": td_report["rtmr1"], + "rtmr2": td_report["rtmr2"], + "rtmr3": td_report["rtmr3"], "mr_aggregated": hex::encode(app_info.mr_aggregated), "os_image_hash": hex::encode(&app_info.os_image_hash), "compose_hash": hex::encode(&app_info.compose_hash), @@ -193,6 +221,8 @@ pub async fn get_info(state: &AppState, external: bool) -> Result { .clone(), tcb_info, vm_config, + cloud_vendor: read_dmi_file("sys_vendor"), + cloud_product: read_dmi_file("product_name"), }) } @@ -212,11 +242,16 @@ impl DstackGuestRpc for InternalRpcHandler { usage_client_auth: request.usage_client_auth, ext_quote: request.usage_ra_tls, }; + let attestation_override = self + .state + .inner + .simulator_attestation() + .context("Failed to load simulator attestation")?; let certificate_chain = self .state .inner .cert_client - .request_cert(&derived_key, config, self.state.config().simulator.enabled) + .request_cert(&derived_key, config, attestation_override) .await .context("Failed to sign the CSR")?; Ok(GetTlsKeyResponse { @@ -268,14 +303,6 @@ impl DstackGuestRpc for InternalRpcHandler { } async fn get_quote(self, request: RawQuoteArgs) -> Result { - fn pad64(data: &[u8]) -> Option<[u8; 64]> { - if data.len() > 64 { - return None; - } - let mut padded = [0u8; 64]; - padded[..data.len()].copy_from_slice(data); - Some(padded) - } let report_data = pad64(&request.report_data).context("Report data is too long")?; if self.state.config().simulator.enabled { return simulate_quote( @@ -284,15 +311,12 @@ impl DstackGuestRpc for InternalRpcHandler { &self.state.inner.vm_config, ); } - let (_, quote) = - tdx_attest::get_quote(&report_data, None).context("Failed to get quote")?; - let event_log = read_event_logs().context("Failed to decode event log")?; - let event_log = - serde_json::to_string(&event_log).context("Failed to serialize event log")?; - + let attestation = Attestation::quote(&report_data).context("Failed to get quote")?; + let tdx_quote = attestation.get_tdx_quote_bytes(); + let tdx_event_log = attestation.get_tdx_event_log_string(); Ok(GetQuoteResponse { - quote, - event_log, + quote: tdx_quote.unwrap_or_default(), + event_log: tdx_event_log.unwrap_or_default(), report_data: report_data.to_vec(), vm_config: self.state.inner.vm_config.clone(), }) @@ -302,7 +326,7 @@ impl DstackGuestRpc for InternalRpcHandler { if self.state.config().simulator.enabled { return Ok(()); } - tdx_attest::extend_rtmr3(&request.event, &request.payload) + emit_runtime_event(&request.event, &request.payload) } async fn info(self) -> Result { @@ -394,6 +418,28 @@ impl DstackGuestRpc for InternalRpcHandler { }; Ok(VerifyResponse { valid }) } + + async fn attest(self, request: RawQuoteArgs) -> Result { + let report_data = pad64(&request.report_data).context("Report data is too long")?; + if let Some(attestation) = self.state.inner.simulator_attestation()? { + return Ok(AttestResponse { + attestation: attestation.to_scale(), + }); + } + let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; + Ok(AttestResponse { + attestation: attestation.into_versioned().to_scale(), + }) + } +} + +fn pad64(data: &[u8]) -> Option<[u8; 64]> { + if data.len() > 64 { + return None; + } + let mut padded = [0u8; 64]; + padded[..data.len()].copy_from_slice(data); + Some(padded) } fn simulate_quote( @@ -401,18 +447,20 @@ fn simulate_quote( report_data: [u8; 64], vm_config: &str, ) -> Result { - let quote_file = - fs::read_to_string(&config.simulator.quote_file).context("Failed to read quote file")?; - let mut quote = hex::decode(quote_file.trim()).context("Failed to decode quote")?; - let event_log = fs::read_to_string(&config.simulator.event_log_file) - .context("Failed to read event log file")?; - if quote.len() < 632 { - return Err(anyhow::anyhow!("Quote is too short")); - } - quote[568..632].copy_from_slice(&report_data); + let attestation_bytes = fs::read(&config.simulator.attestation_file) + .context("Failed to read simulator attestation file")?; + let VersionedAttestation::V0 { attestation } = + VersionedAttestation::from_scale(&attestation_bytes) + .context("Failed to decode simulator attestation")?; + let Some(mut quote) = attestation.tdx_quote else { + return Err(anyhow::anyhow!("Quote not found")); + }; + + quote.quote[568..632].copy_from_slice(&report_data); Ok(GetQuoteResponse { - quote, - event_log, + quote: quote.quote, + event_log: serde_json::to_string("e.event_log) + .context("Failed to serialize event log")?, report_data: report_data.to_vec(), vm_config: vm_config.to_string(), }) @@ -453,11 +501,16 @@ impl TappdRpc for InternalRpcHandlerV0 { usage_client_auth: request.usage_client_auth, ext_quote: request.usage_ra_tls, }; + let attestation_override = self + .state + .inner + .simulator_attestation() + .context("Failed to load simulator attestation")?; let certificate_chain = self .state .inner .cert_client - .request_cert(&derived_key, config, self.state.config().simulator.enabled) + .request_cert(&derived_key, config, attestation_override) .await .context("Failed to sign the CSR")?; Ok(GetTlsKeyResponse { @@ -507,11 +560,10 @@ impl TappdRpc for InternalRpcHandlerV0 { prefix, }); } - let event_log = read_event_logs().context("Failed to decode event log")?; + let event_log = read_event_log().context("Failed to decode event log")?; let event_log = serde_json::to_string(&event_log).context("Failed to serialize event log")?; - let (_, quote) = - tdx_attest::get_quote(&report_data, None).context("Failed to get quote")?; + let quote = tdx_attest::get_quote(&report_data).context("Failed to get quote")?; Ok(TdxQuoteResponse { quote, event_log, @@ -603,11 +655,10 @@ impl WorkerRpc for ExternalRpcHandler { &self.state.inner.vm_config, )?) } else { - let ed25519_quote = tdx_attest::get_quote(&ed25519_report_data, None) - .context("Failed to get ed25519 quote")? - .1; + let ed25519_quote = tdx_attest::get_quote(&ed25519_report_data) + .context("Failed to get ed25519 quote")?; let event_log = serde_json::to_string( - &read_event_logs().context("Failed to read event log")?, + &read_event_log().context("Failed to read event log")?, )?; Ok(GetQuoteResponse { quote: ed25519_quote, @@ -635,11 +686,10 @@ impl WorkerRpc for ExternalRpcHandler { &self.state.inner.vm_config, )?) } else { - let secp256k1_quote = tdx_attest::get_quote(&secp256k1_report_data, None) - .context("Failed to get secp256k1 quote")? - .1; + let secp256k1_quote = tdx_attest::get_quote(&secp256k1_report_data) + .context("Failed to get secp256k1 quote")?; let event_log = serde_json::to_string( - &read_event_logs().context("Failed to read event log")?, + &read_event_log().context("Failed to read event log")?, )?; Ok(GetQuoteResponse { @@ -697,18 +747,16 @@ mod tests { } } - async fn setup_test_state() -> (AppState, tempfile::NamedTempFile, tempfile::NamedTempFile) { - let mut dummy_quote_file = tempfile::NamedTempFile::new().unwrap(); - let dummy_event_log_file = tempfile::NamedTempFile::new().unwrap(); + async fn setup_test_state() -> (AppState, tempfile::NamedTempFile) { + let mut temp_attestation_file = tempfile::NamedTempFile::new().unwrap(); - let dummy_quote = vec![b'0'; 10020]; - dummy_quote_file.write_all(&dummy_quote).unwrap(); - dummy_quote_file.flush().unwrap(); + let attestation = include_bytes!("../fixtures/attestation.bin"); + temp_attestation_file.write_all(attestation).unwrap(); + temp_attestation_file.flush().unwrap(); let dummy_simulator = Simulator { enabled: true, - quote_file: dummy_quote_file.path().to_str().unwrap().to_string(), - event_log_file: dummy_event_log_file.path().to_str().unwrap().to_string(), + attestation_file: temp_attestation_file.path().to_str().unwrap().to_string(), }; let dummy_appcompose = AppCompose { @@ -831,14 +879,13 @@ pNs85uhOZE8z2jr8Pg== AppState { inner: Arc::new(inner), }, - dummy_quote_file, - dummy_event_log_file, + temp_attestation_file, ) } #[tokio::test] async fn test_verify_ed25519_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -865,7 +912,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_verify_secp256k1_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -892,7 +939,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_ed25519_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -922,7 +969,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_secp256k1_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -954,7 +1001,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_secp256k1_prehashed_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -991,7 +1038,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_secp256k1_prehashed_invalid_length_fails() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -1014,7 +1061,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_unsupported_algorithm_fails() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state }; let request = SignRequest { algorithm: "rsa".to_string(), // Unsupported algorithm @@ -1028,7 +1075,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_get_attestation_for_app_key_ed25519_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = ExternalRpcHandler::new(state.clone()); let request = GetAttestationForAppKeyRequest { algorithm: "ed25519".to_string(), @@ -1043,7 +1090,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_get_attestation_for_app_key_secp256k1_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = ExternalRpcHandler::new(state.clone()); let request = GetAttestationForAppKeyRequest { algorithm: "secp256k1".to_string(), @@ -1058,7 +1105,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_get_attestation_for_app_key_unsupported_algorithm_fails() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = ExternalRpcHandler::new(state); let request = GetAttestationForAppKeyRequest { algorithm: "ecdsa".to_string(), // Unsupported algorithm diff --git a/guest-agent/templates/dashboard.html b/guest-agent/templates/dashboard.html index a81e3a45..212df06a 100644 --- a/guest-agent/templates/dashboard.html +++ b/guest-agent/templates/dashboard.html @@ -168,6 +168,12 @@

Node Information

Key Provider Info
{{key_provider_info}}
+ {% if !cloud_vendor.is_empty() || !cloud_product.is_empty() %} +
+
Machine Provider
+
{{cloud_vendor}} ({{cloud_product}})
+
+ {% endif %} {% if public_sysinfo %}
Operating System
diff --git a/kms/Cargo.toml b/kms/Cargo.toml index 3a08c748..bc33bc6a 100644 --- a/kms/Cargo.toml +++ b/kms/Cargo.toml @@ -45,6 +45,7 @@ dstack-types.workspace = true tokio = { workspace = true, features = ["process"] } tempfile.workspace = true serde-duration.workspace = true +dstack-verifier = { workspace = true, default-features = false } dstack-mr.workspace = true [features] diff --git a/kms/dstack-app/builder/Dockerfile b/kms/dstack-app/builder/Dockerfile index e9c9448b..d6201535 100644 --- a/kms/dstack-app/builder/Dockerfile +++ b/kms/dstack-app/builder/Dockerfile @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -FROM rust:1.86.0@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 AS kms-builder +FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS kms-builder COPY ./shared /build ARG DSTACK_REV ARG DSTACK_SRC_URL=https://github.com/Dstack-TEE/dstack.git diff --git a/kms/dstack-app/docker-compose.yaml b/kms/dstack-app/docker-compose.yaml index 43fe43ed..3d8a18f5 100644 --- a/kms/dstack-app/docker-compose.yaml +++ b/kms/dstack-app/docker-compose.yaml @@ -8,7 +8,7 @@ services: build: context: . dockerfile_inline: | - FROM rust:1.86-alpine@sha256:661d708cc863ce32007cf46807a72062a80d2944a6fae9e0d83742d2e04d5375 + FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 WORKDIR /app RUN apk add --no-cache git build-base openssl-dev protobuf protobuf-dev perl RUN git clone https://github.com/a16z/helios && \ @@ -57,7 +57,7 @@ services: build: context: . dockerfile_inline: | - FROM rust:1.86.0@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 + FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 WORKDIR /app RUN apt-get update && apt-get install -y \ git \ diff --git a/kms/rpc/proto/kms_rpc.proto b/kms/rpc/proto/kms_rpc.proto index baac1911..00815121 100644 --- a/kms/rpc/proto/kms_rpc.proto +++ b/kms/rpc/proto/kms_rpc.proto @@ -115,8 +115,7 @@ message BootstrapRequest { message BootstrapResponse { bytes ca_pubkey = 1; bytes k256_pubkey = 2; - bytes quote = 3; - bytes eventlog = 4; + bytes attestation = 3; } message OnboardRequest { diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 66fbf87b..24b9e06d 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -2,11 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -use std::{ - ffi::OsStr, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{path::PathBuf, sync::Arc}; use anyhow::{bail, Context, Result}; use dstack_kms_rpc::{ @@ -16,18 +12,17 @@ use dstack_kms_rpc::{ SignCertRequest, SignCertResponse, }; use dstack_types::VmConfig; +use dstack_verifier::{CvmVerifier, VerificationDetails}; use fs_err as fs; use k256::ecdsa::SigningKey; -use ra_rpc::{Attestation, CallContext, RpcCall}; +use ra_rpc::{CallContext, RpcCall}; use ra_tls::{ attestation::VerifiedAttestation, - cert::{CaCert, CertRequest, CertSigningRequest}, + cert::{CaCert, CertRequest, CertSigningRequestV1, CertSigningRequestV2, Csr}, kdf, }; use scale::Decode; -use serde::{Deserialize, Serialize}; use sha2::Digest; -use tokio::{io::AsyncWriteExt, process::Command}; use tracing::{debug, info}; use upgrade_authority::BootInfo; @@ -57,6 +52,7 @@ pub struct KmsStateInner { k256_key: SigningKey, temp_ca_cert: String, temp_ca_key: String, + verifier: CvmVerifier, } impl KmsState { @@ -70,6 +66,11 @@ impl KmsState { fs::read_to_string(config.tmp_ca_key()).context("Faeild to read temp ca key")?; let temp_ca_cert = fs::read_to_string(config.tmp_ca_cert()).context("Faeild to read temp ca cert")?; + let verifier = CvmVerifier::new( + config.image.cache_dir.display().to_string(), + config.image.download_url.clone(), + config.image.download_timeout, + ); Ok(Self { inner: Arc::new(KmsStateInner { config, @@ -77,6 +78,7 @@ impl KmsState { k256_key, temp_ca_cert, temp_ca_key, + verifier, }), }) } @@ -93,49 +95,6 @@ struct BootConfig { os_image_hash: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] -struct Mrs { - mrtd: String, - rtmr0: String, - rtmr1: String, - rtmr2: String, -} - -impl Mrs { - fn assert_eq(&self, other: &Self) -> Result<()> { - let Self { - mrtd, - rtmr0, - rtmr1, - rtmr2, - } = self; - if mrtd != &other.mrtd { - bail!("MRTD does not match"); - } - if rtmr0 != &other.rtmr0 { - bail!("RTMR0 does not match"); - } - if rtmr1 != &other.rtmr1 { - bail!("RTMR1 does not match"); - } - if rtmr2 != &other.rtmr2 { - bail!("RTMR2 does not match"); - } - Ok(()) - } -} - -impl From<&BootInfo> for Mrs { - fn from(report: &BootInfo) -> Self { - Self { - mrtd: hex::encode(&report.mrtd), - rtmr0: hex::encode(&report.rtmr0), - rtmr1: hex::encode(&report.rtmr1), - rtmr2: hex::encode(&report.rtmr2), - } - } -} - impl RpcHandler { fn ensure_attested(&self) -> Result<&VerifiedAttestation> { let Some(attestation) = &self.attestation else { @@ -161,10 +120,6 @@ impl RpcHandler { self.state.config.image.cache_dir.join("images") } - fn mr_cache_dir(&self) -> PathBuf { - self.state.config.image.cache_dir.join("computed") - } - fn remove_cache(&self, parent_dir: &PathBuf, sub_dir: &str) -> Result<()> { if sub_dir.is_empty() { return Ok(()); @@ -190,236 +145,21 @@ impl RpcHandler { Ok(()) } - fn get_cached_mrs(&self, key: &str) -> Result { - let path = self.mr_cache_dir().join(key); - if !path.exists() { - bail!("Cached MRs not found"); - } - let content = fs::read_to_string(path).context("Failed to read cached MRs")?; - let cached_mrs: Mrs = - serde_json::from_str(&content).context("Failed to parse cached MRs")?; - Ok(cached_mrs) - } - - fn cache_mrs(&self, key: &str, mrs: &Mrs) -> Result<()> { - let path = self.mr_cache_dir().join(key); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).context("Failed to create cache directory")?; - } - safe_write::safe_write( - &path, - serde_json::to_string(mrs).context("Failed to serialize cached MRs")?, - ) - .context("Failed to write cached MRs")?; - Ok(()) - } - - async fn verify_os_image_hash(&self, vm_config: &VmConfig, report: &BootInfo) -> Result<()> { + async fn verify_os_image_hash( + &self, + vm_config: String, + report: &VerifiedAttestation, + ) -> Result<()> { if !self.state.config.image.verify { info!("Image verification is disabled"); return Ok(()); } - let hex_os_image_hash = hex::encode(&vm_config.os_image_hash); - info!("Verifying image {hex_os_image_hash}"); - - let verified_mrs: Mrs = report.into(); - - let cache_key = { - let vm_config = - serde_json::to_vec(vm_config).context("Failed to serialize VM config")?; - hex::encode(sha2::Sha256::new_with_prefix(&vm_config).finalize()) - }; - if let Ok(cached_mrs) = self.get_cached_mrs(&cache_key) { - cached_mrs - .assert_eq(&verified_mrs) - .context("MRs do not match (cached)")?; - return Ok(()); - } - - // Create a directory for the image if it doesn't exist - let image_dir = self.image_cache_dir().join(&hex_os_image_hash); - // Check if metadata.json exists, if not download the image - let metadata_path = image_dir.join("metadata.json"); - if !metadata_path.exists() { - info!("Image {} not found, downloading", hex_os_image_hash); - tokio::time::timeout( - self.state.config.image.download_timeout, - self.download_image(&hex_os_image_hash, &image_dir), - ) - .await - .context("Download image timeout")? - .with_context(|| format!("Failed to download image {hex_os_image_hash}"))?; - } - - let image_info = - fs::read_to_string(metadata_path).context("Failed to read image metadata")?; - let image_info: dstack_types::ImageInfo = - serde_json::from_str(&image_info).context("Failed to parse image metadata")?; - - let fw_path = image_dir.join(&image_info.bios); - let kernel_path = image_dir.join(&image_info.kernel); - let initrd_path = image_dir.join(&image_info.initrd); - let kernel_cmdline = image_info.cmdline + " initrd=initrd"; - - let mrs = dstack_mr::Machine::builder() - .cpu_count(vm_config.cpu_count) - .memory_size(vm_config.memory_size) - .firmware(&fw_path.display().to_string()) - .kernel(&kernel_path.display().to_string()) - .initrd(&initrd_path.display().to_string()) - .kernel_cmdline(&kernel_cmdline) - .root_verity(true) - .hotplug_off(vm_config.hotplug_off) - .maybe_two_pass_add_pages(vm_config.qemu_single_pass_add_pages) - .maybe_pic(vm_config.pic) - .maybe_qemu_version(vm_config.qemu_version.clone()) - .maybe_pci_hole64_size(if vm_config.pci_hole64_size > 0 { - Some(vm_config.pci_hole64_size) - } else { - None - }) - .hugepages(vm_config.hugepages) - .num_gpus(vm_config.num_gpus) - .num_nvswitches(vm_config.num_nvswitches) - .build() - .measure() - .context("Failed to compute expected MRs")?; - - let expected_mrs: Mrs = Mrs { - mrtd: hex::encode(&mrs.mrtd), - rtmr0: hex::encode(&mrs.rtmr0), - rtmr1: hex::encode(&mrs.rtmr1), - rtmr2: hex::encode(&mrs.rtmr2), - }; - self.cache_mrs(&cache_key, &expected_mrs) - .context("Failed to cache MRs")?; - expected_mrs - .assert_eq(&verified_mrs) - .context("MRs do not match")?; - Ok(()) - } - - async fn download_image(&self, hex_os_image_hash: &str, dst_dir: &Path) -> Result<()> { - // Create a hex representation of the os_image_hash for URL and directory naming - let url = self - .state - .config - .image - .download_url - .replace("{OS_IMAGE_HASH}", hex_os_image_hash); - - // Create a temporary directory for extraction within the cache directory - let cache_dir = self.image_cache_dir().join("tmp"); - fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?; - let auto_delete_temp_dir = tempfile::Builder::new() - .prefix("tmp-download-") - .tempdir_in(&cache_dir) - .context("Failed to create temporary directory")?; - let tmp_dir = auto_delete_temp_dir.path(); - // Download the image tarball - info!("Downloading image from {}", url); - let client = reqwest::Client::new(); - let response = client - .get(&url) - .send() - .await - .context("Failed to download image")?; - - if !response.status().is_success() { - bail!( - "Failed to download image: HTTP status {}, url: {url}", - response.status(), - ); - } - - // Save the tarball to a temporary file using streaming - let tarball_path = tmp_dir.join("image.tar.gz"); - let mut file = tokio::fs::File::create(&tarball_path) + let mut detail = VerificationDetails::default(); + self.state + .verifier + .verify_os_image_hash(vm_config, report, false, &mut detail) .await - .context("Failed to create tarball file")?; - let mut response = response; - while let Some(chunk) = response.chunk().await? { - file.write_all(&chunk) - .await - .context("Failed to write chunk to file")?; - } - - let extracted_dir = tmp_dir.join("extracted"); - fs::create_dir_all(&extracted_dir).context("Failed to create extraction directory")?; - - // Extract the tarball - let output = Command::new("tar") - .arg("xzf") - .arg(&tarball_path) - .current_dir(&extracted_dir) - .output() - .await - .context("Failed to extract tarball")?; - - if !output.status.success() { - bail!( - "Failed to extract tarball: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - // Verify checksum - let output = Command::new("sha256sum") - .arg("-c") - .arg("sha256sum.txt") - .current_dir(&extracted_dir) - .output() - .await - .context("Failed to verify checksum")?; - - if !output.status.success() { - bail!( - "Checksum verification failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - // Remove the files that are not listed in sha256sum.txt - let sha256sum_path = extracted_dir.join("sha256sum.txt"); - let files_doc = - fs::read_to_string(&sha256sum_path).context("Failed to read sha256sum.txt")?; - let listed_files: Vec<&OsStr> = files_doc - .lines() - .flat_map(|line| line.split_whitespace().nth(1)) - .map(|s| s.as_ref()) - .collect(); - let files = fs::read_dir(&extracted_dir).context("Failed to read directory")?; - for file in files { - let file = file.context("Failed to read directory entry")?; - let filename = file.file_name(); - if !listed_files.contains(&filename.as_os_str()) { - if file.path().is_dir() { - fs::remove_dir_all(file.path()).context("Failed to remove directory")?; - } else { - fs::remove_file(file.path()).context("Failed to remove file")?; - } - } - } - - // os_image_hash should eq to sha256sum of the sha256sum.txt - let os_image_hash = sha2::Sha256::new_with_prefix(files_doc.as_bytes()).finalize(); - if hex::encode(os_image_hash) != hex_os_image_hash { - bail!("os_image_hash does not match sha256sum of the sha256sum.txt"); - } - - // Move the extracted files to the destination directory - let metadata_path = extracted_dir.join("metadata.json"); - if !metadata_path.exists() { - bail!("metadata.json not found in the extracted archive"); - } - - if dst_dir.exists() { - fs::remove_dir_all(dst_dir).context("Failed to remove destination directory")?; - } - let dst_dir_parent = dst_dir.parent().context("Failed to get parent directory")?; - fs::create_dir_all(dst_dir_parent).context("Failed to create parent directory")?; - // Move the extracted files to the destination directory - fs::rename(extracted_dir, dst_dir) - .context("Failed to move extracted files to destination directory")?; + .context("Failed to verify os image hash")?; Ok(()) } @@ -428,24 +168,17 @@ impl RpcHandler { att: &VerifiedAttestation, is_kms: bool, use_boottime_mr: bool, - vm_config: &str, + vm_config_str: &str, ) -> Result { - let report = att - .report - .report - .as_td10() - .context("Failed to decode TD report")?; - let app_info = att.decode_app_info(use_boottime_mr)?; - debug!("vm_config: {vm_config}"); + let Some(tdx_report) = &att.report.tdx_report else { + bail!("No TD report in attestation"); + }; + debug!("vm_config: {vm_config_str}"); let vm_config: VmConfig = - serde_json::from_str(vm_config).context("Failed to decode VM config")?; + serde_json::from_str(vm_config_str).context("Failed to decode VM config")?; + let app_info = att.decode_app_info(use_boottime_mr)?; let os_image_hash = vm_config.os_image_hash.clone(); let boot_info = BootInfo { - mrtd: report.mr_td.to_vec(), - rtmr0: report.rt_mr0.to_vec(), - rtmr1: report.rt_mr1.to_vec(), - rtmr2: report.rt_mr2.to_vec(), - rtmr3: report.rt_mr3.to_vec(), mr_aggregated: app_info.mr_aggregated.to_vec(), os_image_hash: os_image_hash.clone(), mr_system: app_info.mr_system.to_vec(), @@ -454,10 +187,8 @@ impl RpcHandler { instance_id: app_info.instance_id, device_id: app_info.device_id, key_provider_info: app_info.key_provider_info, - event_log: String::from_utf8(att.raw_event_log.clone()) - .context("Failed to serialize event log")?, - tcb_status: att.report.status.clone(), - advisory_ids: att.report.advisory_ids.clone(), + tcb_status: tdx_report.status.clone(), + advisory_ids: tdx_report.advisory_ids.clone(), }; let response = self .state @@ -468,7 +199,7 @@ impl RpcHandler { if !response.is_allowed { bail!("Boot denied: {}", response.reason); } - self.verify_os_image_hash(&vm_config, &boot_info) + self.verify_os_image_hash(vm_config_str.into(), att) .await .context("Failed to verify os image hash")?; Ok(BootConfig { @@ -614,15 +345,27 @@ impl KmsRpc for RpcHandler { } async fn sign_cert(self, request: SignCertRequest) -> Result { - if request.api_version > 1 { - bail!("Unsupported API version: {}", request.api_version); - } - let csr = - CertSigningRequest::decode(&mut &request.csr[..]).context("Failed to parse csr")?; - csr.verify(&request.signature) - .context("Failed to verify csr signature")?; - let attestation = Attestation::new(csr.quote.clone(), csr.event_log.clone()) - .context("Failed to create attestation from quote and event log")? + let csr = match request.api_version { + 1 => { + let csr = CertSigningRequestV1::decode(&mut &request.csr[..]) + .context("Failed to parse csr")?; + csr.verify(&request.signature) + .context("Failed to verify csr signature")?; + csr.try_into().context("Failed to upgrade csr v1 to v2")? + } + 2 => { + let csr = CertSigningRequestV2::decode(&mut &request.csr[..]) + .context("Failed to parse csr")?; + csr.verify(&request.signature) + .context("Failed to verify csr signature")?; + csr + } + _ => bail!("Unsupported API version: {}", request.api_version), + }; + let attestation = csr + .attestation + .clone() + .into_inner() .verify_with_ra_pubkey(&csr.pubkey, self.state.config.pccs_url.as_deref()) .await .context("Quote verification failed")?; @@ -646,8 +389,10 @@ impl KmsRpc for RpcHandler { self.ensure_admin(&request.token)?; self.remove_cache(&self.image_cache_dir(), &request.image_hash) .context("Failed to clear image cache")?; - self.remove_cache(&self.mr_cache_dir(), &request.config_hash) - .context("Failed to clear MR cache")?; + // Clear measurement cache (now handled by verifier's cache in measurements/ dir) + let mr_cache_dir = self.state.config.image.cache_dir.join("measurements"); + self.remove_cache(&mr_cache_dir, &request.config_hash) + .context("Failed to clear measurement cache")?; Ok(()) } } diff --git a/kms/src/main_service/upgrade_authority.rs b/kms/src/main_service/upgrade_authority.rs index 5db8a47d..b3dd2db3 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -10,16 +10,6 @@ use serde_human_bytes as hex_bytes; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct BootInfo { - #[serde(with = "hex_bytes")] - pub mrtd: Vec, - #[serde(with = "hex_bytes")] - pub rtmr0: Vec, - #[serde(with = "hex_bytes")] - pub rtmr1: Vec, - #[serde(with = "hex_bytes")] - pub rtmr2: Vec, - #[serde(with = "hex_bytes")] - pub rtmr3: Vec, #[serde(with = "hex_bytes")] pub mr_aggregated: Vec, #[serde(with = "hex_bytes")] @@ -36,7 +26,6 @@ pub(crate) struct BootInfo { pub device_id: Vec, #[serde(with = "hex_bytes")] pub key_provider_info: Vec, - pub event_log: String, pub tcb_status: String, pub advisory_ids: Vec, } diff --git a/kms/src/onboard_service.rs b/kms/src/onboard_service.rs index ffe38f16..4a4107fd 100644 --- a/kms/src/onboard_service.rs +++ b/kms/src/onboard_service.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use dstack_guest_agent_rpc::{ - dstack_guest_client::DstackGuestClient, GetQuoteResponse, RawQuoteArgs, + dstack_guest_client::DstackGuestClient, AttestResponse, RawQuoteArgs, }; use dstack_kms_rpc::{ kms_client::KmsClient, @@ -16,7 +16,7 @@ use http_client::prpc::PrpcClient; use k256::ecdsa::SigningKey; use ra_rpc::{client::RaClient, CallContext, RpcCall}; use ra_tls::{ - attestation::QuoteContentType, + attestation::{QuoteContentType, VersionedAttestation}, cert::{CaCert, CertRequest}, rcgen::{Certificate, KeyPair, PKCS_ECDSA_P256_SHA256}, }; @@ -58,21 +58,17 @@ impl OnboardRpc for OnboardHandler { let k256_pubkey = keys.k256_key.verifying_key().to_sec1_bytes().to_vec(); let ca_pubkey = keys.ca_key.public_key_der(); - let quote; - let eventlog; - if quote_enabled { - (quote, eventlog) = quote_keys(&ca_pubkey, &k256_pubkey).await?; + let attestation = if quote_enabled { + Some(attest_keys(&ca_pubkey, &k256_pubkey).await?) } else { - quote = vec![]; - eventlog = vec![]; + None }; let cfg = &self.state.config; let response = BootstrapResponse { ca_pubkey, k256_pubkey, - quote, - eventlog, + attestation: attestation.unwrap_or_default(), }; // Store the bootstrap info safe_write(cfg.bootstrap_info(), serde_json::to_vec(&response)?)?; @@ -143,18 +139,17 @@ impl Keys { .key(&ca_key) .build() .self_signed()?; - - let mut quote = None; - let mut event_log = None; - - if quote_enabled { + let attestation = if quote_enabled { let pubkey = rpc_key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let resposne = app_quote(report_data.to_vec()) + let response = app_attest(report_data.to_vec()) .await .context("Failed to get quote")?; - quote = Some(resposne.quote); - event_log = Some(resposne.event_log.into_bytes()); + let attestation = VersionedAttestation::from_scale(&response.attestation) + .context("Invalid attestation")?; + Some(attestation) + } else { + None }; // Sign WWW server cert with KMS cert @@ -162,8 +157,7 @@ impl Keys { .subject(domain) .alt_names(&[domain.to_string()]) .special_usage("kms:rpc") - .maybe_quote(quote.as_deref()) - .maybe_event_log(event_log.as_deref()) + .maybe_attestation(attestation.as_ref()) .key(&rpc_key) .build() .signed_by(&ca_cert, &ca_key)?; @@ -302,21 +296,18 @@ fn dstack_client() -> DstackGuestClient { DstackGuestClient::new(http_client) } -async fn app_quote(report_data: Vec) -> Result { - let quote = dstack_client() - .get_quote(RawQuoteArgs { report_data }) - .await?; - Ok(quote) +async fn app_attest(report_data: Vec) -> Result { + dstack_client().attest(RawQuoteArgs { report_data }).await } -async fn quote_keys(p256_pubkey: &[u8], k256_pubkey: &[u8]) -> Result<(Vec, Vec)> { +async fn attest_keys(p256_pubkey: &[u8], k256_pubkey: &[u8]) -> Result> { let p256_hex = hex::encode(p256_pubkey); let k256_hex = hex::encode(k256_pubkey); let content_to_quote = format!("dstack-kms-genereted-keys-v1:{p256_hex};{k256_hex};"); let hash = keccak256(content_to_quote.as_bytes()); let report_data = pad64(hash); - let res = app_quote(report_data).await?; - Ok((res.quote, res.event_log.into())) + let res = app_attest(report_data).await?; + Ok(res.attestation) } fn keccak256(msg: &[u8]) -> [u8; 32] { @@ -338,19 +329,17 @@ async fn gen_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result<(String, use ra_tls::rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}; let ca = CaCert::new(ca_cert_pem, ca_key_pem)?; - let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let pubkey = key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let quote_res = app_quote(report_data.to_vec()) + let response = app_attest(report_data.to_vec()) .await .context("Failed to get quote")?; - let quote = quote_res.quote; - let event_log: Vec = quote_res.event_log.into(); + let attestation = + VersionedAttestation::from_scale(&response.attestation).context("Invalid attestation")?; let req = CertRequest::builder() .subject("RA-TLS TEMP Cert") - .quote("e) - .event_log(&event_log) + .attestation(&attestation) .key(&key) .build(); let cert = ca.sign(req).context("Failed to sign certificate")?; diff --git a/mod-tdx-guest/Kconfig b/mod-tdx-guest/Kconfig deleted file mode 100644 index 22dd59e1..00000000 --- a/mod-tdx-guest/Kconfig +++ /dev/null @@ -1,11 +0,0 @@ -config TDX_GUEST_DRIVER - tristate "TDX Guest driver" - depends on INTEL_TDX_GUEST - select TSM_REPORTS - help - The driver provides userspace interface to communicate with - the TDX module to request the TDX guest details like attestation - report. - - To compile this driver as module, choose M here. The module will - be called tdx-guest. diff --git a/mod-tdx-guest/Makefile b/mod-tdx-guest/Makefile deleted file mode 100644 index a9a17e21..00000000 --- a/mod-tdx-guest/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# SPDX-FileCopyrightText: © 2022 Intel Corporation -# SPDX-FileCopyrightText: © 2024 Phala Network - -SRC := $(shell pwd) -KERNEL_SRC ?= /lib/modules/$(shell uname -r)/build -INSTALL_MOD_PATH := $(shell pwd)/dist/ - -obj-m += tdx-guest.o -tdx-guest-objs := tdcall.o mod.o - -all: - $(MAKE) -C $(KERNEL_SRC) M=$(SRC) - -modules_install: - $(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install - -clean: - $(MAKE) -C $(KERNEL_SRC) M=$(SRC) clean - -install: - make -C $(KDIR) M=$(PWD) modules_install INSTALL_MOD_PATH=$(INSTALL_MOD_PATH) diff --git a/mod-tdx-guest/mod.c b/mod-tdx-guest/mod.c deleted file mode 100644 index 98b865d1..00000000 --- a/mod-tdx-guest/mod.c +++ /dev/null @@ -1,184 +0,0 @@ -// SPDX-FileCopyrightText: © 2022 Intel Corporation -// SPDX-FileCopyrightText: © 2024 Phala Network -// SPDX-License-Identifier: GPL-2.0-only -/* - * TDX guest user interface driver - * - * Copyright (C) 2022 Intel Corporation - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "tdx-guest.h" -#include "tdx.h" - -#define TDCALL_RETURN_CODE(a) ((a) >> 32) -#define TDCALL_INVALID_OPERAND 0xc0000100 - -#define TDREPORT_SUBTYPE_0 0 - -static int tdx_mcall_get_report0(u8 *reportdata, u8 *tdreport) -{ - struct tdx_module_args args = { - .rcx = virt_to_phys(tdreport), - .rdx = virt_to_phys(reportdata), - .r8 = TDREPORT_SUBTYPE_0, - }; - u64 ret; - - ret = __tdcall(TDG_MR_REPORT, &args); - if (ret) { - if (TDCALL_RETURN_CODE(ret) == TDCALL_INVALID_OPERAND) - return -EINVAL; - return -EIO; - } - - return 0; -} - -static long tdx_get_report0(struct tdx_report_req __user *req) -{ - u8 *reportdata, *tdreport; - long ret; - - reportdata = kmalloc(TDX_REPORTDATA_LEN, GFP_KERNEL); - if (!reportdata) - return -ENOMEM; - - tdreport = kzalloc(TDX_REPORT_LEN, GFP_KERNEL); - if (!tdreport) - { - ret = -ENOMEM; - goto out; - } - - if (copy_from_user(reportdata, req->reportdata, TDX_REPORTDATA_LEN)) - { - ret = -EFAULT; - goto out; - } - - /* Generate TDREPORT0 using "TDG.MR.REPORT" TDCALL */ - ret = tdx_mcall_get_report0(reportdata, tdreport); - if (ret) - goto out; - - if (copy_to_user(req->tdreport, tdreport, TDX_REPORT_LEN)) - ret = -EFAULT; - -out: - kfree(reportdata); - kfree(tdreport); - - return ret; -} - -static long tdx_extend_rtmr(struct tdx_extend_rtmr_req __user *req) -{ - u8 *data; - u8 index; - long ret; - - data = kmalloc(TDX_EXTEND_RTMR_DATA_LEN, GFP_KERNEL); - if (!data) - return -ENOMEM; - - if (copy_from_user(data, req->data, TDX_EXTEND_RTMR_DATA_LEN)) - { - ret = -EFAULT; - goto out; - } - - if (copy_from_user(&index, (u8 __user *)&req->index, 1)) - { - ret = -EFAULT; - goto out; - } - - { - struct tdx_module_args args = { - .rcx = virt_to_phys(data), - .rdx = index, - }; - - ret = __tdcall(TDG_MR_RTMR_EXTEND, &args); - } -out: - kfree(data); - return ret; -} - -static long tdx_guest_ioctl(struct file *file, unsigned int cmd, - unsigned long arg) -{ - switch (cmd) - { - case TDX_CMD_GET_REPORT0: - return tdx_get_report0((struct tdx_report_req __user *)arg); - case TDX_CMD_EXTEND_RTMR: - case TDX_CMD_EXTEND_RTMR2: - return tdx_extend_rtmr((struct tdx_extend_rtmr_req __user *)arg); - default: - return -ENOTTY; - } -} - -static const struct file_operations tdx_guest_fops = { - .owner = THIS_MODULE, - .unlocked_ioctl = tdx_guest_ioctl, - .llseek = no_llseek, -}; - -static struct miscdevice tdx_misc_dev = { - .name = KBUILD_MODNAME, - .minor = MISC_DYNAMIC_MINOR, - .fops = &tdx_guest_fops, -}; - -static const struct x86_cpu_id tdx_guest_ids[] = { - X86_MATCH_FEATURE(X86_FEATURE_TDX_GUEST, NULL), - {} -}; -MODULE_DEVICE_TABLE(x86cpu, tdx_guest_ids); - -static int __init tdx_guest_init(void) -{ - int ret; - - if (!x86_match_cpu(tdx_guest_ids)) - return -ENODEV; - - ret = misc_register(&tdx_misc_dev); - if (ret) - return ret; - - - return 0; - - misc_deregister(&tdx_misc_dev); - - return ret; -} -module_init(tdx_guest_init); - -static void __exit tdx_guest_exit(void) -{ - misc_deregister(&tdx_misc_dev); -} -module_exit(tdx_guest_exit); - -MODULE_AUTHOR("Kuppuswamy Sathyanarayanan , kvinwang"); -MODULE_DESCRIPTION("TDX Guest Driver"); -MODULE_LICENSE("GPL"); diff --git a/mod-tdx-guest/tdcall.S b/mod-tdx-guest/tdcall.S deleted file mode 100644 index a9852869..00000000 --- a/mod-tdx-guest/tdcall.S +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2023, Intel Inc - * SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note - */ -#include -#include - -#include -#include - -#include -#include - -/* - * TDCALL and SEAMCALL are supported in Binutils >= 2.36. - */ -#define tdcall .byte 0x66,0x0f,0x01,0xcc -#define seamcall .byte 0x66,0x0f,0x01,0xcf - -/* - * TDX_MODULE_CALL - common helper macro for both - * TDCALL and SEAMCALL instructions. - * - * TDCALL - used by TDX guests to make requests to the - * TDX module and hypercalls to the VMM. - * SEAMCALL - used by TDX hosts to make requests to the - * TDX module. - * - *------------------------------------------------------------------------- - * TDCALL/SEAMCALL ABI: - *------------------------------------------------------------------------- - * Input Registers: - * - * RAX - TDCALL/SEAMCALL Leaf number. - * RCX,RDX,RDI,RSI,RBX,R8-R15 - TDCALL/SEAMCALL Leaf specific input registers. - * - * Output Registers: - * - * RAX - TDCALL/SEAMCALL instruction error code. - * RCX,RDX,RDI,RSI,RBX,R8-R15 - TDCALL/SEAMCALL Leaf specific output registers. - * - *------------------------------------------------------------------------- - * - * So while the common core (RAX,RCX,RDX,R8-R11) fits nicely in the - * callee-clobbered registers and even leaves RDI,RSI free to act as a - * base pointer, some leafs (e.g., VP.ENTER) make a giant mess of things. - * - * For simplicity, assume that anything that needs the callee-saved regs - * also tramples on RDI,RSI. This isn't strictly true, see for example - * TDH.EXPORT.MEM. - */ -.macro TDX_MODULE_CALL host:req ret=0 - FRAME_BEGIN - - /* Move Leaf ID to RAX */ - mov %rdi, %rax - - /* Move other input regs from 'struct tdx_module_args' */ - movq TDX_MODULE_rcx(%rsi), %rcx - movq TDX_MODULE_rdx(%rsi), %rdx - movq TDX_MODULE_r8(%rsi), %r8 - movq TDX_MODULE_r9(%rsi), %r9 - movq TDX_MODULE_r10(%rsi), %r10 - movq TDX_MODULE_r11(%rsi), %r11 - -.if \host -.Lseamcall\@: - seamcall - /* - * SEAMCALL instruction is essentially a VMExit from VMX root - * mode to SEAM VMX root mode. VMfailInvalid (CF=1) indicates - * that the targeted SEAM firmware is not loaded or disabled, - * or P-SEAMLDR is busy with another SEAMCALL. %rax is not - * changed in this case. - * - * Set %rax to TDX_SEAMCALL_VMFAILINVALID for VMfailInvalid. - * This value will never be used as actual SEAMCALL error code as - * it is from the Reserved status code class. - */ - jc .Lseamcall_vmfailinvalid\@ -.else - tdcall -.endif - -.if \ret - /* Copy output registers to the structure */ - movq %rcx, TDX_MODULE_rcx(%rsi) - movq %rdx, TDX_MODULE_rdx(%rsi) - movq %r8, TDX_MODULE_r8(%rsi) - movq %r9, TDX_MODULE_r9(%rsi) - movq %r10, TDX_MODULE_r10(%rsi) - movq %r11, TDX_MODULE_r11(%rsi) -.endif /* \ret */ - -.if \host -.Lout\@: -.endif - - FRAME_END - RET - -.if \host -.Lseamcall_vmfailinvalid\@: - mov $TDX_SEAMCALL_VMFAILINVALID, %rax - jmp .Lseamcall_fail\@ - -.Lseamcall_trap\@: - /* - * SEAMCALL caused #GP or #UD. By reaching here RAX contains - * the trap number. Convert the trap number to the TDX error - * code by setting TDX_SW_ERROR to the high 32-bits of RAX. - * - * Note cannot OR TDX_SW_ERROR directly to RAX as OR instruction - * only accepts 32-bit immediate at most. - */ - movq $TDX_SW_ERROR, %rdi - orq %rdi, %rax - -.Lseamcall_fail\@: - jmp .Lout\@ - - _ASM_EXTABLE_FAULT(.Lseamcall\@, .Lseamcall_trap\@) -.endif /* \host */ - -.endm - -.section .noinstr.text, "ax" - -/* - * __tdcall() - Used by TDX guests to request services from the TDX - * module (does not include VMM services) using TDCALL instruction. - * - * __tdcall() function ABI: - * - * @fn (RDI) - TDCALL Leaf ID, moved to RAX - * @args (RSI) - struct tdx_module_args for input - * - * Only RCX/RDX/R8-R11 are used as input registers. - * - * Return status of TDCALL via RAX. - */ -SYM_FUNC_START(__tdcall) - TDX_MODULE_CALL host=0 -SYM_FUNC_END(__tdcall) - -/* - * __tdcall_ret() - Used by TDX guests to request services from the TDX - * module (does not include VMM services) using TDCALL instruction, with - * saving output registers to the 'struct tdx_module_args' used as input. - * - * __tdcall_ret() function ABI: - * - * @fn (RDI) - TDCALL Leaf ID, moved to RAX - * @args (RSI) - struct tdx_module_args for input and output - * - * Only RCX/RDX/R8-R11 are used as input/output registers. - * - * Return status of TDCALL via RAX. - */ -SYM_FUNC_START(__tdcall_ret) - TDX_MODULE_CALL host=0 ret=1 -SYM_FUNC_END(__tdcall_ret) diff --git a/mod-tdx-guest/tdx-guest.h b/mod-tdx-guest/tdx-guest.h deleted file mode 100644 index 05b2cb07..00000000 --- a/mod-tdx-guest/tdx-guest.h +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: © 2022 Intel Corporation -// SPDX-FileCopyrightText: © 2024 Phala Network -// SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note -/* - * Userspace interface for TDX guest driver - * - * Copyright (C) 2022 Intel Corporation - */ - -#ifndef _UAPI_LINUX_TDX_GUEST_H_ -#define _UAPI_LINUX_TDX_GUEST_H_ - -#include -#include - -/* Length of the REPORTDATA used in TDG.MR.REPORT TDCALL */ -#define TDX_REPORTDATA_LEN 64 - -/* Length of TDREPORT used in TDG.MR.REPORT TDCALL */ -#define TDX_REPORT_LEN 1024 - -/** - * struct tdx_report_req - Request struct for TDX_CMD_GET_REPORT0 IOCTL. - * - * @reportdata: User buffer with REPORTDATA to be included into TDREPORT. - * Typically it can be some nonce provided by attestation - * service, so the generated TDREPORT can be uniquely verified. - * @tdreport: User buffer to store TDREPORT output from TDCALL[TDG.MR.REPORT]. - */ -struct tdx_report_req { - __u8 reportdata[TDX_REPORTDATA_LEN]; - __u8 tdreport[TDX_REPORT_LEN]; -}; - -/* - * TDX_CMD_GET_REPORT0 - Get TDREPORT0 (a.k.a. TDREPORT subtype 0) using - * TDCALL[TDG.MR.REPORT] - * - * Return 0 on success, -EIO on TDCALL execution failure, and - * standard errno on other general error cases. - */ -#define TDX_CMD_GET_REPORT0 _IOWR('T', 1, struct tdx_report_req) - -#define TDX_CMD_EXTEND_RTMR _IOR('T', 3, struct tdx_extend_rtmr_req) -#define TDX_CMD_EXTEND_RTMR2 _IOW('T', 3, struct tdx_extend_rtmr_req) -#define TDX_EXTEND_RTMR_DATA_LEN 48 -struct tdx_extend_rtmr_req -{ - u8 data[TDX_EXTEND_RTMR_DATA_LEN]; - u8 index; -}; - -#endif /* _UAPI_LINUX_TDX_GUEST_H_ */ diff --git a/mod-tdx-guest/tdx.h b/mod-tdx-guest/tdx.h deleted file mode 100644 index 05e8b97e..00000000 --- a/mod-tdx-guest/tdx.h +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: © 2022 Intel Corporation -// SPDX-FileCopyrightText: © 2024 Phala Network -// SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note -#ifndef _ASM_X86_SHARED_TDX_H -#define _ASM_X86_SHARED_TDX_H - -#include -#include - -#define TDX_HYPERCALL_STANDARD 0 - -#define TDX_CPUID_LEAF_ID 0x21 -#define TDX_IDENT "IntelTDX " - -/* TDX module Call Leaf IDs */ -#define TDG_VP_VMCALL 0 -#define TDG_VP_INFO 1 -#define TDG_MR_RTMR_EXTEND 2 -#define TDG_VP_VEINFO_GET 3 -#define TDG_MR_REPORT 4 -#define TDG_MEM_PAGE_ACCEPT 6 -#define TDG_VM_WR 8 - -/* TDCS fields. To be used by TDG.VM.WR and TDG.VM.RD module calls */ -#define TDCS_NOTIFY_ENABLES 0x9100000000000010 - -/* TDX hypercall Leaf IDs */ -#define TDVMCALL_MAP_GPA 0x10001 -#define TDVMCALL_GET_QUOTE 0x10002 -#define TDVMCALL_REPORT_FATAL_ERROR 0x10003 - -#define TDVMCALL_STATUS_RETRY 1 - -/* - * Bitmasks of exposed registers (with VMM). - */ -#define TDX_RDX BIT(2) -#define TDX_RBX BIT(3) -#define TDX_RSI BIT(6) -#define TDX_RDI BIT(7) -#define TDX_R8 BIT(8) -#define TDX_R9 BIT(9) -#define TDX_R10 BIT(10) -#define TDX_R11 BIT(11) -#define TDX_R12 BIT(12) -#define TDX_R13 BIT(13) -#define TDX_R14 BIT(14) -#define TDX_R15 BIT(15) - -/* - * These registers are clobbered to hold arguments for each - * TDVMCALL. They are safe to expose to the VMM. - * Each bit in this mask represents a register ID. Bit field - * details can be found in TDX GHCI specification, section - * titled "TDCALL [TDG.VP.VMCALL] leaf". - */ -#define TDVMCALL_EXPOSE_REGS_MASK \ - (TDX_RDX | TDX_RBX | TDX_RSI | TDX_RDI | TDX_R8 | TDX_R9 | \ - TDX_R10 | TDX_R11 | TDX_R12 | TDX_R13 | TDX_R14 | TDX_R15) - -/* TDX supported page sizes from the TDX module ABI. */ -#define TDX_PS_4K 0 -#define TDX_PS_2M 1 -#define TDX_PS_1G 2 -#define TDX_PS_NR (TDX_PS_1G + 1) - -#ifndef __ASSEMBLY__ - -#include - -/* - * Used in __tdcall*() to gather the input/output registers' values of the - * TDCALL instruction when requesting services from the TDX module. This is a - * software only structure and not part of the TDX module/VMM ABI - */ -struct tdx_module_args { - /* callee-clobbered */ - u64 rcx; - u64 rdx; - u64 r8; - u64 r9; - /* extra callee-clobbered */ - u64 r10; - u64 r11; - /* callee-saved + rdi/rsi */ - u64 r12; - u64 r13; - u64 r14; - u64 r15; - u64 rbx; - u64 rdi; - u64 rsi; -}; - -/* Used to communicate with the TDX module */ -u64 __tdcall(u64 fn, struct tdx_module_args *args); -u64 __tdcall_ret(u64 fn, struct tdx_module_args *args); -u64 __tdcall_saved_ret(u64 fn, struct tdx_module_args *args); - -#endif /* !__ASSEMBLY__ */ -#endif /* _ASM_X86_SHARED_TDX_H */ diff --git a/ra-rpc/src/client.rs b/ra-rpc/src/client.rs index 7e456317..1f68ad83 100644 --- a/ra-rpc/src/client.rs +++ b/ra-rpc/src/client.rs @@ -9,10 +9,7 @@ use prpc::{ client::{Error, RequestClient}, Message, }; -use ra_tls::{ - attestation::{Attestation, VerifiedAttestation}, - traits::CertExt, -}; +use ra_tls::{attestation::VerifiedAttestation, traits::CertExt}; use reqwest::{tls::TlsInfo, Certificate, Client, Identity, Response}; use serde::{de::DeserializeOwned, Serialize}; @@ -135,7 +132,7 @@ impl RaClient { let attestation = if !self.verify_server_attestation { None } else { - match Attestation::from_cert(&cert).context("Failed to parse attestation")? { + match ra_tls::attestation::from_cert(&cert).context("Failed to parse attestation")? { None => None, Some(attestation) => { let verified_attestation = attestation diff --git a/ra-rpc/src/rocket_helper.rs b/ra-rpc/src/rocket_helper.rs index dcdbfa15..88e32060 100644 --- a/ra-rpc/src/rocket_helper.rs +++ b/ra-rpc/src/rocket_helper.rs @@ -12,7 +12,7 @@ use rocket::response::content::{RawHtml, RawJson}; use std::{borrow::Cow, sync::Arc}; use anyhow::{Context, Result}; -use ra_tls::{attestation::Attestation, traits::CertExt}; +use ra_tls::traits::CertExt; use rocket::{ data::{ByteUnit, Data, Limits, ToByteUnit}, http::{uri::Origin, ContentType, Method, Status}, @@ -301,7 +301,7 @@ pub async fn handle_prpc_impl>( let attestation = request .certificate .as_ref() - .map(|cert| Attestation::from_der(cert.as_bytes())) + .map(|cert| ra_tls::attestation::from_der(cert.as_bytes())) .transpose()? .flatten(); let attestation = match (request.quote_verifier, attestation) { diff --git a/ra-tls/Cargo.toml b/ra-tls/Cargo.toml index c6451fbb..aed0ee5d 100644 --- a/ra-tls/Cargo.toml +++ b/ra-tls/Cargo.toml @@ -33,4 +33,15 @@ scale.workspace = true cc-eventlog.workspace = true serde-human-bytes.workspace = true +flate2.workspace = true or-panic.workspace = true +rand.workspace = true +tpm-types.workspace = true +dstack-types.workspace = true +tpm-qvl.workspace = true +hex_fmt.workspace = true +ez-hash.workspace = true +dstack-attest.workspace = true + +[features] +quote = ["dstack-attest/quote"] diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index 103f7f62..fcd6815a 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -1,513 +1,44 @@ -// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// SPDX-FileCopyrightText: © 2025 Phala Network // // SPDX-License-Identifier: Apache-2.0 -//! Attestation functions +//! Embedding and extracting attestation from/to TLS certificate -use std::borrow::Cow; - -use anyhow::{anyhow, bail, Context, Result}; -use dcap_qvl::quote::Quote; -use qvl::{ - quote::{EnclaveReport, Report, TDReport10, TDReport15}, - verify::VerifiedReport, -}; -use serde::Serialize; -use sha2::{Digest as _, Sha384}; -use x509_parser::parse_x509_certificate; +pub use dstack_attest::attestation::*; use crate::{oids, traits::CertExt}; -use cc_eventlog::TdxEventLog as EventLog; -use or_panic::ResultOrPanic; -use serde_human_bytes as hex_bytes; - -/// The content type of a quote. A CVM should only generate quotes for these types. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum QuoteContentType<'a> { - /// The public key of KMS root CA - KmsRootCa, - /// The public key of the RA-TLS certificate - RaTlsCert, - /// App defined data - AppData, - /// The custom content type - Custom(&'a str), -} - -/// The default hash algorithm used to hash the report data. -pub const DEFAULT_HASH_ALGORITHM: &str = "sha512"; - -impl QuoteContentType<'_> { - /// The tag of the content type used in the report data. - pub fn tag(&self) -> &str { - match self { - Self::KmsRootCa => "kms-root-ca", - Self::RaTlsCert => "ratls-cert", - Self::AppData => "app-data", - Self::Custom(tag) => tag, - } - } - - /// Convert the content to the report data. - pub fn to_report_data(&self, content: &[u8]) -> [u8; 64] { - self.to_report_data_with_hash(content, "") - .or_panic("sha512 hash should not fail") - } - - /// Convert the content to the report data with a specific hash algorithm. - pub fn to_report_data_with_hash(&self, content: &[u8], hash: &str) -> Result<[u8; 64]> { - macro_rules! do_hash { - ($hash: ty) => {{ - // The format is: - // hash(:) - let mut hasher = <$hash>::new(); - hasher.update(self.tag().as_bytes()); - hasher.update(b":"); - hasher.update(content); - let output = hasher.finalize(); - - let mut padded = [0u8; 64]; - padded[..output.len()].copy_from_slice(&output); - padded - }}; - } - let hash = if hash.is_empty() { - DEFAULT_HASH_ALGORITHM - } else { - hash - }; - let output = match hash { - "sha256" => do_hash!(sha2::Sha256), - "sha384" => do_hash!(sha2::Sha384), - "sha512" => do_hash!(sha2::Sha512), - "sha3-256" => do_hash!(sha3::Sha3_256), - "sha3-384" => do_hash!(sha3::Sha3_384), - "sha3-512" => do_hash!(sha3::Sha3_512), - "keccak256" => do_hash!(sha3::Keccak256), - "keccak384" => do_hash!(sha3::Keccak384), - "keccak512" => do_hash!(sha3::Keccak512), - "raw" => content.try_into().ok().context("invalid content length")?, - _ => bail!("invalid hash algorithm"), - }; - Ok(output) - } -} - -/// Represents a verified attestation -pub type VerifiedAttestation = Attestation; - -/// Attestation data -#[derive(Debug, Clone)] -pub struct Attestation { - /// Quote - pub quote: Vec, - /// Raw event log - pub raw_event_log: Vec, - /// Event log - pub event_log: Vec, - /// Verified report - pub report: R, -} - -impl Attestation { - /// Decode the quote - pub fn decode_quote(&self) -> Result { - Quote::parse(&self.quote) - } - - fn find_event(&self, imr: u32, ad: &str) -> Result { - for event in &self.event_log { - if event.imr == 3 && event.event == "system-ready" { - break; - } - if event.imr == imr && event.event == ad { - return Ok(event.clone()); - } - } - Err(anyhow!("event {ad} not found")) - } - - /// Replay event logs - pub fn replay_event_logs(&self, to_event: Option<&str>) -> Result<[[u8; 48]; 4]> { - replay_event_logs(&self.event_log, to_event) - } - - fn find_event_payload(&self, event: &str) -> Result> { - self.find_event(3, event).map(|event| event.event_payload) - } - - /// Decode the app-id from the event log - pub fn decode_app_id(&self) -> Result { - self.find_event(3, "app-id") - .map(|event| hex::encode(&event.event_payload)) - } - - /// Decode the instance-id from the event log - pub fn decode_instance_id(&self) -> Result { - self.find_event(3, "instance-id") - .map(|event| hex::encode(&event.event_payload)) - } - - /// Decode the upgraded app-id from the event log - pub fn decode_compose_hash(&self) -> Result { - let event = self.find_event(3, "compose-hash").or_else(|_| { - // Old images use this event name - self.find_event(3, "upgraded-app-id") - })?; - Ok(hex::encode(&event.event_payload)) - } - - /// Decode the app info from the event log - pub fn decode_app_info(&self, boottime_mr: bool) -> Result { - let rtmrs = self - .replay_event_logs(boottime_mr.then_some("boot-mr-done")) - .context("Failed to replay event logs")?; - let quote = self.decode_quote()?; - let device_id = sha256(&["e.header.user_data]).to_vec(); - let td_report = quote.report.as_td10().context("TDX report not found")?; - let key_provider_info = if boottime_mr { - vec![] - } else { - self.find_event_payload("key-provider").unwrap_or_default() - }; - let mr_key_provider = if key_provider_info.is_empty() { - [0u8; 32] - } else { - sha256(&[&key_provider_info]) - }; - let mr_system = sha256(&[ - &td_report.mr_td, - &rtmrs[0], - &rtmrs[1], - &rtmrs[2], - &mr_key_provider, - ]); - let mr_aggregated = { - use sha2::{Digest as _, Sha256}; - let mut hasher = Sha256::new(); - for d in [&td_report.mr_td, &rtmrs[0], &rtmrs[1], &rtmrs[2], &rtmrs[3]] { - hasher.update(d); - } - // For backward compatibility. Don't include mr_config_id, mr_owner, mr_owner_config if they are all 0. - if td_report.mr_config_id != [0u8; 48] - || td_report.mr_owner != [0u8; 48] - || td_report.mr_owner_config != [0u8; 48] - { - hasher.update(td_report.mr_config_id); - hasher.update(td_report.mr_owner); - hasher.update(td_report.mr_owner_config); - } - hasher.finalize().into() - }; - Ok(AppInfo { - app_id: self.find_event_payload("app-id").unwrap_or_default(), - compose_hash: self.find_event_payload("compose-hash")?, - instance_id: self.find_event_payload("instance-id").unwrap_or_default(), - device_id, - mrtd: td_report.mr_td, - rtmr0: rtmrs[0], - rtmr1: rtmrs[1], - rtmr2: rtmrs[2], - rtmr3: rtmrs[3], - os_image_hash: self.find_event_payload("os-image-hash").unwrap_or_default(), - mr_system, - mr_aggregated, - key_provider_info, - }) - } - - /// Decode the rootfs hash from the event log - pub fn decode_rootfs_hash(&self) -> Result { - self.find_event(3, "rootfs-hash") - .map(|event| hex::encode(event.digest)) - } - - /// Decode the report data in the quote - pub fn decode_report_data(&self) -> Result<[u8; 64]> { - match self.decode_quote()?.report { - Report::SgxEnclave(report) => Ok(report.report_data), - Report::TD10(report) => Ok(report.report_data), - Report::TD15(report) => Ok(report.base.report_data), - } - } -} - -impl Attestation { - /// Create an attestation for local machine - pub fn local() -> Result { - let (_, quote) = tdx_attest::get_quote(&[0u8; 64], None).context("Failed to get quote")?; - let event_log = - tdx_attest::eventlog::read_event_logs().context("Failed to read event logs")?; - let raw_event_log = - serde_json::to_vec(&event_log).context("Failed to serialize event log")?; - Ok(Self { - quote, - raw_event_log, - event_log, - report: (), - }) - } - - /// Create a new attestation - pub fn new(quote: Vec, raw_event_log: Vec) -> Result { - let event_log: Vec = if !raw_event_log.is_empty() { - serde_json::from_slice(&raw_event_log).context("invalid event log")? - } else { - vec![] - }; - Ok(Self { - quote, - raw_event_log, - event_log, - report: (), - }) - } - - /// Extract attestation data from a certificate - pub fn from_cert(cert: &impl CertExt) -> Result> { - Self::from_ext_getter(|oid| cert.get_extension_bytes(oid)) - } - - /// From an extension getter - pub fn from_ext_getter( - get_ext: impl Fn(&[u64]) -> Result>>, - ) -> Result> { - let quote = match get_ext(oids::PHALA_RATLS_QUOTE)? { - Some(v) => v, - None => return Ok(None), - }; - let raw_event_log = get_ext(oids::PHALA_RATLS_EVENT_LOG)?.unwrap_or_default(); - Self::new(quote, raw_event_log).map(Some) - } - - /// Extract attestation from x509 certificate - pub fn from_der(cert: &[u8]) -> Result> { - let (_, cert) = parse_x509_certificate(cert).context("Failed to parse certificate")?; - Self::from_cert(&cert) - } - - /// Extract attestation from x509 certificate in PEM format - pub fn from_pem(cert: &[u8]) -> Result> { - let (_, pem) = - x509_parser::pem::parse_x509_pem(cert).context("Failed to parse certificate")?; - let cert = pem.parse_x509().context("Failed to parse certificate")?; - Self::from_cert(&cert) - } - - /// Verify the quote - pub async fn verify_with_ra_pubkey( - self, - ra_pubkey_der: &[u8], - pccs_url: Option<&str>, - ) -> Result { - self.verify( - &QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der), - pccs_url, - ) - .await - } - - /// Verify the quote - pub async fn verify( - self, - report_data: &[u8; 64], - pccs_url: Option<&str>, - ) -> Result { - let quote = &self.quote; - if &self.decode_report_data()? != report_data { - bail!("report data mismatch"); - } - let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default()); - if pccs_url.is_empty() { - // try to read from PCCS_URL env var - pccs_url = match std::env::var("PCCS_URL") { - Ok(url) => Cow::Owned(url), - Err(_) => Cow::Borrowed(""), - }; - } - let report = qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref())) - .await - .context("Failed to get collateral")?; - if let Some(report) = report.report.as_td10() { - // Replay the event logs - let rtmrs = self - .replay_event_logs(None) - .context("Failed to replay event logs")?; - if rtmrs != [report.rt_mr0, report.rt_mr1, report.rt_mr2, report.rt_mr3] { - bail!("RTMR mismatch"); - } - } - validate_tcb(&report)?; - Ok(VerifiedAttestation { - quote: self.quote, - raw_event_log: self.raw_event_log, - event_log: self.event_log, - report, - }) - } -} - -impl Attestation {} - -/// Validate the TCB attributes -pub fn validate_tcb(report: &VerifiedReport) -> Result<()> { - fn validate_td10(report: &TDReport10) -> Result<()> { - let is_debug = report.td_attributes[0] & 0x01 != 0; - if is_debug { - bail!("Debug mode is not allowed"); - } - if report.mr_signer_seam != [0u8; 48] { - bail!("Invalid mr signer seam"); - } - Ok(()) - } - fn validate_td15(report: &TDReport15) -> Result<()> { - if report.mr_service_td != [0u8; 48] { - bail!("Invalid mr service td"); - } - validate_td10(&report.base) - } - fn validate_sgx(report: &EnclaveReport) -> Result<()> { - let is_debug = report.attributes[0] & 0x02 != 0; - if is_debug { - bail!("Debug mode is not allowed"); - } - Ok(()) - } - match &report.report { - Report::TD15(report) => validate_td15(report), - Report::TD10(report) => validate_td10(report), - Report::SgxEnclave(report) => validate_sgx(report), - } -} - -/// Information about the app extracted from event log -#[derive(Debug, Clone, Serialize)] -pub struct AppInfo { - /// App ID - #[serde(with = "hex_bytes")] - pub app_id: Vec, - /// SHA256 of the app compose file - #[serde(with = "hex_bytes")] - pub compose_hash: Vec, - /// ID of the CVM instance - #[serde(with = "hex_bytes")] - pub instance_id: Vec, - /// ID of the device - #[serde(with = "hex_bytes")] - pub device_id: Vec, - /// TCB info - #[serde(with = "hex_bytes")] - pub mrtd: [u8; 48], - /// Runtime MR0 - #[serde(with = "hex_bytes")] - pub rtmr0: [u8; 48], - /// Runtime MR1 - #[serde(with = "hex_bytes")] - pub rtmr1: [u8; 48], - /// Runtime MR2 - #[serde(with = "hex_bytes")] - pub rtmr2: [u8; 48], - /// Runtime MR3 - #[serde(with = "hex_bytes")] - pub rtmr3: [u8; 48], - /// Measurement of everything except the app info - #[serde(with = "hex_bytes")] - pub mr_system: [u8; 32], - /// Measurement of the entire vm execution environment - #[serde(with = "hex_bytes")] - pub mr_aggregated: [u8; 32], - /// Measurement of the app image - #[serde(with = "hex_bytes")] - pub os_image_hash: Vec, - /// Key provider info - #[serde(with = "hex_bytes")] - pub key_provider_info: Vec, -} - -/// Replay event logs -pub fn replay_event_logs(eventlog: &[EventLog], to_event: Option<&str>) -> Result<[[u8; 48]; 4]> { - let mut rtmrs = [[0u8; 48]; 4]; - for idx in 0..4 { - let mut mr = [0u8; 48]; - - for event in eventlog.iter() { - event - .validate() - .context("Failed to validate event digest")?; - if event.imr == idx { - let mut hasher = Sha384::new(); - hasher.update(mr); - hasher.update(event.digest); - mr = hasher.finalize().into(); - } - if let Some(to_event) = to_event { - if event.event == to_event { - break; - } - } - } - rtmrs[idx as usize] = mr; - } - - Ok(rtmrs) -} - -fn sha256(data: &[&[u8]]) -> [u8; 32] { - use sha2::{Digest as _, Sha256}; - let mut hasher = Sha256::new(); - for d in data { - hasher.update(d); - } - hasher.finalize().into() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_to_report_data_with_hash() { - let content_type = QuoteContentType::AppData; - let content = b"test content"; - - let report_data = content_type.to_report_data(content); - assert_eq!(hex::encode(report_data), "7ea0b744ed5e9c0c83ff9f575668e1697652cd349f2027cdf26f918d4c53e8cd50b5ea9b449b4c3d50e20ae00ec29688d5a214e8daff8a10041f5d624dae8a01"); - - // Test SHA-256 - let result = content_type - .to_report_data_with_hash(content, "sha256") - .unwrap(); - assert_eq!(result[32..], [0u8; 32]); // Check padding - assert_ne!(result[..32], [0u8; 32]); // Check hash is non-zero - - // Test SHA-384 - let result = content_type - .to_report_data_with_hash(content, "sha384") - .unwrap(); - assert_eq!(result[48..], [0u8; 16]); // Check padding - assert_ne!(result[..48], [0u8; 48]); // Check hash is non-zero - - // Test default - let result = content_type.to_report_data_with_hash(content, "").unwrap(); - assert_ne!(result, [0u8; 64]); // Should fill entire buffer - - // Test raw content - let exact_content = [42u8; 64]; - let result = content_type - .to_report_data_with_hash(&exact_content, "raw") - .unwrap(); - assert_eq!(result, exact_content); - - // Test invalid raw content length - let invalid_content = [42u8; 65]; - assert!(content_type - .to_report_data_with_hash(&invalid_content, "raw") - .is_err()); - - // Test invalid hash algorithm - assert!(content_type - .to_report_data_with_hash(content, "invalid") - .is_err()); - } +use anyhow::{Context, Result}; + +/// Extract attestation from x509 certificate +pub fn from_der(cert: &[u8]) -> Result> { + let (_, cert) = + x509_parser::parse_x509_certificate(cert).context("Failed to parse certificate")?; + from_cert(&cert) +} + +/// Extract attestation from a certificate +pub fn from_cert(cert: &impl CertExt) -> Result> { + from_ext_getter(|oid| cert.get_extension_bytes(oid)) +} + +/// Extract attestation from a certificate extension getter +pub fn from_ext_getter( + get_ext: impl Fn(&[u64]) -> Result>>, +) -> Result> { + // Try to detect attestation mode from certificate extension + if let Some(attestation_bytes) = get_ext(oids::PHALA_RATLS_ATTESTATION)? { + let VersionedAttestation::V0 { attestation } = + VersionedAttestation::from_scale(&attestation_bytes) + .context("Failed to decode attestation from cert extension")?; + return Ok(Some(attestation)); + } + // Backward compatibility: if PHALA_RATLS_ATTESTATION + let Some(tdx_quote) = get_ext(oids::PHALA_RATLS_TDX_QUOTE)? else { + return Ok(None); + }; + let raw_event_log = get_ext(oids::PHALA_RATLS_EVENT_LOG)?.context("TDX event log missing")?; + Ok(Some( + Attestation::from_tdx_quote(tdx_quote, &raw_event_log) + .context("Failed to create attestation from TDX quote")?, + )) } diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index 2cfa3e0c..eb5bb2c8 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -14,23 +14,23 @@ use rcgen::{ ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, PublicKeyData, SanType, }; use ring::rand::SystemRandom; -use tdx_attest::eventlog::read_event_logs; -use tdx_attest::get_quote; +use ring::signature::{ + EcdsaKeyPair, UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P256_SHA256_ASN1_SIGNING, +}; +use scale::{Decode, Encode}; use x509_parser::der_parser::Oid; use x509_parser::prelude::{FromDer as _, X509Certificate}; use x509_parser::public_key::PublicKey; use x509_parser::x509::SubjectPublicKeyInfo; -use crate::attestation::QuoteContentType; -use crate::oids::{PHALA_RATLS_APP_ID, PHALA_RATLS_CERT_USAGE}; -use crate::{ - oids::{PHALA_RATLS_EVENT_LOG, PHALA_RATLS_QUOTE}, - traits::CertExt, +use crate::oids::{ + PHALA_RATLS_APP_ID, PHALA_RATLS_ATTESTATION, PHALA_RATLS_CERT_USAGE, PHALA_RATLS_EVENT_LOG, + PHALA_RATLS_TDX_QUOTE, }; -use ring::signature::{ - EcdsaKeyPair, UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P256_SHA256_ASN1_SIGNING, +use crate::traits::CertExt; +use dstack_attest::attestation::{ + Attestation, AttestationMode, QuoteContentType, VersionedAttestation, }; -use scale::{Decode, Encode}; /// A CA certificate and private key. pub struct CaCert { @@ -81,13 +81,14 @@ impl CaCert { /// Sign a remote certificate signing request. pub fn sign_csr( &self, - csr: &CertSigningRequest, + csr: &CertSigningRequestV2, app_id: Option<&[u8]>, usage: &str, ) -> Result { let pki = rcgen::SubjectPublicKeyInfo::from_der(&csr.pubkey) .context("Failed to parse signature")?; let cfg = &csr.config; + let attestation = cfg.ext_quote.then_some(&csr.attestation); let req = CertRequest::builder() .key(&pki) .subject(&cfg.subject) @@ -95,8 +96,7 @@ impl CaCert { .alt_names(&cfg.subject_alt_names) .usage_server_auth(cfg.usage_server_auth) .usage_client_auth(cfg.usage_client_auth) - .maybe_quote(cfg.ext_quote.then_some(&csr.quote)) - .maybe_event_log(cfg.ext_quote.then_some(&csr.event_log)) + .maybe_attestation(attestation) .maybe_app_id(app_id) .special_usage(usage) .build(); @@ -122,8 +122,8 @@ pub struct CertConfig { } /// A certificate signing request. -#[derive(Encode, Decode, Clone, PartialEq)] -pub struct CertSigningRequest { +#[derive(Encode, Decode, Clone)] +pub struct CertSigningRequestV1 { /// The confirm word, need to be "please sign cert:" pub confirm: String, /// The public key of the certificate. @@ -136,10 +136,23 @@ pub struct CertSigningRequest { pub event_log: Vec, } -impl CertSigningRequest { - /// Sign the certificate signing request. - pub fn signed_by(&self, key: &KeyPair) -> Result> { - let encoded = self.encode(); +/// A trait for Certificate Signing Request (CSR) operations. +/// +/// This trait provides methods for signing and verifying CSRs using ECDSA P-256 keys. +/// Implementors must provide the data to sign, the public key, and a magic string for validation. +pub trait Csr { + /// Signs the CSR data using the provided key pair. + /// + /// # Arguments + /// * `key` - The ECDSA key pair used to sign the CSR. + /// + /// # Returns + /// The DER-encoded ECDSA signature as a byte vector. + /// + /// # Errors + /// Returns an error if key pair creation or signing fails. + fn signed_by(&self, key: &KeyPair) -> Result> { + let encoded = self.data_to_sign(); let rng = SystemRandom::new(); // Extract the DER-encoded private key and create an ECDSA key pair let key_pair = @@ -155,11 +168,24 @@ impl CertSigningRequest { Ok(signature) } - /// Verify the signature of the certificate signing request. - pub fn verify(&self, signature: &[u8]) -> Result<()> { - let encoded = self.encode(); + /// Verifies the signature of the CSR. + /// + /// # Arguments + /// * `signature` - The signature bytes to verify against the CSR data. + /// + /// # Returns + /// `Ok(())` if the signature is valid and the magic string matches. + /// + /// # Errors + /// Returns an error if: + /// - The public key cannot be parsed + /// - The algorithm is not ECDSA P-256 + /// - The signature is invalid + /// - The magic string does not match "please sign cert:" + fn verify(&self, signature: &[u8]) -> Result<()> { + let encoded = self.data_to_sign(); let (_rem, pki) = - SubjectPublicKeyInfo::from_der(&self.pubkey).context("Failed to parse pubkey")?; + SubjectPublicKeyInfo::from_der(self.pubkey()).context("Failed to parse pubkey")?; let parsed_pki = pki.parsed().context("Failed to parse pki")?; if !matches!(parsed_pki, PublicKey::EC(_)) { bail!("Unsupported algorithm"); @@ -169,16 +195,92 @@ impl CertSigningRequest { key.verify(&encoded, signature) .ok() .context("Invalid signature")?; - if self.confirm != "please sign cert:" { + if self.magic() != "please sign cert:" { bail!("Invalid confirm word"); } Ok(()) } - /// Encode the certificate signing request to a vector. + /// Returns the data that should be signed or verified. + /// + /// Implementors should return the encoded CSR data as a byte vector. + fn data_to_sign(&self) -> Vec; + + /// Returns the public key associated with this CSR. + /// + /// The public key should be in DER-encoded SubjectPublicKeyInfo format. + fn pubkey(&self) -> &[u8]; + + /// Returns the magic string used for validation. + /// + /// This string is checked during verification to ensure the CSR is valid. + /// Expected value: "please sign cert:" + fn magic(&self) -> &str; +} + +impl Csr for CertSigningRequestV1 { + fn data_to_sign(&self) -> Vec { + self.encode() + } + + fn pubkey(&self) -> &[u8] { + &self.pubkey + } + + fn magic(&self) -> &str { + &self.confirm + } +} + +/// A certificate signing request. +#[derive(Encode, Decode, Clone)] +pub struct CertSigningRequestV2 { + /// The confirm word, need to be "please sign cert:" + pub confirm: String, + /// The public key of the certificate. + pub pubkey: Vec, + /// The certificate configuration. + pub config: CertConfig, + /// The attestation. + pub attestation: VersionedAttestation, +} + +impl TryFrom for CertSigningRequestV2 { + type Error = anyhow::Error; + fn try_from(v0: CertSigningRequestV1) -> Result { + Ok(Self { + confirm: v0.confirm, + pubkey: v0.pubkey, + config: v0.config, + attestation: Attestation::from_tdx_quote(v0.quote, &v0.event_log)?.into_versioned(), + }) + } +} + +impl Csr for CertSigningRequestV2 { + fn data_to_sign(&self) -> Vec { + self.encode() + } + + fn pubkey(&self) -> &[u8] { + &self.pubkey + } + + fn magic(&self) -> &str { + &self.confirm + } +} + +impl CertSigningRequestV2 { + /// Encodes the certificate signing request into a byte vector. pub fn to_vec(&self) -> Vec { self.encode() } + + /// To attestation + pub fn to_attestation(&self) -> Result { + Ok(self.attestation.clone()) + } } /// Information required to create a certificate. @@ -191,8 +293,7 @@ pub struct CertRequest<'a, Key> { ca_level: Option, app_id: Option<&'a [u8]>, special_usage: Option<&'a str>, - quote: Option<&'a [u8]>, - event_log: Option<&'a [u8]>, + attestation: Option<&'a VersionedAttestation>, not_before: Option, not_after: Option, #[builder(default = false)] @@ -228,33 +329,31 @@ impl CertRequest<'_, Key> { .push(SanType::DnsName(alt_name.clone().try_into()?)); } } - if let Some(quote) = self.quote { - let content = yasna::construct_der(|writer| { - writer.write_bytes(quote); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_QUOTE, content); - params.custom_extensions.push(ext); - } - if let Some(event_log) = self.event_log { - let content = yasna::construct_der(|writer| { - writer.write_bytes(event_log); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_EVENT_LOG, content); - params.custom_extensions.push(ext); - } if let Some(app_id) = self.app_id { - let content = yasna::construct_der(|writer| { - writer.write_bytes(app_id); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_APP_ID, content); - params.custom_extensions.push(ext); + add_ext(&mut params, PHALA_RATLS_APP_ID, app_id); } - if let Some(special_usage) = self.special_usage { - let content = yasna::construct_der(|writer| { - writer.write_bytes(special_usage.as_bytes()); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_CERT_USAGE, content); - params.custom_extensions.push(ext); + if let Some(usage) = self.special_usage { + add_ext(&mut params, PHALA_RATLS_CERT_USAGE, usage); + } + if let Some(ver_att) = self.attestation { + let VersionedAttestation::V0 { attestation } = &ver_att; + match attestation.mode { + AttestationMode::DstackTdx => { + // For backward compatibility, we serialize the quote to the classic oids. + let Some(tdx_quote) = &attestation.tdx_quote else { + bail!("missing tdx quote") + }; + let event_log = serde_json::to_vec(&tdx_quote.event_log) + .context("Failed to serialize event log")?; + add_ext(&mut params, PHALA_RATLS_TDX_QUOTE, &tdx_quote.quote); + add_ext(&mut params, PHALA_RATLS_EVENT_LOG, &event_log); + } + _ => { + // The event logs are too large on GCP TDX to put in the certificate, so we strip them + let attestation_bytes = ver_att.clone().into_stripped().to_scale(); + add_ext(&mut params, PHALA_RATLS_ATTESTATION, &attestation_bytes); + } + } } if let Some(ca_level) = self.ca_level { params.is_ca = IsCa::Ca(BasicConstraints::Constrained(ca_level)); @@ -276,6 +375,15 @@ impl CertRequest<'_, Key> { } } +fn add_ext(params: &mut CertificateParams, oid: &[u64], content: impl AsRef<[u8]>) { + let content = yasna::construct_der(|writer| { + writer.write_bytes(content.as_ref()); + }); + params + .custom_extensions + .push(CustomExtension::from_oid_content(oid, content)); +} + impl CertRequest<'_, KeyPair> { /// Create a self-signed certificate. pub fn self_signed(self) -> Result { @@ -327,6 +435,50 @@ pub struct CertPair { pub key_pem: String, } +/// Magic prefix for gzip-compressed event log (version 1) +pub const EVENTLOG_GZIP_MAGIC: &[u8] = b"ELGZv1"; + +/// Compress a certificate extension value +pub fn compress_ext_value(data: &[u8]) -> Result> { + use flate2::write::GzEncoder; + use flate2::Compression; + use std::io::Write; + + let mut encoder = GzEncoder::new(Vec::new(), Compression::best()); + encoder + .write_all(data) + .context("failed to write to gzip encoder")?; + let compressed = encoder + .finish() + .context("failed to finish gzip compression")?; + + // Prepend magic prefix + let mut result = Vec::with_capacity(EVENTLOG_GZIP_MAGIC.len() + compressed.len()); + result.extend_from_slice(EVENTLOG_GZIP_MAGIC); + result.extend_from_slice(&compressed); + Ok(result) +} + +/// Decompress a certificate extension value +pub fn decompress_ext_value(data: &[u8]) -> Result> { + use flate2::read::GzDecoder; + use std::io::Read; + + if data.starts_with(EVENTLOG_GZIP_MAGIC) { + // Compressed format + let compressed = &data[EVENTLOG_GZIP_MAGIC.len()..]; + let mut decoder = GzDecoder::new(compressed); + let mut decompressed = Vec::new(); + decoder + .read_to_end(&mut decompressed) + .context("failed to decompress event log")?; + Ok(decompressed) + } else { + // Uncompressed format (backwards compatibility) + Ok(data.to_vec()) + } +} + /// Generate a certificate with RA-TLS quote and event log. pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result { use rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}; @@ -335,15 +487,18 @@ pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result Result" +} ``` ## Error Responses diff --git a/sdk/go/dstack/client.go b/sdk/go/dstack/client.go index ebff5724..c700e1f2 100644 --- a/sdk/go/dstack/client.go +++ b/sdk/go/dstack/client.go @@ -41,6 +41,11 @@ type GetQuoteResponse struct { VmConfig string `json:"vm_config"` } +// Represents the response from an attestation request. +type AttestResponse struct { + Attestation []byte +} + // Represents an event log entry in the TCB info type EventLog struct { IMR int `json:"imr"` @@ -411,6 +416,36 @@ func (c *DstackClient) GetQuote(ctx context.Context, reportData []byte) (*GetQuo }, nil } +// Gets a versioned attestation from the dstack service. +func (c *DstackClient) Attest(ctx context.Context, reportData []byte) (*AttestResponse, error) { + if len(reportData) > 64 { + return nil, fmt.Errorf("report data is too large, it should be at most 64 bytes") + } + + payload := map[string]interface{}{ + "report_data": hex.EncodeToString(reportData), + } + + data, err := c.sendRPCRequest(ctx, "/Attest", payload) + if err != nil { + return nil, err + } + + var response struct { + Attestation string `json:"attestation"` + } + if err := json.Unmarshal(data, &response); err != nil { + return nil, err + } + + attestation, err := hex.DecodeString(response.Attestation) + if err != nil { + return nil, err + } + + return &AttestResponse{Attestation: attestation}, nil +} + // Sends a request to get information about the CVM instance func (c *DstackClient) Info(ctx context.Context) (*InfoResponse, error) { data, err := c.sendRPCRequest(ctx, "/Info", map[string]interface{}{}) diff --git a/sdk/go/dstack/client_test.go b/sdk/go/dstack/client_test.go index ee8df0ff..477cd031 100644 --- a/sdk/go/dstack/client_test.go +++ b/sdk/go/dstack/client_test.go @@ -96,6 +96,26 @@ func TestGetQuote(t *testing.T) { } } +func TestAttest(t *testing.T) { + client := dstack.NewDstackClient() + resp, err := client.Attest(context.Background(), []byte("test")) + if err != nil { + t.Fatal(err) + } + + if len(resp.Attestation) == 0 { + t.Error("expected attestation to not be empty") + } + + _, err = client.Attest(context.Background(), bytes.Repeat([]byte("a"), 65)) + if err == nil { + t.Fatal("expected error for report data larger than 64 bytes") + } + if !strings.Contains(err.Error(), "report data is too large") { + t.Fatalf("expected error to mention report data size, got: %v", err) + } +} + func TestGetTlsKey(t *testing.T) { client := dstack.NewDstackClient() altNames := []string{"localhost"} @@ -429,7 +449,7 @@ func TestInfo(t *testing.T) { } func TestGetKeySignatureVerification(t *testing.T) { - expectedAppPubkey, _ := hex.DecodeString("02b85cceca0c02d878f0ebcda72a97469a472416eb6faf3c4807642132f9786810") + expectedAppPubkey, _ := hex.DecodeString("02818494263695e8839122dbd88e281d7380622999df4e60a14befa0f2d096fc7c") expectedKmsPubkey, _ := hex.DecodeString("02cad3a8bb11c5c0858fb3e402048b5137457039d577986daade678ed4b4ab1b9b") client := dstack.NewDstackClient() diff --git a/sdk/js/src/__tests__/index.test.ts b/sdk/js/src/__tests__/index.test.ts index dea1a1eb..a3a732ef 100644 --- a/sdk/js/src/__tests__/index.test.ts +++ b/sdk/js/src/__tests__/index.test.ts @@ -49,6 +49,13 @@ describe('DstackClient', () => { expect(result.replayRtmrs().length).toBe(4) }) + it('should be able to attest', async () => { + const client = new DstackClient() + const result = await client.attest('test') + expect(result).toHaveProperty('attestation') + expect(result.attestation).not.toBe('') + }) + it('should able to get derive key result as uint8array', async () => { const client = new DstackClient() const result = await client.getKey('/', 'test') @@ -87,6 +94,11 @@ describe('DstackClient', () => { await expect(() => client.getQuote(input)).rejects.toThrow() }) + it('should throw error on attest report_data larger than 64 bytes', async () => { + const client = new DstackClient() + await expect(() => client.attest(Buffer.alloc(65))).rejects.toThrow() + }) + it('should be able to get info', async () => { const client = new DstackClient() const result = await client.info() diff --git a/sdk/js/src/get-compose-hash.ts b/sdk/js/src/get-compose-hash.ts index 0442b132..3f3a44ef 100644 --- a/sdk/js/src/get-compose-hash.ts +++ b/sdk/js/src/get-compose-hash.ts @@ -34,7 +34,7 @@ function sortObject(obj: SortableValue): SortableValue { return obj; } -export type KeyProviderKind = "none" | "kms" | "local"; +export type KeyProviderKind = "none" | "kms" | "local" | "tpm"; export interface DockerConfig extends SortableObject { registry?: string; @@ -105,4 +105,4 @@ export function getComposeHash(app_compose: AppCompose, normalize: boolean = fal } const manifest_str = toDeterministicJson(app_compose); return crypto.createHash("sha256").update(manifest_str, "utf8").digest("hex"); -} \ No newline at end of file +} diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index 6e8e3206..01d90931 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -97,6 +97,12 @@ export interface GetQuoteResponse { replayRtmrs: () => string[] } +export interface AttestResponse { + __name__: Readonly<'AttestResponse'> + + attestation: Hex +} + export function to_hex(data: string | Buffer | Uint8Array): string { if (typeof data === 'string') { return Buffer.from(data).toString('hex'); @@ -242,6 +248,23 @@ export class DstackClient { return Object.freeze(result) } + async attest(report_data: string | Buffer | Uint8Array): Promise { + let hex = to_hex(report_data) + if (hex.length > 128) { + throw new Error(`Report data is too large, it should be less than 64 bytes.`) + } + const payload = JSON.stringify({ report_data: hex }) + const result = await send_rpc_request<{ attestation: string }>(this.endpoint, '/Attest', payload) + if ('error' in (result as any)) { + const err = (result as any)['error'] as string + throw new Error(err) + } + return Object.freeze({ + __name__: 'AttestResponse', + attestation: result.attestation as Hex, + }) + } + async info(): Promise> { const result = await send_rpc_request, 'tcb_info'> & { tcb_info: string }>(this.endpoint, '/Info', '{}') return Object.freeze({ diff --git a/sdk/python/src/dstack_sdk/__init__.py b/sdk/python/src/dstack_sdk/__init__.py index 2831a17d..ecf74414 100644 --- a/sdk/python/src/dstack_sdk/__init__.py +++ b/sdk/python/src/dstack_sdk/__init__.py @@ -4,6 +4,7 @@ from .dstack_client import AsyncDstackClient from .dstack_client import AsyncTappdClient +from .dstack_client import AttestResponse from .dstack_client import DstackClient from .dstack_client import EventLog from .dstack_client import GetKeyResponse @@ -32,6 +33,7 @@ # Response types "GetKeyResponse", "GetTlsKeyResponse", + "AttestResponse", "GetQuoteResponse", "InfoResponse", "TcbInfo", diff --git a/sdk/python/src/dstack_sdk/dstack_client.py b/sdk/python/src/dstack_sdk/dstack_client.py index 372c3f55..c62e8b01 100644 --- a/sdk/python/src/dstack_sdk/dstack_client.py +++ b/sdk/python/src/dstack_sdk/dstack_client.py @@ -152,6 +152,13 @@ def replay_rtmrs(self) -> Dict[int, str]: return rtmrs +class AttestResponse(BaseModel): + attestation: str + + def decode_attestation(self) -> bytes: + return bytes.fromhex(self.attestation) + + class SignResponse(BaseModel): signature: str signature_chain: List[str] @@ -378,6 +385,22 @@ async def get_quote( result = await self._send_rpc_request("GetQuote", {"report_data": hex}) return GetQuoteResponse(**result) + async def attest( + self, + report_data: str | bytes, + ) -> AttestResponse: + """Request a versioned attestation for the provided report data.""" + if not report_data or not isinstance(report_data, (bytes, str)): + raise ValueError("report_data can not be empty") + report_bytes: bytes = ( + report_data.encode() if isinstance(report_data, str) else report_data + ) + if len(report_bytes) > 64: + raise ValueError("report_data must be less than 64 bytes") + hex = binascii.hexlify(report_bytes).decode() + result = await self._send_rpc_request("Attest", {"report_data": hex}) + return AttestResponse(**result) + async def info(self) -> InfoResponse[TcbInfo]: """Fetch service information including parsed TCB info.""" result = await self._send_rpc_request("Info", {}) @@ -494,6 +517,14 @@ def get_quote( """Request an attestation quote for the provided report data.""" raise NotImplementedError + @call_async + def attest( + self, + report_data: str | bytes, + ) -> AttestResponse: + """Request a versioned attestation for the provided report data.""" + raise NotImplementedError + @call_async def info(self) -> InfoResponse[TcbInfo]: """Fetch service information including parsed TCB info.""" diff --git a/sdk/python/src/dstack_sdk/get_compose_hash.py b/sdk/python/src/dstack_sdk/get_compose_hash.py index 3839c614..da5ae453 100644 --- a/sdk/python/src/dstack_sdk/get_compose_hash.py +++ b/sdk/python/src/dstack_sdk/get_compose_hash.py @@ -17,7 +17,7 @@ from typing import Optional from typing import Union -KeyProviderKind = Literal["none", "kms", "local"] +KeyProviderKind = Literal["none", "kms", "local", "tpm"] class DockerConfig: diff --git a/sdk/python/tests/test_client.py b/sdk/python/tests/test_client.py index 437acf95..6b0d9b1b 100644 --- a/sdk/python/tests/test_client.py +++ b/sdk/python/tests/test_client.py @@ -10,6 +10,7 @@ from dstack_sdk import AsyncDstackClient from dstack_sdk import AsyncTappdClient +from dstack_sdk import AttestResponse from dstack_sdk import DstackClient from dstack_sdk import GetKeyResponse from dstack_sdk import GetQuoteResponse @@ -43,6 +44,13 @@ def test_sync_client_get_quote(): assert isinstance(result, GetQuoteResponse) +def test_sync_client_attest(): + client = DstackClient() + result = client.attest("test") + assert isinstance(result, AttestResponse) + assert len(result.attestation) > 0 + + def test_sync_client_get_tls_key(): client = DstackClient() result = client.get_tls_key() @@ -98,6 +106,14 @@ async def test_async_client_get_quote(): assert isinstance(result, GetQuoteResponse) +@pytest.mark.asyncio +async def test_async_client_attest(): + client = AsyncDstackClient() + result = await client.attest("test") + assert isinstance(result, AttestResponse) + assert len(result.attestation) > 0 + + @pytest.mark.asyncio async def test_async_client_get_tls_key(): client = AsyncDstackClient() diff --git a/sdk/run-tests.sh b/sdk/run-tests.sh index 74a64475..00ebcd7d 100755 --- a/sdk/run-tests.sh +++ b/sdk/run-tests.sh @@ -25,6 +25,7 @@ cargo test -p dstack-sdk-types --test no_std_test --no-default-features popd pushd go/ +go clean -testcache go test -v ./dstack DSTACK_SIMULATOR_ENDPOINT=$TAPPD_SIMULATOR_ENDPOINT go test -v ./tappd popd diff --git a/sdk/rust/src/dstack_client.rs b/sdk/rust/src/dstack_client.rs index 40a5242a..281b325c 100644 --- a/sdk/rust/src/dstack_client.rs +++ b/sdk/rust/src/dstack_client.rs @@ -141,6 +141,18 @@ impl DstackClient { Ok(response) } + pub async fn attest(&self, report_data: Vec) -> Result { + if report_data.is_empty() || report_data.len() > 64 { + anyhow::bail!("Invalid report data length") + } + let hex_data = hex_encode(report_data); + let data = json!({ "report_data": hex_data }); + let response = self.send_rpc_request("/Attest", &data).await?; + let response = serde_json::from_value::(response)?; + + Ok(response) + } + pub async fn info(&self) -> Result { let response = self.send_rpc_request("/Info", &json!({})).await?; Ok(InfoResponse::validated_from_value(response)?) diff --git a/sdk/rust/tests/test_client.rs b/sdk/rust/tests/test_client.rs index e7be67e0..3010a110 100644 --- a/sdk/rust/tests/test_client.rs +++ b/sdk/rust/tests/test_client.rs @@ -24,6 +24,17 @@ async fn test_async_client_get_quote() { assert!(!result.quote.is_empty()); } +#[tokio::test] +async fn test_async_client_attest() { + let client = AsyncDstackClient::new(None); + let result = client.attest(b"test".to_vec()).await.unwrap(); + let attestation = result.decode_attestation().unwrap(); + assert!(!attestation.is_empty()); + + let too_large = client.attest(vec![0_u8; 65]).await; + assert!(too_large.is_err()); +} + #[tokio::test] async fn test_async_client_get_tls_key() { let client = AsyncDstackClient::new(None); diff --git a/sdk/rust/types/src/dstack.rs b/sdk/rust/types/src/dstack.rs index ba151fbc..332ddd51 100644 --- a/sdk/rust/types/src/dstack.rs +++ b/sdk/rust/types/src/dstack.rs @@ -113,6 +113,21 @@ pub struct GetQuoteResponse { pub vm_config: String, } +/// Response containing a versioned attestation +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] +pub struct AttestResponse { + /// The attestation in hexadecimal format + pub attestation: String, +} + +impl AttestResponse { + pub fn decode_attestation(&self) -> Result, FromHexError> { + hex::decode(&self.attestation) + } +} + impl GetQuoteResponse { pub fn decode_quote(&self) -> Result, FromHexError> { hex::decode(&self.quote) diff --git a/sdk/simulator/app-compose.json b/sdk/simulator/app-compose.json index c5f5ff8c..bcbba37d 100644 --- a/sdk/simulator/app-compose.json +++ b/sdk/simulator/app-compose.json @@ -1,15 +1 @@ -{ - "manifest_version": 2, - "name": "kvin-nb", - "runner": "docker-compose", - "docker_compose_file": "services:\n jupyter:\n image: quay.io/jupyter/base-notebook\n user: root\n environment:\n - GRANT_SUDO=yes\n ports:\n - \"8888:8888\"\n volumes:\n - /:/host/\n - /var/run/tappd.sock:/var/run/tappd.sock\n - /var/run/dstack.sock:/var/run/dstack.sock\n logging:\n driver: journald\n options:\n tag: jupyter-notebook\n", - "docker_config": {}, - "kms_enabled": true, - "tproxy_enabled": true, - "public_logs": true, - "public_sysinfo": true, - "public_tcbinfo": false, - "local_key_provider_enabled": false, - "allowed_envs": [], - "no_instance_id": false -} +{"manifest_version":2,"name":"guest-agent","runner":"docker-compose","docker_compose_file":"services:\n dstack-agent:\n image: ubuntu\n user: root\n network_mode: host\n volumes:\n - /:/host/\n - /var/run/tappd.sock:/var/run/tappd.sock\n - /var/run/dstack.sock:/var/run/dstack.sock\n entrypoint: |\n bash -c '\n apt-get update && apt-get install -y socat\n socat TCP-LISTEN:2000,fork UNIX-CONNECT:/var/run/tappd.sock &\n socat TCP-LISTEN:3000,fork UNIX-CONNECT:/var/run/dstack.sock &\n tail -f /dev/null\n '\n dstack-verifier:\n image: dstacktee/dstack-verifier:0.5.4\n ports:\n - \"8080:8080\"\n restart: unless-stopped","gateway_enabled":true,"public_logs":true,"public_sysinfo":true,"public_tcbinfo":true,"key_provider_id":"","allowed_envs":[],"no_instance_id":false,"secure_time":false,"key_provider":"kms","kms_enabled":true,"storage_fs":"ext4","pre_launch_script":"docker run --rm --privileged --pid=host --net=host -v /:/host \\\n -e SSH_GITHUB_USER=\"kvinwang\" \\\n kvin/dstack-openssh-installer:latest"} \ No newline at end of file diff --git a/sdk/simulator/appkeys.json b/sdk/simulator/appkeys.json index aa5d44c1..1e67f019 100644 --- a/sdk/simulator/appkeys.json +++ b/sdk/simulator/appkeys.json @@ -1,13 +1,13 @@ { - "disk_crypt_key": "1122e1f340c19407adc5ec531ac98d72bcf702bf7858f6fa49b5be79b61e4d5b", - "env_crypt_key": "ca1a3895d9d613287fc14034d0ec60abb5089896e7c8fd7c2f02bd91fa0076aa", - "k256_key": "e0e5d254fb944dcc370a2e5288b336a1e809871545a73ee645368957fefa31f9", - "k256_signature": "2f431c7956869a4fe3e028c5f9518a935e2d01e81a3628f8b1d178fc2fac7b6d2405ace433624e5568e23c4ed291dbaf60dac79b756837c0fe745154ebfdc0a601", - "gateway_app_id": "any", - "ca_cert": "-----BEGIN CERTIFICATE-----\nMIIBmTCCAUCgAwIBAgIUU7801+krCs2OpIdne3t6OWrJ2fMwCgYIKoZIzj0EAwIw\nKTEPMA0GA1UECgwGRHN0YWNrMRYwFAYDVQQDDA1Ec3RhY2sgS01TIENBMB4XDTc1\nMDEwMTAwMDAwMFoXDTM1MDMxNzA5NDQ0MlowKTEPMA0GA1UECgwGRHN0YWNrMRYw\nFAYDVQQDDA1Ec3RhY2sgS01TIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\nGbJFfdm4qmRG2YDxNv/3gS7NbHd0DusOKLENVsDAACiltuWdzqMH1YO9H3B2npwR\nbfK8+xdYqV2GE+feHISCwKNGMEQwDwYDVR0PAQH/BAUDAweAADAdBgNVHQ4EFgQU\nevjJ+VZPvDxHJ2ejjeIaUYMMcEcwEgYDVR0TAQH/BAgwBgEB/wIBATAKBggqhkjO\nPQQDAgNHADBEAiAhQHQNbmyvx9BDBXRjW1eCkPCpFs/2Vt/nvbi+M69FPAIgQ13F\n3pmxicxyFeVW2iOjrbG1cxLdT9Kh+9ICF9zn8kA=\n-----END CERTIFICATE-----\n", - "key_provider": { - "None": { - "key": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1PYCFKYfDmUfv5fk\nstppasf4mPGqnz0fEoLEnGx8CnKhRANCAAQZskV92biqZEbZgPE2//eBLs1sd3QO\n6w4osQ1WwMAAKKW25Z3OowfVg70fcHaenBFt8rz7F1ipXYYT594chILA\n-----END PRIVATE KEY-----\n" + "disk_crypt_key": "2cbc10ccbed084b91af2ceff8400e6082402367f18a2c6248bac17d2fc951607", + "env_crypt_key": "4f3cf0a19a0444674c8e51222afd395b8df9fad2ba3cd7956f640a4b3c046db6", + "k256_key": "d6e88992cdeeee35fe70b5db61ab66cdb191fb9b6ec9313757ef162dd7214d5d", + "k256_signature": "9e618603e72d01fedb82deff6daf2d62a572becf0059eec3f89c1ab40e1f2e594d2a283f843f34e8f39e4cc49a612496ce67223a12ac923f8efe330346dfc6c500", + "gateway_app_id": "any", + "ca_cert": "-----BEGIN CERTIFICATE-----\nMIIBmzCCAUCgAwIBAgIUU7801+krCs2OpIdne3t6OWrJ2fMwCgYIKoZIzj0EAwIw\nKTEPMA0GA1UECgwGRHN0YWNrMRYwFAYDVQQDDA1Ec3RhY2sgS01TIENBMB4XDTc1\nMDEwMTAwMDAwMFoXDTM1MTIxOTAyNTEzOVowKTEPMA0GA1UECgwGRHN0YWNrMRYw\nFAYDVQQDDA1Ec3RhY2sgS01TIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\nGbJFfdm4qmRG2YDxNv/3gS7NbHd0DusOKLENVsDAACiltuWdzqMH1YO9H3B2npwR\nbfK8+xdYqV2GE+feHISCwKNGMEQwDwYDVR0PAQH/BAUDAweGADAdBgNVHQ4EFgQU\nevjJ+VZPvDxHJ2ejjeIaUYMMcEcwEgYDVR0TAQH/BAgwBgEB/wIBATAKBggqhkjO\nPQQDAgNJADBGAiEAvAYUOGbU5QC23zzQtJqm7/hGzVK5SlI0P7yGDii+/4ACIQCN\nbKkagb0uncr6sUKlhKrpHhID+WWTvqJj0TrkvbVdCg==\n-----END CERTIFICATE-----\n", + "key_provider": { + "None": { + "key": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1PYCFKYfDmUfv5fk\nstppasf4mPGqnz0fEoLEnGx8CnKhRANCAAQZskV92biqZEbZgPE2//eBLs1sd3QO\n6w4osQ1WwMAAKKW25Z3OowfVg70fcHaenBFt8rz7F1ipXYYT594chILA\n-----END PRIVATE KEY-----\n" + } } - } } \ No newline at end of file diff --git a/sdk/simulator/attestation.bin b/sdk/simulator/attestation.bin new file mode 100644 index 00000000..2f7a9c35 Binary files /dev/null and b/sdk/simulator/attestation.bin differ diff --git a/sdk/simulator/dstack.toml b/sdk/simulator/dstack.toml index ab5bcebd..ecf4a8e4 100644 --- a/sdk/simulator/dstack.toml +++ b/sdk/simulator/dstack.toml @@ -17,8 +17,7 @@ sys_config_file = "sys-config.json" [default.core.simulator] enabled = true -quote_file = "quote.hex" -event_log_file = "eventlog.json" +attestation_file = "attestation.bin" [internal-v0] address = "unix:./tappd.sock" @@ -35,4 +34,3 @@ reuse = true [guest-api] address = "unix:./guest.sock" reuse = true - diff --git a/sdk/simulator/eventlog.json b/sdk/simulator/eventlog.json deleted file mode 100644 index 7036e3ec..00000000 --- a/sdk/simulator/eventlog.json +++ /dev/null @@ -1 +0,0 @@ -[{"imr":0,"event_type":2147483659,"digest":"0e35f1b315ba6c912cf791e5c79dd9d3a2b8704516aa27d4e5aa78fb09ede04aef2bbd02ac7a8734c48562b9c26ba35d","event":"","event_payload":"095464785461626c65000100000000000000af96bb93f2b9b84e9462e0ba745642360090800000000000"},{"imr":0,"event_type":2147483658,"digest":"344bc51c980ba621aaa00da3ed7436f7d6e549197dfe699515dfa2c6583d95e6412af21c097d473155875ffd561d6790","event":"","event_payload":"2946762858585858585858582d585858582d585858582d585858582d58585858585858585858585829000000c0ff000000000040080000000000"},{"imr":0,"event_type":2147483649,"digest":"9dc3a1f80bcec915391dcda5ffbb15e7419f77eab462bbf72b42166fb70d50325e37b36f93537a863769bcf9bedae6fb","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c0a00000000000000000000000000000053006500630075007200650042006f006f007400"},{"imr":0,"event_type":2147483649,"digest":"6f2e3cbc14f9def86980f5f66fd85e99d63e69a73014ed8a5633ce56eca5b64b692108c56110e22acadcef58c3250f1b","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c0200000000000000000000000000000050004b00"},{"imr":0,"event_type":2147483649,"digest":"d607c0efb41c0d757d69bca0615c3a9ac0b1db06c557d992e906c6b7dee40e0e031640c7bfd7bcd35844ef9edeadc6f9","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c030000000000000000000000000000004b0045004b00"},{"imr":0,"event_type":2147483649,"digest":"08a74f8963b337acb6c93682f934496373679dd26af1089cb4eaf0c30cf260a12e814856385ab8843e56a9acea19e127","event":"","event_payload":"cbb219d73a3d9645a3bcdad00e67656f0200000000000000000000000000000064006200"},{"imr":0,"event_type":2147483649,"digest":"18cc6e01f0c6ea99aa23f8a280423e94ad81d96d0aeb5180504fc0f7a40cb3619dd39bd6a95ec1680a86ed6ab0f9828d","event":"","event_payload":"cbb219d73a3d9645a3bcdad00e67656f03000000000000000000000000000000640062007800"},{"imr":0,"event_type":4,"digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0","event":"","event_payload":"00000000"},{"imr":0,"event_type":10,"digest":"68cd79315e70aecd4afe7c1b23a5ed7b3b8e51a477e1739f111b3156def86bbc56ebf239dcd4591bc7a9fff90023f481","event":"","event_payload":"414350492044415441"},{"imr":0,"event_type":10,"digest":"6bc203b3843388cc4918459c3f5c6d1300a796fb594781b7ecfaa3ae7456975f095bfcc1156c9f2d25e8b8bc1b520f66","event":"","event_payload":"414350492044415441"},{"imr":0,"event_type":10,"digest":"ec9e8622a100c399d71062a945f95d8e4cdb7294e8b1c6d17a6a8d37b5084444000a78b007ef533f290243421256d25c","event":"","event_payload":"414350492044415441"},{"imr":1,"event_type":2147483651,"digest":"0db5964580e727672734da95797318d8455ab74b3e3d66fbb1aaa4ddd01a3f8555f4889e57c19a15c165594e31678dc0","event":"","event_payload":"18a0447b0000000000b4b2000000000000000000000000002a000000000000000403140072f728144ab61e44b8c39ebdd7f893c7040412006b00650072006e0065006c0000007fff0400"},{"imr":0,"event_type":2147483650,"digest":"1dd6f7b457ad880d840d41c961283bab688e94e4b59359ea45686581e90feccea3c624b1226113f824f315eb60ae0a7c","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c0900000000000000020000000000000042006f006f0074004f0072006400650072000000"},{"imr":0,"event_type":2147483650,"digest":"23ada07f5261f12f34a0bd8e46760962d6b4d576a416f1fea1c64bc656b1d28eacf7047ae6e967c58fd2a98bfa74c298","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c08000000000000003e0000000000000042006f006f0074003000300030003000090100002c0055006900410070007000000004071400c9bdb87cebf8344faaea3ee4af6516a10406140021aa2c4614760345836e8ab6f46623317fff0400"},{"imr":1,"event_type":2147483655,"digest":"77a0dab2312b4e1e57a84d865a21e5b2ee8d677a21012ada819d0a98988078d3d740f6346bfe0abaa938ca20439a8d71","event":"","event_payload":"43616c6c696e6720454649204170706c69636174696f6e2066726f6d20426f6f74204f7074696f6e"},{"imr":1,"event_type":4,"digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0","event":"","event_payload":"00000000"},{"imr":2,"event_type":6,"digest":"ad49ca7e80258d7580c5c580cd21ada7ecbf418dde5197d6f8c835493ceb6edec0f8954b733bd9b889f96f33e5f9cb05","event":"","event_payload":"ed223b8f1a0000004c4f414445445f494d4147453a3a4c6f61644f7074696f6e7300"},{"imr":2,"event_type":6,"digest":"e0cdb72fbba75a0f4d396c0b80a4336db049b383a9730467160dec0b7059cb22aca87639dcc655d935d6c6356b3108ad","event":"","event_payload":"ec223b8f0d0000004c696e757820696e6974726400"},{"imr":1,"event_type":2147483655,"digest":"214b0bef1379756011344877743fdc2a5382bac6e70362d624ccf3f654407c1b4badf7d8f9295dd3dabdef65b27677e0","event":"","event_payload":"4578697420426f6f7420536572766963657320496e766f636174696f6e"},{"imr":1,"event_type":2147483655,"digest":"0a2e01c85deae718a530ad8c6d20a84009babe6c8989269e950d8cf440c6e997695e64d455c4174a652cd080f6230b74","event":"","event_payload":"4578697420426f6f742053657276696365732052657475726e656420776974682053756363657373"},{"imr":3,"event_type":134217729,"digest":"f9974020ef507068183313d0ca808e0d1ca9b2d1ad0c61f5784e7157c362c06536f5ddacdad4451693f48fcc72fff624","event":"system-preparing","event_payload":""},{"imr":3,"event_type":134217729,"digest":"b01c7a2e6a406ae9cd5aa81451e4614e112b8f404df12e6ef506962c1a5279a94dc58da0923c4b7db89e26da9e538302","event":"app-id","event_payload":"ea549f02e1a25fabd1cb788380e033ec5461b2ff"},{"imr":3,"event_type":134217729,"digest":"9c1fecc259af1e8494484a391bdef460cb74d677c76dd114b1e9e7fac343da4e773b2b0eb8df7a6fc0dd8ba5edbb30e1","event":"compose-hash","event_payload":"ea549f02e1a25fabd1cb788380e033ec5461b2ffe4328d753642cf035452e48b"},{"imr":3,"event_type":134217729,"digest":"a8dc2d07060d74dfba7b4942411bcf93ae198da42d172860f0c6dcb9207198a2c857a4b0e57bb019d68be072074a2d01","event":"instance-id","event_payload":"59df8036b824b0aac54f8998b9e1fb2a0cfc5d3a"},{"imr":3,"event_type":134217729,"digest":"98bd7e6bd3952720b65027fd494834045d06b4a714bf737a06b874638b3ea00ff402f7f583e3e3b05e921c8570433ac6","event":"boot-mr-done","event_payload":""},{"imr":3,"event_type":134217729,"digest":"cc0ae424f1335f3059359f712f72f0aebee7a01fba2e4d527f3ea9299bac808a3ea1f8ae2982875fb3c9697fd6f4a5f2","event":"key-provider","event_payload":"7b226e616d65223a226b6d73222c226964223a223330353933303133303630373261383634386365336430323031303630383261383634386365336430333031303730333432303030343139623234353764643962386161363434366439383066313336666666373831326563643663373737343065656230653238623130643536633063303030323861356236653539646365613330376435383362643166373037363965396331313664663262636662313735386139356438363133653764653163383438326330227d"},{"imr":3,"event_type":134217729,"digest":"1a76b2a80a0be71eae59f80945d876351a7a3fb8e9fd1ff1cede5734aa84ea11fd72b4edfbb6f04e5a85edd114c751bd","event":"system-ready","event_payload":""}] diff --git a/sdk/simulator/quote.hex b/sdk/simulator/quote.hex deleted file mode 100644 index 33a3fd92..00000000 --- a/sdk/simulator/quote.hex +++ /dev/null @@ -1 +0,0 @@ -040002008100000000000000939a7233f79c4ca9940a0db3957f060783fbfe61525f55581315cd9dc950f44700000000060102000000000000000000000000005b38e33a6487958b72c3c12a938eaa5e3fd4510c51aeeab58c7d5ecee41d7c436489d6c8e4f92f160b7cad34207b00c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000e702060000000000c68518a0ebb42136c12b2275164f8c72f25fa9a34392228687ed6e9caeb9c0f1dbd895e9cf475121c029dc47e70e91fd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085e0855a6384fa1c8a6ab36d0dcbfaa11a5753e5a070c08218ae5fe872fcb86967fd2449c29e22e59dc9fec998cb65476ba8f87f35d0641e8abca07e75e3882abdc9f19d7cc8f6e3fe04435bd5f694d4e3cf008b60d7c7233896e8d1f23c34a703b1c4afcac07d00d8e853163aff3ba3f9af68ddfbdbeafab70210a8dc601b409c28873d74fb6dbe7dc33a8da7c096216d1a3da994b6611ee602f25f07b41671ece90cd2898689f1ad4448fdf1155e3668736cca4499659caae2d8044070de5700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cc1000004f8ed43bde5c1c75f4dcc530d5015ab0514879a8b9dc2663e6c462ac2a0a31face0b334f64976b2aadc4ec0acf00601d5f5738cbf61c12fdcc25dab524a9eac84996a9e56e40ac6c0b019709537f16d751c03e8c0d905d79f224ff06ddc4102860a8770107748c011cdbfcccc857e418735b699ac89dc2ed4da11d5125cb925e0600461000000202191b03ff0006000000000000000000000000000000000000000000000000000000000000000000000000000000001500000000000000e700000000000000e5a3a7b5d830c2953b98534c6c59a3a34fdc34e933f7f5898f0a85cf08846bca0000000000000000000000000000000000000000000000000000000000000000dc9e2a7c6f948f17474e34a7fc43ed030f7c1563f1babddf6340c82e0e54a8c500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000503bbfe5befa55a13e21747c3859f0b618a050312a0340e980187eea232356d60000000000000000000000000000000000000000000000000000000000000000784b1126be37912aaa4189f677ac8821e36366bb526c1b9ffc42c9ad0c332804423f05b854f20d4c511dbcaee26c5911e9b47d28b0f791b9c3d993554034b1382000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f05005e0e00002d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d49494538444343424a65674177494241674956414c5235544954392b396e73423142545a3173725851346c627752424d416f4743437147534d343942414d430a4d484178496a416742674e5642414d4d47556c756447567349464e4857434251513073675547786864475a76636d306751304578476a415942674e5642416f4d0a45556c756447567349454e76636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155450a4341774351304578437a414a42674e5642415954416c56544d423458445449304d4467774d6a45784d54557a4e316f5844544d784d4467774d6a45784d54557a0a4e316f77634445694d434147413155454177775a535735305a5777675530645949464244537942445a584a3061575a70593246305a5445614d426747413155450a43677752535735305a577767513239796347397959585270623234784644415342674e564241634d43314e68626e526849454e7359584a684d517377435159440a5651514944414a445154454c4d416b474131554542684d4356564d775754415442676371686b6a4f5051494242676771686b6a4f50514d4242774e43414154590a77777155344778504a6a596f6a4d4752686136327970346a425164355744764b776d54366c6c314147786a59363870694a50676950686462387a544766374b620a314f79643153464f4d5a70594c795054427a59646f3449444444434341776777487759445652306a42426777466f41556c5739647a62306234656c4153636e550a3944504f4156634c336c5177617759445652306642475177596a42676f46366758495a616148523063484d364c79396863476b7564484a316333526c5a484e6c0a636e5a705932567a4c6d6c75644756734c6d4e766253397a5a3367765932567964476c6d61574e6864476c76626939324e4339775932746a636d772f593245390a6347786864475a76636d306d5a57356a62325270626d63395a4756794d423047413155644467515742425146303476507654474b7762416c356f54765664664d0a2b356a6e7554414f42674e56485138424166384542414d434273417744415944565230544151482f4241497741444343416a6b4743537147534962345451454e0a4151534341696f776767496d4d42344743697147534962345451454e41514545454e3564416f7135634b356e383277396f793165346e34776767466a42676f710a686b69472b453042445145434d494942557a415142677371686b69472b4530424451454341514942416a415142677371686b69472b45304244514543416749420a416a415142677371686b69472b4530424451454341774942416a415142677371686b69472b4530424451454342414942416a415142677371686b69472b4530420a4451454342514942417a415142677371686b69472b45304244514543426749424154415142677371686b69472b453042445145434277494241444151426773710a686b69472b4530424451454343414942417a415142677371686b69472b45304244514543435149424144415142677371686b69472b45304244514543436749420a4144415142677371686b69472b45304244514543437749424144415142677371686b69472b45304244514543444149424144415142677371686b69472b4530420a44514543445149424144415142677371686b69472b45304244514543446749424144415142677371686b69472b453042445145434477494241444151426773710a686b69472b45304244514543454149424144415142677371686b69472b4530424451454345514942437a416642677371686b69472b45304244514543456751510a4167494341674d4241414d4141414141414141414144415142676f71686b69472b45304244514544424149414144415542676f71686b69472b453042445145450a4241617777473841414141774477594b4b6f5a496876684e4151304242516f424154416542676f71686b69472b453042445145474242424a316472685349736d0a682b2f46793074746a6a762f4d45514743697147534962345451454e415163774e6a415142677371686b69472b45304244514548415145422f7a4151426773710a686b69472b45304244514548416745422f7a415142677371686b69472b45304244514548417745422f7a414b42676771686b6a4f5051514441674e48414442450a41694270455738754f726b537469486b4c4b6e6a426855416f637a39545733366a4e2f303765416844503635617749674d2f31474c58745a70446436706150760a535a386d4e7472543830305635346b465944474f7a4f78504374383d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949436c6a4343416a32674177494241674956414a567658633239472b487051456e4a3150517a7a674658433935554d416f4743437147534d343942414d430a4d476778476a415942674e5642414d4d45556c756447567349464e48574342536232393049454e424d526f77474159445651514b4442464a626e526c624342440a62334a7762334a6864476c76626a45554d424947413155454277774c553246756447456751327868636d4578437a414a42674e564241674d416b4e424d5173770a435159445651514745774a56557a4165467730784f4441314d6a45784d4455774d5442614677307a4d7a41314d6a45784d4455774d5442614d484178496a41670a42674e5642414d4d47556c756447567349464e4857434251513073675547786864475a76636d306751304578476a415942674e5642416f4d45556c75644756730a49454e76636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b474131554543417743513045780a437a414a42674e5642415954416c56544d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a304441516344516741454e53422f377432316c58534f0a3243757a7078773734654a423732457944476757357258437478327456544c7136684b6b367a2b5569525a436e71523770734f766771466553786c6d546c4a6c0a65546d693257597a33714f42757a43427544416642674e5648534d4547444157674251695a517a575770303069664f44744a5653763141624f536347724442530a42674e5648523845537a424a4d45656752614244686b466f64485277637a6f764c324e6c636e52705a6d6c6a5958526c63793530636e567a6447566b633256790a646d6c6a5a584d75615735305a577775593239744c306c756447567355306459556d397664454e424c6d526c636a416442674e5648513445466751556c5739640a7a62306234656c4153636e553944504f4156634c336c517744675944565230504151482f42415144416745474d42494741315564457745422f7751494d4159420a4166384341514177436759494b6f5a497a6a30454177494452774177524149675873566b6930772b6936565947573355462f32327561586530594a446a3155650a6e412b546a44316169356343494359623153416d4435786b66545670766f34556f79695359787244574c6d5552344349394e4b7966504e2b0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949436a7a4343416a53674177494241674955496d554d316c71644e496e7a6737535655723951477a6b6e42717777436759494b6f5a497a6a3045417749770a614445614d4267474131554541777752535735305a5777675530645949464a766233516751304578476a415942674e5642416f4d45556c756447567349454e760a636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155454341774351304578437a414a0a42674e5642415954416c56544d423458445445344d4455794d5445774e4455784d466f58445451354d54497a4d54497a4e546b314f566f77614445614d4267470a4131554541777752535735305a5777675530645949464a766233516751304578476a415942674e5642416f4d45556c756447567349454e76636e4276636d46300a615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155454341774351304578437a414a42674e56424159540a416c56544d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a3044415163445167414543366e45774d4449595a4f6a2f69505773437a61454b69370a314f694f534c52466857476a626e42564a66566e6b59347533496a6b4459594c304d784f346d717379596a6c42616c54565978465032734a424b357a6c4b4f420a757a43427544416642674e5648534d4547444157674251695a517a575770303069664f44744a5653763141624f5363477244425342674e5648523845537a424a0a4d45656752614244686b466f64485277637a6f764c324e6c636e52705a6d6c6a5958526c63793530636e567a6447566b63325679646d6c6a5a584d75615735300a5a577775593239744c306c756447567355306459556d397664454e424c6d526c636a416442674e564851344546675155496d554d316c71644e496e7a673753560a55723951477a6b6e4271777744675944565230504151482f42415144416745474d42494741315564457745422f7751494d4159424166384341514577436759490a4b6f5a497a6a3045417749445351417752674968414f572f35516b522b533943695344634e6f6f774c7550524c735747662f59693747535839344267775477670a41694541344a306c72486f4d732b586f356f2f7358364f39515778485241765a55474f6452513763767152586171493d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/serde-duration/src/lib.rs b/serde-duration/src/lib.rs index 41a223b3..345cc4f6 100644 --- a/serde-duration/src/lib.rs +++ b/serde-duration/src/lib.rs @@ -12,11 +12,11 @@ where if duration == &Duration::MAX { return serializer.serialize_str("never"); } - let (value, unit) = if duration.as_secs() % (24 * 3600) == 0 { + let (value, unit) = if duration.as_secs().is_multiple_of(24 * 3600) { (duration.as_secs() / (24 * 3600), "d") - } else if duration.as_secs() % 3600 == 0 { + } else if duration.as_secs().is_multiple_of(3600) { (duration.as_secs() / 3600, "h") - } else if duration.as_secs() % 60 == 0 { + } else if duration.as_secs().is_multiple_of(60) { (duration.as_secs() / 60, "m") } else { (duration.as_secs(), "s") diff --git a/supervisor/src/process.rs b/supervisor/src/process.rs index 887aac14..c424f9b5 100644 --- a/supervisor/src/process.rs +++ b/supervisor/src/process.rs @@ -167,7 +167,7 @@ impl Process { } } - pub(crate) fn lock(&self) -> MutexGuard { + pub(crate) fn lock(&self) -> MutexGuard<'_, ProcessStateRT> { self.state.lock().or_panic("lock should never fail") } diff --git a/tdx-attest-sys/Cargo.lock b/tdx-attest-sys/Cargo.lock deleted file mode 100644 index 52d7f54c..00000000 --- a/tdx-attest-sys/Cargo.lock +++ /dev/null @@ -1,286 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "bindgen" -version = "0.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "libloading" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" -dependencies = [ - "cfg-if", - "windows-targets", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "prettyplease" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "regex" -version = "1.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "syn" -version = "2.0.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tdx-attest-sys" -version = "0.1.0" -dependencies = [ - "bindgen", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/tdx-attest-sys/Cargo.toml b/tdx-attest-sys/Cargo.toml deleted file mode 100644 index 01cdd7a4..00000000 --- a/tdx-attest-sys/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-FileCopyrightText: © 2024 Phala Network -# -# SPDX-License-Identifier: Apache-2.0 - -[package] -name = "tdx-attest-sys" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] - -[build-dependencies] -bindgen.workspace = true -cc.workspace = true diff --git a/tdx-attest-sys/bindings.h b/tdx-attest-sys/bindings.h deleted file mode 100644 index afd97d06..00000000 --- a/tdx-attest-sys/bindings.h +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: © 2024 Phala Network - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "csrc/tdx_attest.h" diff --git a/tdx-attest-sys/build.rs b/tdx-attest-sys/build.rs deleted file mode 100644 index 9e30e05f..00000000 --- a/tdx-attest-sys/build.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: © 2024 Phala Network -// -// SPDX-License-Identifier: Apache-2.0 - -#![allow(clippy::expect_used)] - -use std::env; -use std::path::PathBuf; - -fn main() { - println!("cargo:rerun-if-changed=csrc/tdx_attest.c"); - println!("cargo:rerun-if-changed=csrc/qgs_msg_lib.cpp"); - let output_path = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set")); - bindgen::Builder::default() - .header("bindings.h") - .default_enum_style(bindgen::EnumVariation::ModuleConsts) - .generate() - .expect("Unable to generate bindings") - .write_to_file(output_path.join("bindings.rs")) - .expect("Couldn't write bindings!"); - cc::Build::new() - .file("csrc/tdx_attest.c") - .file("csrc/qgs_msg_lib.cpp") - .compile("tdx_attest"); -} diff --git a/tdx-attest-sys/csrc/README.txt b/tdx-attest-sys/csrc/README.txt deleted file mode 100644 index 16af54a4..00000000 --- a/tdx-attest-sys/csrc/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -The C source code is forked from: https://github.com/intel/SGXDataCenterAttestationPrimitives/tree/DCAP_1.21/QuoteGeneration/quote_wrapper/tdx_attest -without functionality modification. diff --git a/tdx-attest-sys/csrc/qgs_msg_lib.cpp b/tdx-attest-sys/csrc/qgs_msg_lib.cpp deleted file mode 100644 index 254f673f..00000000 --- a/tdx-attest-sys/csrc/qgs_msg_lib.cpp +++ /dev/null @@ -1,1073 +0,0 @@ -/* - * Copyright (C) 2011-2021 Intel Corporation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Intel Corporation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -#include "qgs_msg_lib.h" - -#include -#include - -const uint32_t QGS_MSG_LIB_MAJOR_VER = 1; -const uint32_t QGS_MSG_LIB_MINOR_VER = 1; - -void qgs_msg_free(void *p_buf) { - free(p_buf); -} - -/** - * @brief Generate serialized get_quote request - * - * @param p_report Cannot be NULL - * @param report_size Cannot be 0 - * @param p_id_list Can be NULL - * @param id_list_size Can be 0 - * @param pp_req returned serialized buffer, valid only if the return code is QGS_MSG_SUCCESS - * @param p_req_size return size of the serialized buffer, valid only if the return code is QGS_MSG_SUCCESS - * @return qgs_msg_error_t - */ -qgs_msg_error_t qgs_msg_gen_get_quote_req( - const uint8_t *p_report, uint32_t report_size, - const uint8_t *p_id_list, uint32_t id_list_size, - uint8_t **pp_req, uint32_t *p_req_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_quote_req_t *p_req = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - - if (!p_report || !report_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if ((!p_id_list && id_list_size) || (p_id_list && !id_list_size)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_req || !p_req_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - temp = sizeof(*p_req); - temp += report_size; - temp += id_list_size; - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_req = (qgs_msg_get_quote_req_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_req) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_req->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_req->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_req->header.type = GET_QUOTE_REQ; - p_req->header.size = buf_size; - p_req->header.error_code = 0; - - p_req->report_size = report_size; - p_req->id_list_size = id_list_size; - memcpy(p_req->report_id_list, p_report, report_size); - if (id_list_size) { - memcpy(p_req->report_id_list + report_size, p_id_list, id_list_size); - } - *pp_req = (uint8_t *)p_req; - *p_req_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point : - return ret; -} - -/** - * @brief Generate serialized get_collateral request - * - * @param p_fsmpc Cannot be NULL - * @param fsmpc_size Cannot be 0 - * @param p_pckca Cannot be NULL - * @param pckca_size Cannot be 0 - * @param pp_req returned serialized buffer, valid only if the return code is QGS_MSG_SUCCESS - * @param p_req_size return size of the serialized buffer, valid only if the return code is QGS_MSG_SUCCESS - * @return qgs_msg_error_t - */ -qgs_msg_error_t qgs_msg_gen_get_collateral_req( - const uint8_t *p_fsmpc, uint32_t fsmpc_size, - const uint8_t *p_pckca, uint32_t pckca_size, - uint8_t **pp_req, uint32_t *p_req_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_collateral_req_t *p_req = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - - if (!p_fsmpc || !fsmpc_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!p_pckca || !pckca_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_req || !p_req_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - temp = sizeof(*p_req); - temp += fsmpc_size; - temp += pckca_size; - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_req = (qgs_msg_get_collateral_req_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_req) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_req->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_req->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_req->header.type = GET_COLLATERAL_REQ; - p_req->header.size = buf_size; - p_req->header.error_code = 0; - - p_req->fsmpc_size = fsmpc_size; - p_req->pckca_size = pckca_size; - memcpy(p_req->fsmpc_pckca, p_fsmpc, fsmpc_size); - memcpy(p_req->fsmpc_pckca + fsmpc_size, p_pckca, pckca_size); - - *pp_req = (uint8_t *)p_req; - *p_req_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_quote_req( - const uint8_t *p_serialized_req, uint32_t size, - const uint8_t **pp_report, uint32_t *p_report_size, - const uint8_t **pp_id_list, uint32_t *p_id_list_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_quote_req_t *p_req = NULL; - uint64_t temp = 0; - - if (!p_serialized_req || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_report || !p_report_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_id_list || !p_id_list_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_quote_req_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_req = (qgs_msg_get_quote_req_t *)p_serialized_req; - // Only major version is checked, minor change is deemed as compatible. - if (p_req->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_req->header.type != GET_QUOTE_REQ) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_req->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_req->header.error_code != 0) { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - if (!p_req->report_size) { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - temp = sizeof(qgs_msg_get_quote_req_t); - temp += p_req->report_size; - temp += p_req->id_list_size; - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_req->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - *pp_report = p_req->report_id_list; - if (p_req->id_list_size) { - *pp_id_list = p_req->report_id_list + p_req->report_size; - } else { - *pp_id_list = NULL; - } - - *p_report_size = p_req->report_size; - *p_id_list_size = p_req->id_list_size; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_collateral_req( - const uint8_t *p_serialized_req, uint32_t size, - const uint8_t **pp_fsmpc, uint32_t *p_fsmpc_size, - const uint8_t **pp_pckca, uint32_t *p_pckca_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_collateral_req_t *p_req = NULL; - uint64_t temp = 0; - - if (!p_serialized_req || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_fsmpc || !p_fsmpc_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_pckca || !p_pckca_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_collateral_req_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_req = (qgs_msg_get_collateral_req_t *)p_serialized_req; - // Only major version is checked, minor change is deemed as compatible. - if (p_req->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_req->header.type != GET_COLLATERAL_REQ) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_req->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_req->header.error_code != 0) { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - if (!p_req->fsmpc_size || !p_req->pckca_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - temp = sizeof(qgs_msg_get_collateral_req_t); - temp += p_req->fsmpc_size; - temp += p_req->pckca_size; - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_req->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - *pp_fsmpc = p_req->fsmpc_pckca; - *pp_pckca = p_req->fsmpc_pckca + p_req->fsmpc_size; - - *p_fsmpc_size = p_req->fsmpc_size; - *p_pckca_size = p_req->pckca_size; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_gen_error_resp( - uint32_t error_code, uint32_t type, - uint8_t **pp_resp, uint32_t *p_resp_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - uint32_t buf_size = 0; - qgs_msg_header_t *p_resp = NULL; - if (error_code == QGS_MSG_SUCCESS) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_resp || !p_resp_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - switch (type) { - case GET_QUOTE_RESP: - buf_size = sizeof(qgs_msg_get_quote_resp_t); - break; - case GET_COLLATERAL_RESP: - buf_size = sizeof(qgs_msg_get_collateral_resp_t); - break; - case GET_PLATFORM_INFO_RESP: - buf_size = sizeof(qgs_msg_get_platform_info_resp_t); - break; - default: - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - p_resp = (qgs_msg_header_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_resp) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_resp->major_version = QGS_MSG_LIB_MAJOR_VER; - p_resp->minor_version = QGS_MSG_LIB_MINOR_VER; - p_resp->type = type; - p_resp->size = buf_size; - p_resp->error_code = error_code; - - *pp_resp = (uint8_t *)p_resp; - *p_resp_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_gen_get_quote_resp( - const uint8_t *p_selected_id, uint32_t id_size, - const uint8_t *p_quote, uint32_t quote_size, - uint8_t **pp_resp, uint32_t *p_resp_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_quote_resp_t *p_resp = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - - if (!pp_resp || !p_resp_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if ((!p_selected_id && id_size) || (p_selected_id && !id_size)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!p_quote || !quote_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - temp = sizeof(*p_resp); - temp += id_size; - temp += quote_size; - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_resp = (qgs_msg_get_quote_resp_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_resp) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_resp->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_resp->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_resp->header.type = GET_QUOTE_RESP; - p_resp->header.size = buf_size; - p_resp->header.error_code = QGS_MSG_SUCCESS; - - p_resp->selected_id_size = id_size; - p_resp->quote_size = quote_size; - if (id_size) { - memcpy(p_resp->id_quote, p_selected_id, id_size); - } - memcpy(p_resp->id_quote + id_size, p_quote, quote_size); - - *pp_resp = (uint8_t *)p_resp; - *p_resp_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point : - return ret; -} - -qgs_msg_error_t qgs_msg_gen_get_collateral_resp( - uint16_t major_version, uint16_t minor_version, - const uint8_t *p_pck_crl_issuer_chain, uint32_t pck_crl_issuer_chain_size, - const uint8_t *p_root_ca_crl, uint32_t root_ca_crl_size, - const uint8_t *p_pck_crl, uint32_t pck_crl_size, - const uint8_t *p_tcb_info_issuer_chain, uint32_t tcb_info_issuer_chain_size, - const uint8_t *p_tcb_info, uint32_t tcb_info_size, - const uint8_t *p_qe_identity_issuer_chain, uint32_t qe_identity_issuer_chain_size, - const uint8_t *p_qe_identity, uint32_t qe_identity_size, - uint8_t **pp_resp, uint32_t *p_resp_size, - const qgs_msg_header_t *p_req_header) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_collateral_resp_t *p_resp = NULL; - uint8_t *p_ptr = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - - if (!pp_resp || !p_resp_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - //TODO major_version and minor_version are ignored here, is 0.0 a valid version? - if (!p_pck_crl_issuer_chain || !pck_crl_issuer_chain_size - || !p_root_ca_crl || !root_ca_crl_size - || !p_pck_crl || !pck_crl_size - || !p_tcb_info_issuer_chain || !tcb_info_issuer_chain_size - || !p_tcb_info || !tcb_info_size - || !p_qe_identity_issuer_chain || !qe_identity_issuer_chain_size - || !p_qe_identity || !qe_identity_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (p_req_header->major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - if (p_req_header->minor_version == 0) { - temp = sizeof(major_version) + sizeof(minor_version) + sizeof(*p_resp); - } else { - temp = sizeof(*p_resp); - } - temp += pck_crl_issuer_chain_size; - temp += root_ca_crl_size; - temp += pck_crl_size; - temp += tcb_info_issuer_chain_size; - temp += tcb_info_size; - temp += qe_identity_issuer_chain_size; - temp += qe_identity_size; - - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_resp = (qgs_msg_get_collateral_resp_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_resp) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_resp->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_resp->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_resp->header.type = GET_COLLATERAL_RESP; - p_resp->header.size = buf_size; - p_resp->header.error_code = QGS_MSG_SUCCESS; - - p_resp->major_version = major_version; - p_resp->minor_version = minor_version; - p_ptr = p_resp->collaterals; - if (pck_crl_issuer_chain_size) { - p_resp->pck_crl_issuer_chain_size = pck_crl_issuer_chain_size; - memcpy(p_ptr, p_pck_crl_issuer_chain, pck_crl_issuer_chain_size); - p_ptr += pck_crl_issuer_chain_size; - } - - if (root_ca_crl_size) { - p_resp->root_ca_crl_size = root_ca_crl_size; - memcpy(p_ptr, p_root_ca_crl, root_ca_crl_size); - p_ptr += root_ca_crl_size; - } - - if (pck_crl_size) { - p_resp->pck_crl_size = pck_crl_size; - memcpy(p_ptr, p_pck_crl, pck_crl_size); - p_ptr += pck_crl_size; - } - - if (tcb_info_issuer_chain_size) { - p_resp->tcb_info_issuer_chain_size = tcb_info_issuer_chain_size; - memcpy(p_ptr, p_tcb_info_issuer_chain, tcb_info_issuer_chain_size); - p_ptr += tcb_info_issuer_chain_size; - } - - if (tcb_info_size) { - p_resp->tcb_info_size = tcb_info_size; - memcpy(p_ptr, p_tcb_info, tcb_info_size); - p_ptr += tcb_info_size; - } - - if (qe_identity_issuer_chain_size) { - p_resp->qe_identity_issuer_chain_size = qe_identity_issuer_chain_size; - memcpy(p_ptr, p_qe_identity_issuer_chain, qe_identity_issuer_chain_size); - p_ptr += qe_identity_issuer_chain_size; - } - - if (root_ca_crl_size) { - p_resp->qe_identity_size = qe_identity_size; - memcpy(p_ptr, p_qe_identity, qe_identity_size); - } - - *pp_resp = (uint8_t *)p_resp; - *p_resp_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_quote_resp( - const uint8_t *p_serialized_resp, uint32_t size, - const uint8_t **pp_selected_id, uint32_t *p_id_size, - const uint8_t **pp_quote, uint32_t *p_quote_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_quote_resp_t *p_resp = NULL; - uint64_t temp = 0; - - if (!p_serialized_resp || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_selected_id || !p_id_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_quote || !p_quote_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_quote_resp_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_resp = (qgs_msg_get_quote_resp_t *)p_serialized_resp; - // Only major version is checked, minor change is deemed as compatible. - if (p_resp->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_resp->header.type != GET_QUOTE_RESP) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_resp->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - temp = sizeof(qgs_msg_get_quote_resp_t); - temp += p_resp->selected_id_size; - temp += p_resp->quote_size; - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_resp->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_resp->header.error_code == QGS_MSG_SUCCESS) { - if (!p_resp->quote_size) { - // It makes no sense to return success and an empty quote - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - if (p_resp->selected_id_size) { - *pp_selected_id = p_resp->id_quote; - *p_id_size = p_resp->selected_id_size; - } else { - *pp_selected_id = NULL; - *p_id_size = 0; - } - *pp_quote = p_resp->id_quote + p_resp->selected_id_size; - *p_quote_size = p_resp->quote_size; - } else if (p_resp->header.error_code < QGS_MSG_ERROR_MAX) { - if (p_resp->selected_id_size || p_resp->quote_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *pp_selected_id = NULL; - *p_id_size = 0; - *pp_quote = NULL; - *p_quote_size = 0; - } else { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - ret = QGS_MSG_SUCCESS; -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_collateral_resp( - const uint8_t *p_serialized_resp, uint32_t size, - uint16_t *p_major_version, uint16_t *p_minor_version, - const uint8_t **pp_pck_crl_issuer_chain, uint32_t *p_pck_crl_issuer_chain_size, - const uint8_t **pp_root_ca_crl, uint32_t *p_root_ca_crl_size, - const uint8_t **pp_pck_crl, uint32_t *p_pck_crl_size, - const uint8_t **pp_tcb_info_issuer_chain, uint32_t *p_tcb_info_issuer_chain_size, - const uint8_t **pp_tcb_info, uint32_t *p_tcb_info_size, - const uint8_t **pp_qe_identity_issuer_chain, uint32_t *p_qe_identity_issuer_chain_size, - const uint8_t **pp_qe_identity, uint32_t *p_qe_identity_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_collateral_resp_t *p_resp = NULL; - uint64_t temp = 0; - - if (!p_serialized_resp || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!p_major_version || !p_minor_version - || !pp_pck_crl_issuer_chain || !p_pck_crl_issuer_chain_size - || !pp_root_ca_crl || !p_root_ca_crl_size - || !pp_pck_crl || !p_pck_crl_size - || !pp_tcb_info_issuer_chain || !p_tcb_info_issuer_chain_size - || !pp_tcb_info || !p_tcb_info_size - || !pp_qe_identity_issuer_chain || !p_qe_identity_issuer_chain_size - || !pp_qe_identity || !p_qe_identity_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_collateral_resp_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_resp = (qgs_msg_get_collateral_resp_t *)p_serialized_resp; - // Only major version is checked, minor change is deemed as compatible. - if (p_resp->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_resp->header.type != GET_COLLATERAL_RESP) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_resp->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_resp->header.minor_version == 0) { - temp = sizeof(*p_resp) + sizeof(p_resp->major_version) + sizeof(p_resp->minor_version); - } else { - temp = sizeof(*p_resp); - } - temp += p_resp->pck_crl_issuer_chain_size; - temp += p_resp->root_ca_crl_size; - temp += p_resp->pck_crl_size; - temp += p_resp->tcb_info_issuer_chain_size; - temp += p_resp->tcb_info_size; - temp += p_resp->qe_identity_issuer_chain_size; - temp += p_resp->qe_identity_size; - - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_resp->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_resp->header.error_code == QGS_MSG_SUCCESS) { - // It makes no sense to return success and empty collaterals - if (!p_resp->pck_crl_issuer_chain_size - || !p_resp->root_ca_crl_size - || !p_resp->pck_crl_size - || !p_resp->tcb_info_issuer_chain_size - || !p_resp->tcb_info_size - || !p_resp->qe_identity_issuer_chain_size - || !p_resp->qe_identity_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *p_major_version = p_resp->major_version; - *p_minor_version = p_resp->minor_version; - - *pp_pck_crl_issuer_chain = p_resp->collaterals; - *p_pck_crl_issuer_chain_size = p_resp->pck_crl_issuer_chain_size; - - *pp_root_ca_crl = *pp_pck_crl_issuer_chain + p_resp->pck_crl_issuer_chain_size; - *p_root_ca_crl_size = p_resp->root_ca_crl_size; - - *pp_pck_crl = *pp_root_ca_crl + p_resp->root_ca_crl_size; - *p_pck_crl_size = p_resp->pck_crl_size; - - *pp_tcb_info_issuer_chain = *pp_pck_crl + p_resp->pck_crl_size; - *p_tcb_info_issuer_chain_size = p_resp->tcb_info_issuer_chain_size; - - *pp_tcb_info = *pp_tcb_info_issuer_chain + p_resp->tcb_info_issuer_chain_size; - *p_tcb_info_size = p_resp->tcb_info_size; - - *pp_qe_identity_issuer_chain = *pp_tcb_info + p_resp->tcb_info_size; - *p_qe_identity_issuer_chain_size = p_resp->qe_identity_issuer_chain_size; - - *pp_qe_identity = *pp_qe_identity_issuer_chain + p_resp->qe_identity_issuer_chain_size; - *p_qe_identity_size = p_resp->qe_identity_size; - - } else if (p_resp->header.error_code < QGS_MSG_ERROR_MAX) { - if (p_resp->pck_crl_issuer_chain_size - || p_resp->root_ca_crl_size - || p_resp->pck_crl_size - || p_resp->tcb_info_issuer_chain_size - || p_resp->tcb_info_size - || p_resp->qe_identity_issuer_chain_size - || p_resp->qe_identity_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *p_major_version = 0; - *p_minor_version = 0; - - *pp_pck_crl_issuer_chain = NULL; - *p_pck_crl_issuer_chain_size = 0; - *pp_root_ca_crl = NULL; - *p_root_ca_crl_size = 0; - *pp_pck_crl = NULL; - *p_pck_crl_size = 0; - *pp_tcb_info_issuer_chain = NULL; - *p_tcb_info_issuer_chain_size = 0; - *pp_tcb_info = NULL; - *p_tcb_info_size = 0; - *pp_qe_identity_issuer_chain = NULL; - *p_qe_identity_issuer_chain_size = 0; - *pp_qe_identity = NULL; - *p_qe_identity_size = 0; - } else { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - ret = QGS_MSG_SUCCESS; -ret_point: - return ret; -} - -uint32_t qgs_msg_get_type(const uint8_t *p_serialized_msg, uint32_t size, uint32_t *p_type) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - const qgs_msg_header_t *p_header = (const qgs_msg_header_t *)p_serialized_msg; - - if (size < sizeof(qgs_msg_header_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (p_header->major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - if (p_header->type >= QGS_MSG_TYPE_MAX) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - *p_type = p_header->type; - ret = QGS_MSG_SUCCESS; -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_gen_get_platform_info_req( - uint8_t **pp_req, uint32_t *p_req_size) -{ - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_platform_info_req_t *p_req = NULL; - - if (!pp_req || !p_req_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_req = (qgs_msg_get_platform_info_req_t *)calloc(sizeof(*p_req), sizeof(uint8_t)); - if (!p_req) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_req->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_req->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_req->header.type = GET_PLATFORM_INFO_REQ; - p_req->header.size = sizeof(*p_req); - p_req->header.error_code = 0; - - *pp_req = (uint8_t *)p_req; - *p_req_size = sizeof(*p_req); - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_platform_info_req( - const uint8_t *p_serialized_req, uint32_t size) -{ - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_platform_info_req_t *p_req = NULL; - - if (!p_serialized_req || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_platform_info_req_t - if (size < sizeof(qgs_msg_get_platform_info_req_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_req = (qgs_msg_get_platform_info_req_t *)p_serialized_req; - // Only major version is checked, minor change is deemed as compatible. - if (p_req->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_req->header.type != GET_PLATFORM_INFO_REQ) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_req->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_req->header.error_code != 0) { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_gen_get_platform_info_resp( - uint16_t tdqe_isvsvn, uint16_t pce_isvsvn, - const uint8_t *p_platform_id, uint32_t platform_id_size, - const uint8_t *p_cpusvn, uint32_t cpusvn_size, - uint8_t **pp_resp, uint32_t *p_resp_size) -{ - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_platform_info_resp_t *p_resp = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - uint8_t *p_ptr = NULL; - - if (!pp_resp || !p_resp_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if ((!p_platform_id || !platform_id_size)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!p_cpusvn || !cpusvn_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - temp = sizeof(tdqe_isvsvn) + sizeof(pce_isvsvn) + sizeof(*p_resp); - temp += platform_id_size + cpusvn_size; - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_resp = (qgs_msg_get_platform_info_resp_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_resp) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_resp->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_resp->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_resp->header.type = GET_PLATFORM_INFO_RESP; - p_resp->header.size = buf_size; - p_resp->header.error_code = QGS_MSG_SUCCESS; - - p_resp->platform_id_size = platform_id_size; - p_resp->cpusvn_size = cpusvn_size; - - p_resp->tdqe_isvsvn = tdqe_isvsvn; - p_resp->pce_isvsvn = pce_isvsvn; - - p_ptr = p_resp->platform_id_cpusvn; - memcpy(p_ptr, p_platform_id, platform_id_size); - p_ptr += platform_id_size; - - memcpy(p_ptr, p_cpusvn, cpusvn_size); - - *pp_resp = (uint8_t *)p_resp; - *p_resp_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_platform_info_resp( - const uint8_t *p_serialized_resp, uint32_t size, - uint16_t *p_tdqe_isvsvn, uint16_t *p_pce_isvsvn, - const uint8_t **pp_platform_id, uint32_t *p_platform_id_size, - const uint8_t **pp_cpusvn, uint32_t *p_cpusvn_size) -{ - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_platform_info_resp_t *p_resp = NULL; - uint64_t temp = 0; - - if (!p_serialized_resp || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_platform_id || !p_platform_id_size - || !pp_cpusvn || !p_cpusvn_size - || !p_tdqe_isvsvn || !p_pce_isvsvn) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_platform_info_resp_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_resp = (qgs_msg_get_platform_info_resp_t *)p_serialized_resp; - // Only major version is checked, minor change is deemed as compatible. - if (p_resp->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_resp->header.type != GET_PLATFORM_INFO_RESP) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_resp->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - temp = sizeof(p_resp->tdqe_isvsvn) + sizeof(p_resp->pce_isvsvn) + sizeof(*p_resp); - temp += p_resp->platform_id_size; - temp += p_resp->cpusvn_size; - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_resp->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_resp->header.error_code == QGS_MSG_SUCCESS) { - // It makes no sense to return success and empty platform info - if (!p_resp->platform_id_size || !p_resp->cpusvn_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *p_tdqe_isvsvn = p_resp->tdqe_isvsvn; - *p_pce_isvsvn = p_resp->pce_isvsvn; - - *pp_platform_id = p_resp->platform_id_cpusvn; - *p_platform_id_size = p_resp->platform_id_size; - - *pp_cpusvn = *pp_platform_id + p_resp->platform_id_size; - *p_cpusvn_size = p_resp->cpusvn_size; - - } else if (p_resp->header.error_code < QGS_MSG_ERROR_MAX) { - if (p_resp->platform_id_size || p_resp->cpusvn_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *p_tdqe_isvsvn = 0; - *p_pce_isvsvn = 0; - - *pp_platform_id = NULL; - *p_platform_id_size = 0; - - *pp_cpusvn = NULL; - *p_cpusvn_size = 0; - } else { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - ret = QGS_MSG_SUCCESS; -ret_point: - return ret; -} \ No newline at end of file diff --git a/tdx-attest-sys/csrc/qgs_msg_lib.h b/tdx-attest-sys/csrc/qgs_msg_lib.h deleted file mode 100644 index c235806c..00000000 --- a/tdx-attest-sys/csrc/qgs_msg_lib.h +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2011-2021 Intel Corporation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Intel Corporation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - - -/** - * File: qgs_msg_lib.h - * - * Description: Message and API definitions for TDX QGS messages - * - */ -#ifndef _QGS_MSG_DEF_H_ -#define _QGS_MSG_DEF_H_ -#include - -#ifndef QGS_MSG_MK_ERROR -#define QGS_MSG_MK_ERROR(x) (0x00012000 | (x)) -#endif - -#pragma pack(push, 1) - -/** Possible errors generated by the qgs message library. */ -typedef enum _qgs_msg_error_t { - QGS_MSG_SUCCESS = 0x0000, ///< Success - QGS_MSG_ERROR_UNEXPECTED = QGS_MSG_MK_ERROR(0x0001), ///< Unexpected error - QGS_MSG_ERROR_OUT_OF_MEMORY = QGS_MSG_MK_ERROR(0x0002), ///< Not enough memory is available to complete this operation - QGS_MSG_ERROR_INVALID_PARAMETER = QGS_MSG_MK_ERROR(0x0003), ///< The parameter is incorrect - QGS_MSG_ERROR_INVALID_VERSION = QGS_MSG_MK_ERROR(0x0004), ///< Unrecognized version of serialized data - QGS_MSG_ERROR_INVALID_TYPE = QGS_MSG_MK_ERROR(0x0005), ///< Invalid message type found - QGS_MSG_ERROR_INVALID_SIZE = QGS_MSG_MK_ERROR(0x0006), ///< Invalid message size found - QGS_MSG_ERROR_INVALID_CODE = QGS_MSG_MK_ERROR(0x0007), ///< Invalid error code - - QGS_MSG_ERROR_MAX, ///< Indicate max error to allow better translation. -} qgs_msg_error_t; - -typedef enum _qgs_msg_type_t { - GET_QUOTE_REQ = 0, - GET_QUOTE_RESP = 1, - GET_COLLATERAL_REQ = 2, - GET_COLLATERAL_RESP = 3, - GET_PLATFORM_INFO_REQ = 4, - GET_PLATFORM_INFO_RESP = 5, - QGS_MSG_TYPE_MAX -} qgs_msg_type_t; - -typedef struct _qgs_msg_header_t { - uint16_t major_version; - uint16_t minor_version; - uint32_t type; - uint32_t size; // size of the whole message, include this header, in byte - uint32_t error_code; // used in response only -} qgs_msg_header_t; - -typedef struct _qgs_msg_get_quote_req_t { - qgs_msg_header_t header; // header.type = GET_QUOTE_REQ - uint32_t report_size; // cannot be 0 - uint32_t id_list_size; // length of id_list, in byte, can be 0 - uint8_t report_id_list[]; // report followed by id list -} qgs_msg_get_quote_req_t; - -typedef struct _qgs_msg_get_quote_resp_s { - qgs_msg_header_t header; // header.type = GET_QUOTE_RESP - uint32_t selected_id_size; // can be 0 in case only one id is sent in request - uint32_t quote_size; // length of quote_data, in byte - uint8_t id_quote[]; // selected id followed by quote -} qgs_msg_get_quote_resp_t; - -typedef struct _qgs_msg_get_collateral_req_t { - qgs_msg_header_t header; // header.type = GET_COLLATERAL_REQ - uint32_t fsmpc_size; // length of fsmpc, in byte - uint32_t pckca_size; // length of pckca, in byte - uint8_t fsmpc_pckca[]; // fsmpc followed by pckca -} qgs_msg_get_collateral_req_t; - -typedef struct _qgs_msg_get_collateral_resp_s { - qgs_msg_header_t header; // header.type = GET_COLLATERAL_RESP - uint16_t major_version; - uint16_t minor_version; - uint32_t pck_crl_issuer_chain_size; - uint32_t root_ca_crl_size; - uint32_t pck_crl_size; - uint32_t tcb_info_issuer_chain_size; - uint32_t tcb_info_size; - uint32_t qe_identity_issuer_chain_size; - uint32_t qe_identity_size; - uint8_t collaterals[]; // payload filled in same order as upper sizes parameters -} qgs_msg_get_collateral_resp_t; - -typedef struct _qgs_msg_get_platform_info_req_t { - qgs_msg_header_t header; // header.type = GET_PLATFORM_INFO_REQ -} qgs_msg_get_platform_info_req_t; - -typedef struct _qgs_msg_get_platform_info_resp_s { - qgs_msg_header_t header; // header.type = GET_PLATFORM_INFO_RESP - uint16_t tdqe_isvsvn; - uint16_t pce_isvsvn; - uint32_t platform_id_size; - uint32_t cpusvn_size; - uint8_t platform_id_cpusvn[]; -} qgs_msg_get_platform_info_resp_t; - -#pragma pack(pop) - -#if defined(__cplusplus) -extern "C" { -#endif -void qgs_msg_free(void *buf); - -qgs_msg_error_t qgs_msg_gen_get_quote_req( - const uint8_t *p_report, uint32_t report_size, - const uint8_t *p_id_list, uint32_t id_list_size, - uint8_t **pp_req, uint32_t *p_req_size); -qgs_msg_error_t qgs_msg_gen_get_collateral_req( - const uint8_t *p_fsmpc, uint32_t fsmpc_size, - const uint8_t *p_pckca, uint32_t pckca_size, - uint8_t **pp_req, uint32_t *p_req_size); - -qgs_msg_error_t qgs_msg_inflate_get_quote_req( - const uint8_t *p_serialized_req, uint32_t size, - const uint8_t **pp_report, uint32_t *p_report_size, - const uint8_t **pp_id_list, uint32_t *p_id_list_size); -qgs_msg_error_t qgs_msg_inflate_get_collateral_req( - const uint8_t *p_serialized_req, uint32_t size, - const uint8_t **pp_fsmpc, uint32_t *p_fsmpc_size, - const uint8_t **pp_pckca, uint32_t *p_pckca_size); - -qgs_msg_error_t qgs_msg_gen_error_resp( - uint32_t error_code, uint32_t type, - uint8_t **pp_resp, uint32_t *p_resp_size); - -qgs_msg_error_t qgs_msg_gen_get_quote_resp( - const uint8_t *p_selected_id, uint32_t id_size, - const uint8_t *p_quote, uint32_t quote_size, - uint8_t **pp_resp, uint32_t *p_resp_size); -qgs_msg_error_t qgs_msg_gen_get_collateral_resp( - uint16_t major_version, uint16_t minor_version, - const uint8_t *p_pck_crl_issuer_chain, uint32_t pck_crl_issuer_chain_size, - const uint8_t *p_root_ca_crl, uint32_t root_ca_crl_size, - const uint8_t *p_pck_crl, uint32_t pck_crl_size, - const uint8_t *p_tcb_info_issuer_chain, uint32_t tcb_info_issuer_chain_size, - const uint8_t *p_tcb_info, uint32_t tcb_info_size, - const uint8_t *p_qe_identity_issuer_chain, uint32_t qe_identity_issuer_chain_size, - const uint8_t *p_qe_identity, uint32_t qe_identity_size, - uint8_t **pp_resp, uint32_t *p_resp_size, - const qgs_msg_header_t *p_req_header); - -qgs_msg_error_t qgs_msg_inflate_get_quote_resp( - const uint8_t *p_serialized_resp, uint32_t size, - const uint8_t **pp_selected_id, uint32_t *p_id_size, - const uint8_t **pp_quote, uint32_t *p_quote_size); -qgs_msg_error_t qgs_msg_inflate_get_collateral_resp( - const uint8_t *p_serialized_resp, uint32_t size, - uint16_t *p_major_version, uint16_t *p_minor_version, - const uint8_t **pp_pck_crl_issuer_chain, uint32_t *p_pck_crl_issuer_chain_size, - const uint8_t **pp_root_ca_crl, uint32_t *p_root_ca_crl_size, - const uint8_t **pp_pck_crl, uint32_t *p_pck_crl_size, - const uint8_t **pp_tcb_info_issuer_chain, uint32_t *p_tcb_info_issuer_chain_size, - const uint8_t **pp_tcb_info, uint32_t *p_tcb_info_size, - const uint8_t **pp_qe_identity_issuer_chain, uint32_t *p_qe_identity_issuer_chain_size, - const uint8_t **pp_qe_identity, uint32_t *p_qe_identity_size); -uint32_t qgs_msg_get_type(const uint8_t *p_serialized_msg, uint32_t size, uint32_t *p_type); - -qgs_msg_error_t qgs_msg_gen_get_platform_info_req( - uint8_t **pp_req, uint32_t *p_req_size); -qgs_msg_error_t qgs_msg_inflate_get_platform_info_req( - const uint8_t *p_serialized_req, uint32_t size); -qgs_msg_error_t qgs_msg_gen_get_platform_info_resp( - uint16_t tdqe_isvsvn, uint16_t pce_isvsvn, - const uint8_t *p_platform_id, uint32_t platform_id_size, - const uint8_t *p_cpusvn, uint32_t cpusvn_size, - uint8_t **pp_resp, uint32_t *p_resp_size); -qgs_msg_error_t qgs_msg_inflate_get_platform_info_resp( - const uint8_t *p_serialized_resp, uint32_t size, - uint16_t *p_tdqe_isvsvn, uint16_t *p_pce_isvsvn, - const uint8_t **pp_platform_id, uint32_t *p_platform_id_size, - const uint8_t **pp_cpusvn, uint32_t *p_cpusvn_size); - -#if defined(__cplusplus) -} -#endif - -#endif \ No newline at end of file diff --git a/tdx-attest-sys/csrc/tdx_attest.c b/tdx-attest-sys/csrc/tdx_attest.c deleted file mode 100644 index 5b6a9399..00000000 --- a/tdx-attest-sys/csrc/tdx_attest.c +++ /dev/null @@ -1,1045 +0,0 @@ -/* - * Copyright (C) 2011-2021 Intel Corporation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Intel Corporation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -#ifndef SERVTD_ATTEST - -#define _GNU_SOURCE -#include -#include -#include "qgs_msg_lib.h" -#include "tdx_attest.h" - -#include -#include -#include -#include -#include // For strtoul -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define TDX_ATTEST_DEV_PATH "/dev/tdx_guest" -#define CFG_FILE_PATH "/etc/tdx-attest.conf" -#define DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV "DCAP_TDX_QUOTE_CONFIGFS_PATH" -#define QUOTE_CONFIGFS_PATH "/sys/kernel/config/tsm/report" -#define DEFAULT_DCAP_TDX_QUOTE_CONFIGFS_PATH QUOTE_CONFIGFS_PATH"/com.intel.dcap" - -// TODO: Should include kernel header, but the header file are included by -// different package in differnt distro, and installed in different locations. -// So add these defines here. Need to remove them later when kernel header -// became stable. - -#define TDX_CMD_GET_REPORT0 _IOWR('T', 1, struct tdx_report_req) -#ifdef V3_DRIVER -#define TDX_CMD_VERIFY_REPORT _IOWR('T', 2, struct tdx_verify_report_req) -#define TDX_CMD_EXTEND_RTMR _IOW('T', 3, struct tdx_extend_rtmr_req) -#define TDX_CMD_GET_QUOTE _IOWR('T', 4, struct tdx_quote_req) -#else -#define TDX_CMD_VERIFY_REPORT _IOR('T', 2, struct tdx_verify_report_req) -#define TDX_CMD_EXTEND_RTMR _IOR('T', 3, struct tdx_extend_rtmr_req) -#define TDX_CMD_GET_QUOTE _IOR('T', 4, struct tdx_quote_req) -#endif - - -/* TD Quote status codes */ -#define GET_QUOTE_SUCCESS 0 -#define GET_QUOTE_IN_FLIGHT 0xffffffffffffffff -#define GET_QUOTE_ERROR 0x8000000000000000 -#define GET_QUOTE_SERVICE_UNAVAILABLE 0x8000000000000001 - -#define TDX_EXTEND_RTMR_DATA_LEN 48 - -#ifdef DEBUG -#define TDX_TRACE \ - do { \ - fprintf(stderr, "\n[%s:%d] ", __FILE__, __LINE__); \ - perror(NULL); \ - }while(0) -#else -#define TDX_TRACE -#endif - -struct tdx_report_req { - __u8 reportdata[TDX_REPORT_DATA_SIZE]; - __u8 tdreport[TDX_REPORT_SIZE]; -}; - -struct tdx_extend_rtmr_req { - __u8 data[TDX_EXTEND_RTMR_DATA_LEN]; - __u8 index; -}; - -struct tdx_quote_hdr { - /* Quote version, filled by TD */ - __u64 version; - /* Status code of Quote request, filled by VMM */ - __u64 status; - /* Length of TDREPORT, filled by TD */ - __u32 in_len; - /* Length of Quote, filled by VMM */ - __u32 out_len; - /* Actual Quote data or TDREPORT on input */ - __u64 data[0]; -}; - -struct tdx_quote_req { - __u64 buf; - __u64 len; -}; - -static const unsigned HEADER_SIZE = 4; -static const size_t REQ_BUF_SIZE = 4 * 4 * 1024; // 4 pages -static const size_t QUOTE_BUF_SIZE = 8 * 1024; //8K -static const size_t QUOTE_MIN_SIZE = 1020; - -static const tdx_uuid_t g_intel_tdqe_uuid = {TDX_SGX_ECDSA_ATTESTATION_ID}; - -static unsigned int get_vsock_port(void) -{ - FILE *p_config_fd = NULL; - char *p_line = NULL; - char *p = NULL; - size_t line_len = 0; - long long_num = 0; - unsigned int port = 0; - - p_config_fd = fopen(CFG_FILE_PATH, "r"); - if (NULL == p_config_fd) { - TDX_TRACE; - return 0; - } - while(-1 != getline(&p_line, &line_len, p_config_fd)) { - char temp[11] = {0}; - int number = 0; - int ret = sscanf(p_line, " %10[#]", temp); - if (ret == 1) { - continue; - } - /* leading or trailing white space are ignored, white space around '=' - are also ignored. The number should no longer than 10 characters. - Trailing non-whitespace are not allowed. */ - ret = sscanf(p_line, " port = %10[0-9] %n", temp, &number); - /* Make sure number is positive then make the cast. It's not likely to - have a negtive value, just a defense-in-depth. The cast is used to - suppress the -Wsign-compare warning. */ - if (ret == 1 && number > 0 && ((size_t)number < line_len) - && !p_line[number]) { - errno = 0; - long_num = strtol(temp, &p, 10); - if (p == temp) { - TDX_TRACE; - port = 0; - break; - } - - // make sure that no range error occurred - if (errno == ERANGE || long_num > UINT_MAX) { - TDX_TRACE; - port = 0; - break; - } - - // range is ok, so we can convert to int - port = (unsigned int)long_num & 0xFFFFFFFF; -#ifdef DEBUG - fprintf(stdout, "\nGet the vsock port number [%u]\n", port); -#endif - break; - } - } - - /* p_line is allocated by sscanf */ - free(p_line); - fclose(p_config_fd); - - return port; -} - -static tdx_attest_error_t get_tdx_report( - int devfd, - const tdx_report_data_t *p_tdx_report_data, - tdx_report_t *p_tdx_report) -{ - if (-1 == devfd) { - return TDX_ATTEST_ERROR_UNEXPECTED; - } - if (!p_tdx_report) { - fprintf(stderr, "\nNeed to input TDX report."); - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (!p_tdx_report_data) { - fprintf(stderr, "\nNeed to input TDX report data."); - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - struct tdx_report_req req = {0}; - memcpy(req.reportdata, p_tdx_report_data->d, sizeof(req.reportdata)); - - if (-1 == ioctl(devfd, TDX_CMD_GET_REPORT0, &req)) { - TDX_TRACE; - return TDX_ATTEST_ERROR_REPORT_FAILURE; - } - memcpy(p_tdx_report->d, req.tdreport, sizeof(p_tdx_report->d)); - return TDX_ATTEST_SUCCESS; -} - -#define MAX_PATH 260 - -static int b_mkdir = 1; -pthread_mutex_t mkdir_mutex; - -void __attribute__((constructor)) init_mutex(void) { pthread_mutex_init(&mkdir_mutex, NULL); } -void __attribute__((destructor)) destroy_mutex(void) { pthread_mutex_destroy(&mkdir_mutex); } - -static tdx_attest_error_t prepare_configfs(char **p_configfs_path) { - int ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - char *configfs_path = NULL; - do { - // Retrive DCAP TDX quote configFS path from environment - configfs_path = secure_getenv(DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV); - if (configfs_path == NULL) { - syslog(LOG_INFO, "libtdx_attest: env '%s' is not provided - try default path.", - DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV); - break; - } - if (strnlen(configfs_path, MAX_PATH) >= MAX_PATH - 20) { - syslog(LOG_ERR, "libtdx_attest: env '%s' is too long.", DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV); - return ret; - } - - // Check whether the configFS directory exists - DIR *dir = opendir(configfs_path); - if (dir == NULL) { - syslog(LOG_ERR, "libtdx_attest: env '%s' is not valid directory.", - DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV); - return ret; - } - closedir(dir); - ret = TDX_ATTEST_SUCCESS; - } while (0); - - while (ret != TDX_ATTEST_SUCCESS) { - // Default DCAP TDX quote configFS path - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - configfs_path = DEFAULT_DCAP_TDX_QUOTE_CONFIGFS_PATH; - pthread_mutex_lock(&mkdir_mutex); - DIR *dir = opendir(configfs_path); - if (dir != NULL) { - pthread_mutex_unlock(&mkdir_mutex); - ret = TDX_ATTEST_SUCCESS; - closedir(dir); - break; - } - if (errno != ENOENT) { - pthread_mutex_unlock(&mkdir_mutex); - syslog(LOG_INFO, "libtdx_attest: default DCAP configFS not supported - fallback to vsock mode."); - break; - } - - // Create default DCAP TDX quote configFS path only once - if (!b_mkdir) { - pthread_mutex_unlock(&mkdir_mutex); - syslog(LOG_INFO, "libtdx_attest: default DCAP configFS not supported - fallback to vsock mode."); - break; - } - b_mkdir = 0; - - dir = opendir(QUOTE_CONFIGFS_PATH); - if (dir == NULL) { - pthread_mutex_unlock(&mkdir_mutex); - syslog(LOG_INFO, "libtdx_attest: configFS not supported - fallback to vsock mode."); - break; - } - closedir(dir); - - if (mkdir(configfs_path, S_IRWXU | S_IRWXG)) { - pthread_mutex_unlock(&mkdir_mutex); - if (errno == EEXIST && (dir = opendir(configfs_path)) != NULL) { - // Another process has just created configfs_path - ret = TDX_ATTEST_SUCCESS; - closedir(dir); - break; - } - syslog(LOG_INFO, "libtdx_attest: cannot create default configFS - fallback to vsock mode."); - break; - } - char provider_path[MAX_PATH]; - snprintf(provider_path, sizeof(provider_path), "%s/provider", configfs_path); - for (size_t retry = 0; retry < 5; retry++) { - // Linux kernel will create provider, generation, inblob, outblob in configfs_path - // after configfs_path direcotry created. - if (access(provider_path, F_OK) == 0) { - pthread_mutex_unlock(&mkdir_mutex); - ret = TDX_ATTEST_SUCCESS; - break; - } - usleep((useconds_t)retry); - } - pthread_mutex_unlock(&mkdir_mutex); - syslog(LOG_INFO, "libtdx_attest: unavailable default configFS - fallback to vsock mode."); - break; - } - - if (ret != TDX_ATTEST_SUCCESS) { - //Both configfs path are unavailable - return ret; - } - - // For Intel TDX, provider is "tdx_guest" - char provider_path[MAX_PATH]; - snprintf(provider_path, sizeof(provider_path), "%s/provider", configfs_path); - int fd = open(provider_path, O_RDONLY); - if (-1 == fd) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: cannot open configFS `%s`.", provider_path); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - // Read the entire file in one shot - char provider[16] = {0}; - ssize_t byte_size = read(fd, provider, 15); - close(fd); - - if (byte_size == -1 || byte_size == 0 || - strncmp(provider, "tdx_guest", sizeof("tdx_guest") - 1)) { - syslog(LOG_ERR, "libtdx_attest: configFS unsupported provider."); - return TDX_ATTEST_ERROR_NOT_SUPPORTED; - } - *p_configfs_path = configfs_path; - return TDX_ATTEST_SUCCESS; -} - -static tdx_attest_error_t read_configfs_generation(char *generation_path, long* p_generation) -{ - int fd = open(generation_path, O_RDONLY); - if (-1 == fd) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: failed to open configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } -#ifdef DEBUG - fprintf(stdout, "\nstart to read generation\n"); -#endif - #define GENERATION_MAX_LENGTH 20 - char str_generation[GENERATION_MAX_LENGTH] = {0}; - ssize_t byte_size = read(fd, str_generation, GENERATION_MAX_LENGTH); - if (byte_size == -1) { - TDX_TRACE; - close(fd); - syslog(LOG_ERR, "libtdx_attest: failed to read configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - close(fd); - if (byte_size == 0) { - syslog(LOG_ERR, "libtdx_attest: no content of configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - if (byte_size >= GENERATION_MAX_LENGTH) { - syslog(LOG_ERR, "libtdx_attest: too large configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - errno = 0; - long generation = strtol(str_generation, NULL, 10); - if (errno != 0) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: cannot parse configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - *p_generation = generation; - -#ifdef DEBUG - fprintf(stdout, "\ngeneration: %ld\n", generation); -#endif - return TDX_ATTEST_SUCCESS; -} - -#define RETRY_WAIT_TIME_USEC 10000000 - -tdx_attest_error_t tdx_att_get_quote( - const tdx_report_data_t *p_tdx_report_data, - const tdx_uuid_t *p_att_key_id_list, - uint32_t list_size, - tdx_uuid_t *p_att_key_id, - uint8_t **pp_quote, - uint32_t *p_quote_size, - uint32_t flags) -{ - int s = -1; - int devfd = -1; - - const uint8_t *p_quote = NULL; - uint32_t quote_size = 0; - tdx_attest_error_t ret = TDX_ATTEST_ERROR_UNEXPECTED; - uint8_t *p_blob_payload = NULL; - - if ((!p_att_key_id_list && list_size) || - (p_att_key_id_list && !list_size)) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (!pp_quote) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (flags) { - //TODO: I think we need to have a runtime version to make this flag usable. - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - - // Currently only intel TDQE are supported - if (1 < list_size) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (p_att_key_id_list && memcmp(p_att_key_id_list, &g_intel_tdqe_uuid, - sizeof(g_intel_tdqe_uuid))) { - return TDX_ATTEST_ERROR_UNSUPPORTED_ATT_KEY_ID; - } - - *pp_quote = NULL; - - do { - char *configfs_path = NULL; - if (prepare_configfs(&configfs_path) != TDX_ATTEST_SUCCESS) - break; - - char inblob_path[MAX_PATH]; - snprintf(inblob_path, sizeof(inblob_path), "%s/inblob", configfs_path); - - // Lock `inblob` to avoid other processes accessing it using libtdx_attest - // Will unlock it via close() - int fd_lock = open(inblob_path, O_WRONLY | O_CLOEXEC); - if (-1 == fd_lock) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: failed to open configFS inblob."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - if (flock(fd_lock, LOCK_EX)) { - TDX_TRACE; - close(fd_lock); - syslog(LOG_ERR, "libtdx_attest: failed to lock configFS inblob."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - /* Read and check generation value before writing inblob, after writing inblob and after - reading outblob to make sure that outblob matches inblob */ - char generation_path[MAX_PATH]; - snprintf(generation_path, sizeof(generation_path), "%s/generation", configfs_path); - long generation1; - ret = read_configfs_generation(generation_path, &generation1); - if (ret) { - close(fd_lock); - return ret; - } - - // Write TDX report data to inblob - int fd_inblob = open(inblob_path, O_WRONLY); - if (-1 == fd_inblob) { - TDX_TRACE; - close(fd_lock); - syslog(LOG_ERR, "libtdx_attest: failed to open configFS inblob."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - ssize_t byte_size = 0; - // Wait and retry when EBUSY; other TDX Quotes are being generating - for (int retry = 0; retry < 3; retry++) { - errno = 0; - byte_size = write(fd_inblob, p_tdx_report_data, sizeof(*p_tdx_report_data)); - if (errno != EBUSY) - break; - usleep(RETRY_WAIT_TIME_USEC); - } - if (byte_size != sizeof(*p_tdx_report_data)) { - if (errno == EBUSY) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_BUSY; - } else { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_UNEXPECTED; - } - close(fd_lock); - close(fd_inblob); - syslog(LOG_ERR, "libtdx_attest: failed to write configFS inblob."); - return ret; - } - close(fd_inblob); - - long generation2; - do { - ret = read_configfs_generation(generation_path, &generation2); - if (ret) { - close(fd_lock); - return ret; - } - // In rare cases, generation is not updated - } while (generation2 == generation1 && !usleep(0)); - if (generation2 != generation1 + 1) { - // Another TDX quote generation has been triggered - close(fd_lock); - return TDX_ATTEST_ERROR_BUSY; - } - - // Read TDX quote from outblob - char outblob_path[MAX_PATH]; - snprintf(outblob_path, sizeof(outblob_path), "%s/outblob", configfs_path); - int fd = open(outblob_path, O_RDONLY); - if (-1 == fd) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: failed to open configFS outblob."); - close(fd_lock); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - // Allocate memory for the entire file content - p_blob_payload = malloc(QUOTE_BUF_SIZE); - if (p_blob_payload == NULL) { - close(fd_lock); - close(fd); - return TDX_ATTEST_ERROR_OUT_OF_MEMORY; - } -#ifdef DEBUG - fprintf(stdout, "\nstart to read outblob\n"); -#endif - // Read the entire file in one shot - for (int retry = 0; retry < 3; retry++) { - errno = 0; - byte_size = read(fd, p_blob_payload, QUOTE_BUF_SIZE); - if (errno == EBUSY) { - usleep(RETRY_WAIT_TIME_USEC); - } else if (errno != EINTR && errno != ETIMEDOUT) - break; - } - if (byte_size == -1 || byte_size == 0) { - if (errno == EBUSY || errno == EINTR || errno == ETIMEDOUT) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_BUSY; - } else - ret = TDX_ATTEST_ERROR_QUOTE_FAILURE; - close(fd_lock); - close(fd); - free(p_blob_payload); - syslog(LOG_ERR, "libtdx_attest: failed to read outblob."); - return ret; - } - close(fd); - - quote_size = (uint32_t)byte_size; -#ifdef DEBUG - fprintf(stdout, "\nquote size: %d\n", quote_size); -#endif - if (quote_size <= QUOTE_MIN_SIZE || quote_size == QUOTE_BUF_SIZE) { - close(fd_lock); - free(p_blob_payload); - return TDX_ATTEST_ERROR_QUOTE_FAILURE; - } - - long generation3; - ret = read_configfs_generation(generation_path, &generation3); - close(fd_lock); - if (ret) { - free(p_blob_payload); - return ret; - } - // Another TDX quote generation is triggered - if (generation3 != generation2) { - free(p_blob_payload); - return TDX_ATTEST_ERROR_BUSY; - } - - void* tmp_p = realloc(p_blob_payload, quote_size); - if (tmp_p == NULL) { - free(p_blob_payload); - return TDX_ATTEST_ERROR_OUT_OF_MEMORY; - } - *pp_quote = tmp_p; - - if (p_quote_size) { - *p_quote_size = quote_size; - } - if (p_att_key_id) { - *p_att_key_id = g_intel_tdqe_uuid; - } - return TDX_ATTEST_SUCCESS; - } while (0); - -#ifdef DEBUG - fprintf(stdout, "\ngoto legacy logic\n"); -#endif - - uint32_t recieved_bytes = 0; - uint32_t in_msg_size = 0; - unsigned int vsock_port = 0; - uint32_t msg_size = 0; - qgs_msg_error_t qgs_msg_ret = QGS_MSG_SUCCESS; - qgs_msg_header_t *p_header = NULL; - uint8_t *p_req = NULL; - const uint8_t *p_selected_id = NULL; - uint32_t id_size = 0; - - tdx_report_t tdx_report; - memset(&tdx_report, 0, sizeof(tdx_report)); - - struct tdx_quote_hdr *p_get_quote_blob = malloc(REQ_BUF_SIZE); - if (!p_get_quote_blob) { - ret = TDX_ATTEST_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - devfd = open(TDX_ATTEST_DEV_PATH, O_RDWR | O_SYNC); - if (-1 == devfd) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_DEVICE_FAILURE; - goto ret_point; - } - - ret = get_tdx_report(devfd, p_tdx_report_data, &tdx_report); - if (TDX_ATTEST_SUCCESS != ret) { - goto ret_point; - } - - qgs_msg_ret = qgs_msg_gen_get_quote_req(tdx_report.d, sizeof(tdx_report.d), - NULL, 0, &p_req, &msg_size); - if (QGS_MSG_SUCCESS != qgs_msg_ret) { -#ifdef DEBUG - fprintf(stdout, "\nqgs_msg_gen_get_quote_req return 0x%x\n", qgs_msg_ret); -#endif - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - if (msg_size > REQ_BUF_SIZE - sizeof(struct tdx_quote_hdr) - HEADER_SIZE) { -#ifdef DEBUG - fprintf(stdout, "\nqmsg_size[%d] is too big\n", msg_size); - #endif - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - goto ret_point; - } - - p_blob_payload = (uint8_t *)&p_get_quote_blob->data; - p_blob_payload[0] = (uint8_t)((msg_size >> 24) & 0xFF); - p_blob_payload[1] = (uint8_t)((msg_size >> 16) & 0xFF); - p_blob_payload[2] = (uint8_t)((msg_size >> 8) & 0xFF); - p_blob_payload[3] = (uint8_t)(msg_size & 0xFF); - - memcpy(p_blob_payload + HEADER_SIZE, p_req, msg_size); - - do { - vsock_port = get_vsock_port(); - if (!vsock_port) { - syslog(LOG_INFO, "libtdx_attest: cannot parse sock port - fallback to tdvmcall mode."); - break; - } - s = socket(AF_VSOCK, SOCK_STREAM, 0); - if (-1 == s) { - syslog(LOG_INFO, "libtdx_attest: cannot create socket - fallback to tdvmcall mode."); - break; - } - struct sockaddr_vm vm_addr; - memset(&vm_addr, 0, sizeof(vm_addr)); - vm_addr.svm_family = AF_VSOCK; - vm_addr.svm_reserved1 = 0; - vm_addr.svm_port = vsock_port; - vm_addr.svm_cid = VMADDR_CID_HOST; - if (connect(s, (struct sockaddr *)&vm_addr, sizeof(vm_addr))) { - syslog(LOG_INFO, "libtdx_attest: cannot connect - fallback to tdvmcall mode."); - break; - } - - // Write to socket - if (HEADER_SIZE + msg_size != send(s, p_blob_payload, - HEADER_SIZE + msg_size, 0)) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_VSOCK_FAILURE; - goto ret_point; - } - - // Read the response size header - if (HEADER_SIZE != recv(s, p_blob_payload, - HEADER_SIZE, 0)) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_VSOCK_FAILURE; - goto ret_point; - } - - // decode the size - for (unsigned i = 0; i < HEADER_SIZE; ++i) { - in_msg_size = in_msg_size * 256 + ((p_blob_payload[i]) & 0xFF); - } - - // prepare the buffer and read the reply body - #ifdef DEBUG - fprintf(stdout, "\nReply message body is %u bytes", in_msg_size); - #endif - - if (REQ_BUF_SIZE - sizeof(struct tdx_quote_hdr) - HEADER_SIZE < in_msg_size) { - #ifdef DEBUG - fprintf(stdout, "\nReply message body is too big"); - #endif - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - while( recieved_bytes < in_msg_size) { - int recv_ret = (int)recv(s, p_blob_payload + HEADER_SIZE + recieved_bytes, - in_msg_size - recieved_bytes, 0); - if (recv_ret < 0) { - ret = TDX_ATTEST_ERROR_VSOCK_FAILURE; - goto ret_point; - } - recieved_bytes += (uint32_t)recv_ret; - } - #ifdef DEBUG - fprintf(stdout, "\nGet %u bytes response from vsock", recieved_bytes); - #endif - - goto done; - } while (0); - - int ioctl_ret; - struct tdx_quote_req arg; - p_get_quote_blob->version = 1; - p_get_quote_blob->status = 0; - p_get_quote_blob->in_len = HEADER_SIZE + msg_size; - p_get_quote_blob->out_len = 0; - arg.buf = (__u64)p_get_quote_blob; - arg.len = REQ_BUF_SIZE; - - ioctl_ret = ioctl(devfd, TDX_CMD_GET_QUOTE, &arg); - if (EBUSY == ioctl_ret) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_BUSY; - goto ret_point; - } else if (ioctl_ret) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_QUOTE_FAILURE; - goto ret_point; - } - if (p_get_quote_blob->status - || p_get_quote_blob->out_len <= HEADER_SIZE) { - TDX_TRACE; - if (GET_QUOTE_IN_FLIGHT == p_get_quote_blob->status) { - ret = TDX_ATTEST_ERROR_BUSY; - } else if (GET_QUOTE_SERVICE_UNAVAILABLE == p_get_quote_blob->status) { - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - } else { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - } - goto ret_point; - } - - //in_msg_size is the size of serialized response - for (unsigned i = 0; i < HEADER_SIZE; ++i) { - in_msg_size = in_msg_size * 256 + ((p_blob_payload[i]) & 0xFF); - } - if (in_msg_size != p_get_quote_blob->out_len - HEADER_SIZE) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - #ifdef DEBUG - fprintf(stdout, "\nGet %u bytes response from tdvmcall", in_msg_size); - #endif - -done: - qgs_msg_ret = qgs_msg_inflate_get_quote_resp( - p_blob_payload + HEADER_SIZE, in_msg_size, - &p_selected_id, &id_size, - &p_quote, "e_size); - if (QGS_MSG_SUCCESS != qgs_msg_ret) { - #ifdef DEBUG - fprintf(stdout, "\nqgs_msg_inflate_get_quote_resp return 0x%x", qgs_msg_ret); - #endif - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - // We've called qgs_msg_inflate_get_quote_resp, the message type should be GET_QUOTE_RESP - p_header = (qgs_msg_header_t *)(p_blob_payload + HEADER_SIZE); - if (p_header->error_code != 0) { - #ifdef DEBUG - fprintf(stdout, "\nerror code in resp msg is 0x%x", p_header->error_code); - #endif - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - *pp_quote = malloc(quote_size); - if (!*pp_quote) { - ret = TDX_ATTEST_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - memcpy(*pp_quote, p_quote, quote_size); - if (p_quote_size) { - *p_quote_size = quote_size; - } - if (p_att_key_id) { - *p_att_key_id = g_intel_tdqe_uuid; - } - ret = TDX_ATTEST_SUCCESS; - -ret_point: - if (s >= 0) { - close(s); - } - if (-1 != devfd) { - close(devfd); - } - qgs_msg_free(p_req); - free(p_get_quote_blob); - - return ret; -} - -tdx_attest_error_t tdx_att_free_quote( - uint8_t *p_quote) -{ - free(p_quote); - return TDX_ATTEST_SUCCESS; -} - -tdx_attest_error_t tdx_att_get_report( - const tdx_report_data_t *p_tdx_report_data, - tdx_report_t *p_tdx_report) -{ - int devfd; - tdx_attest_error_t ret = TDX_ATTEST_SUCCESS; - - devfd = open(TDX_ATTEST_DEV_PATH, O_RDWR | O_SYNC); - if (-1 == devfd) { - TDX_TRACE; - return TDX_ATTEST_ERROR_DEVICE_FAILURE; - } - - ret = get_tdx_report(devfd, p_tdx_report_data, p_tdx_report); - - close(devfd); - return ret; -} - -tdx_attest_error_t tdx_att_get_supported_att_key_ids( - tdx_uuid_t *p_att_key_id_list, - uint32_t *p_list_size) -{ - if (!p_list_size) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (p_att_key_id_list && !*p_list_size) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (!p_att_key_id_list && *p_list_size) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (p_att_key_id_list) { - p_att_key_id_list[0] = g_intel_tdqe_uuid; - } - *p_list_size = 1; - return TDX_ATTEST_SUCCESS; -} - -tdx_attest_error_t tdx_att_extend( - const tdx_rtmr_event_t *p_rtmr_event) -{ -#ifdef TDX_CMD_EXTEND_RTMR - int devfd = -1; - struct tdx_extend_rtmr_req req; - if (!p_rtmr_event || p_rtmr_event->version != 1) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (p_rtmr_event->event_data_size) { - return TDX_ATTEST_ERROR_NOT_SUPPORTED; - } - if (p_rtmr_event->rtmr_index > 3) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - - devfd = open(TDX_ATTEST_DEV_PATH, O_RDWR | O_SYNC); - if (-1 == devfd) { - TDX_TRACE; - return TDX_ATTEST_ERROR_DEVICE_FAILURE; - } - - static_assert(TDX_EXTEND_RTMR_DATA_LEN == sizeof(p_rtmr_event->extend_data), - "rtmr extend size mismatch!"); - req.index = (uint8_t)p_rtmr_event->rtmr_index; - memcpy(req.data, p_rtmr_event->extend_data, TDX_EXTEND_RTMR_DATA_LEN); - if (-1 == ioctl(devfd, TDX_CMD_EXTEND_RTMR, &req)) { - TDX_TRACE; - close(devfd); - if (EINVAL == errno) { - return TDX_ATTEST_ERROR_INVALID_RTMR_INDEX; - } - return TDX_ATTEST_ERROR_EXTEND_FAILURE; - } - close(devfd); - return TDX_ATTEST_SUCCESS; -#else - (void)p_rtmr_event; - return TDX_ATTEST_ERROR_NOT_SUPPORTED; -#endif -} - -#else - -#include "tdx_attest.h" -#include "servtd_com.h" -#include "servtd_external.h" -#include "qgs_msg_lib.h" - -#include -#include -#include -#include - -__attribute__ ((visibility("default"))) tdx_attest_error_t tdx_att_get_quote_by_report ( - const void *p_tdx_report, - uint32_t tdx_report_size, - void *p_quote, - uint32_t *p_quote_size) -{ - uint32_t quote_size = 0; - uint32_t in_msg_size = 0; - tdx_attest_error_t ret = TDX_ATTEST_ERROR_UNEXPECTED; - struct servtd_tdx_quote_hdr *p_get_quote_blob = NULL; - uint8_t *p_blob_payload = NULL; - uint32_t msg_size = 0; - int servtd_get_quote_ret = 0; - const uint8_t *tmp_p_quote = NULL; - const uint8_t *p_selected_id = NULL; - uint32_t id_size = 0; - qgs_msg_error_t qgs_msg_ret = QGS_MSG_SUCCESS; - qgs_msg_header_t *p_header = NULL; - uint8_t *p_req = NULL; - - if (NULL == p_tdx_report || TDX_REPORT_SIZE != tdx_report_size) { - ret = TDX_ATTEST_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (NULL == p_quote || NULL == p_quote_size || 0 == *p_quote_size) { - ret = TDX_ATTEST_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_get_quote_blob = (struct servtd_tdx_quote_hdr *)malloc(SERVTD_REQ_BUF_SIZE); - if (!p_get_quote_blob) { - ret = TDX_ATTEST_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - qgs_msg_ret = qgs_msg_gen_get_quote_req(p_tdx_report, tdx_report_size, - NULL, 0, &p_req, &msg_size); - if (QGS_MSG_SUCCESS != qgs_msg_ret) { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - if (msg_size > SERVTD_REQ_BUF_SIZE - sizeof(struct servtd_tdx_quote_hdr) - SERVTD_HEADER_SIZE) { - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - goto ret_point; - } - - p_blob_payload = (uint8_t *)&p_get_quote_blob->data; - p_blob_payload[0] = (uint8_t)((msg_size >> 24) & 0xFF); - p_blob_payload[1] = (uint8_t)((msg_size >> 16) & 0xFF); - p_blob_payload[2] = (uint8_t)((msg_size >> 8) & 0xFF); - p_blob_payload[3] = (uint8_t)(msg_size & 0xFF); - - // Serialization - memcpy(p_blob_payload + SERVTD_HEADER_SIZE, p_req, msg_size); - - p_get_quote_blob->version = 1; - p_get_quote_blob->status = 0; - p_get_quote_blob->in_len = SERVTD_HEADER_SIZE + msg_size; - p_get_quote_blob->out_len = 0; - - servtd_get_quote_ret = servtd_get_quote(p_get_quote_blob, SERVTD_REQ_BUF_SIZE); - if (servtd_get_quote_ret) { - ret = TDX_ATTEST_ERROR_QUOTE_FAILURE; - goto ret_point; - } - - if (p_get_quote_blob->status - || p_get_quote_blob->out_len <= SERVTD_HEADER_SIZE) { - if (GET_QUOTE_IN_FLIGHT == p_get_quote_blob->status) { - ret = TDX_ATTEST_ERROR_BUSY; - } else if (GET_QUOTE_SERVICE_UNAVAILABLE == p_get_quote_blob->status) { - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - } else { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - } - goto ret_point; - } - - //in_msg_size is the size of serialized response, remove 4bytes header - for (unsigned i = 0; i < SERVTD_HEADER_SIZE; ++i) { - in_msg_size = in_msg_size * 256 + ((p_blob_payload[i]) & 0xFF); - } - if (in_msg_size != p_get_quote_blob->out_len - SERVTD_HEADER_SIZE) { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - qgs_msg_ret = qgs_msg_inflate_get_quote_resp( - p_blob_payload + SERVTD_HEADER_SIZE, in_msg_size, - &p_selected_id, &id_size, - (const uint8_t **)&tmp_p_quote, "e_size); - if (QGS_MSG_SUCCESS != qgs_msg_ret) { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - // We've called qgs_msg_inflate_get_quote_resp, the message type should be GET_QUOTE_RESP - p_header = (qgs_msg_header_t *)(p_blob_payload + SERVTD_HEADER_SIZE); - if (p_header->error_code != 0) { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - if (quote_size > *p_quote_size) { - ret = TDX_ATTEST_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - memcpy(p_quote, tmp_p_quote, quote_size); - - *p_quote_size = quote_size; - ret = TDX_ATTEST_SUCCESS; - -ret_point: - qgs_msg_free(p_req); - SAFE_FREE(p_get_quote_blob); - return ret; -} - -#endif diff --git a/tdx-attest-sys/csrc/tdx_attest.h b/tdx-attest-sys/csrc/tdx_attest.h deleted file mode 100644 index 258617bf..00000000 --- a/tdx-attest-sys/csrc/tdx_attest.h +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (C) 2011-2021 Intel Corporation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Intel Corporation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - - -/** - * File: tdx_attest.h - * - * Description: API definitions for TDX Attestation library - * - */ -#ifndef _TDX_ATTEST_H_ -#define _TDX_ATTEST_H_ -#include - -typedef enum _tdx_attest_error_t { - TDX_ATTEST_SUCCESS = 0x0000, ///< Success - TDX_ATTEST_ERROR_MIN = 0x0001, ///< Indicate min error to allow better translation. - TDX_ATTEST_ERROR_UNEXPECTED = 0x0001, ///< Unexpected error - TDX_ATTEST_ERROR_INVALID_PARAMETER = 0x0002, ///< The parameter is incorrect - TDX_ATTEST_ERROR_OUT_OF_MEMORY = 0x0003, ///< Not enough memory is available to complete this operation - TDX_ATTEST_ERROR_VSOCK_FAILURE = 0x0004, ///< vsock related failure - TDX_ATTEST_ERROR_REPORT_FAILURE = 0x0005, ///< Failed to get the TD Report - TDX_ATTEST_ERROR_EXTEND_FAILURE = 0x0006, ///< Failed to extend rtmr - TDX_ATTEST_ERROR_NOT_SUPPORTED = 0x0007, ///< Request feature is not supported - TDX_ATTEST_ERROR_QUOTE_FAILURE = 0x0008, ///< Failed to get the TD Quote - TDX_ATTEST_ERROR_BUSY = 0x0009, ///< The device driver return busy - TDX_ATTEST_ERROR_DEVICE_FAILURE = 0x000a, ///< Failed to acess tdx attest device - TDX_ATTEST_ERROR_INVALID_RTMR_INDEX = 0x000b, ///< Only supported RTMR index is 2 and 3 - TDX_ATTEST_ERROR_UNSUPPORTED_ATT_KEY_ID = 0x000c, ///< The platform Quoting infrastructure does not support any of the keys described in att_key_id_list - TDX_ATTEST_ERROR_MAX -} tdx_attest_error_t; - -#define TDX_UUID_SIZE 16 - -#pragma pack(push, 1) - -#define TDX_UUID_SIZE 16 -typedef struct _tdx_uuid_t -{ - uint8_t d[TDX_UUID_SIZE]; -} tdx_uuid_t; - -#define TDX_SGX_ECDSA_ATTESTATION_ID \ -{ \ - 0xe8, 0x6c, 0x04, 0x6e, 0x8c, 0xc4, 0x4d, 0x95, \ - 0x81, 0x73, 0xfc, 0x43, 0xc1, 0xfa, 0x4f, 0x3f \ -} - -#define TDX_REPORT_DATA_SIZE 64 -typedef struct _tdx_report_data_t -{ - uint8_t d[TDX_REPORT_DATA_SIZE]; -} tdx_report_data_t; - -#define TDX_REPORT_SIZE 1024 -typedef struct _tdx_report_t -{ - uint8_t d[TDX_REPORT_SIZE]; -} tdx_report_t; - -typedef struct _tdx_rtmr_event_t { - uint32_t version; - uint64_t rtmr_index; - uint8_t extend_data[48]; - uint32_t event_type; - uint32_t event_data_size; - uint8_t event_data[]; -} tdx_rtmr_event_t; - -#pragma pack(pop) - -#if defined(__cplusplus) -extern "C" { -#endif - -#ifndef SERVTD_ATTEST -/** - * @brief Request a Quote of the calling TD. - * - * The caller provides data intended to be cryptographically bound to the - * resulting Quote. (This data should not require confidentiality protection.) - * The caller also provides information about the type of Quote signing that - * should be used. - * - * In general, a given platform can create Quotes using - * different cryptographic algorithms or using different vendors’ code/enclaves. - * The att_key_id_list parameter is related to this. It is a list of key IDs - * supported by the eventual verifier of the Quote. How the caller of this - * function obtains this list is outside the scope of the R3AAL. - * - * A default key ID is supported and will be used when att_key_id_list == NULL. - * In this case, the default key ID is returned via the p_att_key_id parameter. - * - * When the function returns successfully, p_quote will point to a buffer - * containing the Quote. This buffer is allocated by the function. Use - * tdx_att_free_quote to free this buffer. - * - * @param p_tdx_report_data [in] Pointer to data that the caller/TD wants to - * cryptographically bind to the Quote, - * typically a hash. Cannot be NULL. - * @param att_key_id_list [in] List (array) of the attestation key IDs supported - * by the Quote verifier. The function compares the - * key IDs in att_key_id_list to the key IDs that - * the platform supports and uses the first match. - * May be NULL. If NULL, the API will use the - * platform’s default key ID. The uuid_t - * corresponding to the key ID that’s used is - * pointed to by p_att_key_id when the function - * returns unless p_att_key_id == NULL. - * @param list_size [in] Size of att_key_id_list in entries. - * @param p_att_key_id [out] The selected attestation key ID when the function - * returns. May be NULL indicating the platform’s - * default key ID - * @param pp_quote [out] Pointer to a pointer that the function will set equal - * to the address of the buffer containing the Quote. The - * function also allocates this buffer. Use - * tdx_att_free_quote to free this buffe - * @param p_quote_size [out] This function will place the size of the Quote, in - * bytes, in the uint32_t pointed to by the - * p_quote_size parameter. May be NULL. - * @param flags [in] Reserved, must be zero. - * @return TDX_ATTEST_SUCCESS: Successfully generated the Quote. - * @return TDX_ATTEST_ERROR_UNEXPECTED: An unexpected internal error occurred. - * @return TDX_ATTEST_ERROR_INVALID_PARAMETER: The parameter is incorrect - * @return TDX_ATTEST_ERROR_DEVICE_FAILURE: Failed to acess tdx attest device. - * @return TDX_ATTEST_ERROR_REPORT_FAILURE: Failed to get TD report. - * @return TDX_ATTEST_ERROR_VSOCK_FAILURE: Failed read/write in vsock mode - * @return TDX_ATTEST_ERROR_QUOTE_FAILURE: Failed to get quote from QGS - * @return TDX_ATTEST_UNSUPPORTED_ATT_KEY_ID: The platform Quoting - * infrastructure does not support any of the keys described in - * att_key_id_list. - * @return TDX_ATTEST_OUT_OF_MEMORY: Heap memory allocation error in library or - * enclave. - */ -tdx_attest_error_t tdx_att_get_quote( - const tdx_report_data_t *p_tdx_report_data, - const tdx_uuid_t att_key_id_list[], - uint32_t list_size, - tdx_uuid_t *p_att_key_id, - uint8_t **pp_quote, - uint32_t *p_quote_size, - uint32_t flags); - - -/** - * @brief Free the Quote buffer allocated by tdx_att_get_quote. - * - * @param p_quote [in] The value of *p_quote returned by tdx_att_get_quote. - * @return TDX_ATTEST_SUCCESS: Successfully freed the p_quote. - */ -tdx_attest_error_t tdx_att_free_quote( - uint8_t *p_quote); - -/** - * @brief Request a TDX Report of the calling TD. - * - * The caller provides data intended to be cryptographically bound to the - * resulting Report. - * - * @param p_tdx_report_data [in] Pointer to data that the caller/TD wants to - * cryptographically bind to the Quote, typically - * a hash. Cannot be NULL. - * @param p_tdx_report [out] Pointer to the buffer that will contain the - * generated TDX Report. Must not be NULL. - * @return TDX_ATTEST_SUCCESS: Successfully generated the Report. - * @return TDX_ATTEST_ERROR_INVALID_PARAMETER: p_tdx_report == NULL - * @return TDX_ATTEST_ERROR_DEVICE_FAILURE: Failed to acess tdx attest device. - * @return TDX_ATTEST_ERROR_REPORT_FAILURE: Failed to get TD report. - */ -tdx_attest_error_t tdx_att_get_report( - const tdx_report_data_t *p_tdx_report_data, - tdx_report_t* p_tdx_report); - - -/** - * @brief Extend one of the TDX runtime measurement registers (RTMRs). - * - * RTMR[rtmr_index] = SHA384(RTMR[rtmr_index] || extend_data) - * rtmr_index and extend_data are fields in the structure that is an input of - * this API. - * This API does not return either the new or old value of the specified RTMR. - * The tdx_att_get_report API may be used for this. - * The input to this API includes a description of the “extend data”. This is - * intended to facilitate reconstruction of the RTMR value. This, in turn, - * suggests maintenance of an event log by the callee. Currently, event_data is - * not supported. - * - * @param p_rtmr_event [in] Pointer to structure that contains the index of the - * RTMR to extend, the data with which to extend it and - * a description of the data. - * @return TDX_ATTEST_SUCCESS: Successfully extended the RTMR. - * @return TDX_ATTEST_ERROR_INVALID_PARAMETER: p_rtmr_event == NULL - * @return TDX_ATTEST_ERROR_UNEXPECTED: An unexpected internal error occurred. - * @return TDX_ATTEST_ERROR_DEVICE_FAILURE: Failed to acess tdx attest device. - * @return TDX_ATTEST_ERROR_EXTEND_FAILURE: Failed to extend data. - * @return TDX_ATTEST_ERROR_NOT_SUPPORTED: p_rtmr_event->event_data_size != 0 - */ -tdx_attest_error_t tdx_att_extend( - const tdx_rtmr_event_t *p_rtmr_event); - - -/** - * @brief Retrieve the list of attestation key IDs supported by the platform. - * - * Specify p_att_key_id_list = NULL to learn the number of entries in the list. - * - * @param p_att_key_id_list [out] List of the attestation key IDs that the - * platform supports. May be NULL. If NULL, the - * API will return the number of entries in the - * list in the uint32_t pointed to by p_list_size - * @param p_list_size [in/out] As input, pointer to a uint32_t specifying the - * size of p_att_key_id_list in entries. As output, - * this function will place the required size, in - * entries, in the uint32_t pointed to by the - * p_list_size parameter. If this value changes, the - * new value will be the required size - * @return TDX_ATTEST_SUCCESS: att_key_id_list populated and p_list_size points - * to a uint32_t that indicates the number of - * entries. - * @return TDX_ATTEST_ERROR_INVALID_PARAMETER: The parameter is incorrect - */ -tdx_attest_error_t tdx_att_get_supported_att_key_ids( - tdx_uuid_t *p_att_key_id_list, - uint32_t *p_list_size); - -#else -__attribute__ ((visibility("default"))) tdx_attest_error_t tdx_att_get_quote_by_report ( - const void *p_tdx_report, - uint32_t tdx_report_size, - void *p_quote, - uint32_t *p_quote_size); -#endif - -#if defined(__cplusplus) -} -#endif - - -#endif - diff --git a/tdx-attest-sys/src/lib.rs b/tdx-attest-sys/src/lib.rs deleted file mode 100644 index abe54931..00000000 --- a/tdx-attest-sys/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(clippy::missing_safety_doc)] - -// SPDX-FileCopyrightText: © 2024 Phala Network -// -// SPDX-License-Identifier: Apache-2.0 - -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/tdx-attest/Cargo.toml b/tdx-attest/Cargo.toml index 1a193024..3470519d 100644 --- a/tdx-attest/Cargo.toml +++ b/tdx-attest/Cargo.toml @@ -13,7 +13,6 @@ license.workspace = true [dependencies] anyhow.workspace = true hex.workspace = true -num_enum.workspace = true scale.workspace = true serde.workspace = true serde-human-bytes.workspace = true @@ -22,9 +21,7 @@ thiserror.workspace = true fs-err.workspace = true serde_json = { workspace = true, features = ["alloc"] } sha2.workspace = true - -[target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] -tdx-attest-sys.workspace = true +log.workspace = true [dev-dependencies] insta.workspace = true diff --git a/tdx-attest/src/dummy.rs b/tdx-attest/src/dummy.rs index 12ee199b..0c595533 100644 --- a/tdx-attest/src/dummy.rs +++ b/tdx-attest/src/dummy.rs @@ -3,61 +3,26 @@ // SPDX-License-Identifier: Apache-2.0 use cc_eventlog::TdxEventLog; -use num_enum::FromPrimitive; use thiserror::Error; -use crate::{TdxReport, TdxReportData, TdxUuid}; +use crate::TdxReportData; type Result = std::result::Result; -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Error)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] pub enum TdxAttestError { - #[error("unexpected")] - Unexpected, - #[error("invalid parameter")] - InvalidParameter, - #[error("out of memory")] - OutOfMemory, - #[error("vsock failure")] - VsockFailure, - #[error("report failure")] - ReportFailure, - #[error("extend failure")] - ExtendFailure, #[error("not supported")] NotSupported, - #[error("quote failure")] - QuoteFailure, - #[error("busy")] - Busy, - #[error("device failure")] - DeviceFailure, - #[error("invalid rtmr index")] - InvalidRtmrIndex, - #[error("unsupported att key id")] - UnsupportedAttKeyId, - #[num_enum(catch_all)] - #[error("unknown error ({0})")] - UnknownError(u32), } pub fn extend_rtmr(_index: u32, _event_type: u32, _digest: [u8; 48]) -> Result<()> { Err(TdxAttestError::NotSupported) } + pub fn log_rtmr_event(_log: &TdxEventLog) -> Result<()> { Err(TdxAttestError::NotSupported) } -pub fn get_report(_report_data: &TdxReportData) -> Result { - Err(TdxAttestError::NotSupported) -} -pub fn get_quote( - _report_data: &TdxReportData, - _att_key_id_list: Option<&[TdxUuid]>, -) -> Result<(TdxUuid, Vec)> { - let _ = _report_data; - Err(TdxAttestError::NotSupported) -} -pub fn get_supported_att_key_ids() -> Result> { + +pub fn get_quote(_report_data: &TdxReportData) -> Result> { Err(TdxAttestError::NotSupported) } diff --git a/tdx-attest/src/lib.rs b/tdx-attest/src/lib.rs index 3cfa4fdc..56808b2a 100644 --- a/tdx-attest/src/lib.rs +++ b/tdx-attest/src/lib.rs @@ -17,21 +17,7 @@ pub use cc_eventlog as eventlog; pub type Result = std::result::Result; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TdxUuid(pub [u8; 16]); - pub type TdxReportData = [u8; 64]; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TdxReport(pub [u8; 1024]); - -pub fn extend_rtmr3(event: &str, payload: &[u8]) -> anyhow::Result<()> { - use anyhow::Context; - // This code is not defined in the TCG specification. - // See https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf - let event_type = 0x08000001; - let index = 3; - let log = eventlog::TdxEventLog::new(index, event_type, event.to_string(), payload.to_vec()); - extend_rtmr(index, event_type, log.digest).context("Failed to extend RTMR")?; - log_rtmr_event(&log).context("Failed to log RTMR event") -} diff --git a/tdx-attest/src/linux.rs b/tdx-attest/src/linux.rs index 49ebbbfe..b8d87393 100644 --- a/tdx-attest/src/linux.rs +++ b/tdx-attest/src/linux.rs @@ -2,170 +2,80 @@ // // SPDX-License-Identifier: Apache-2.0 -use anyhow::Context; -use cc_eventlog::TdxEventLog; - -use tdx_attest_sys as sys; - -use std::io::Write; -use std::ptr; -use std::slice; - -use sys::*; +use anyhow::bail; +use std::path::PathBuf; use fs_err as fs; -use num_enum::FromPrimitive; use thiserror::Error; -use crate::TdxReport; +use crate::Result; use crate::TdxReportData; -use crate::{Result, TdxUuid}; - -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Error)] -pub enum TdxAttestError { - #[error("TDX_ATTEST_ERROR_UNEXPECTED")] - Unexpected = _tdx_attest_error_t::TDX_ATTEST_ERROR_UNEXPECTED, - #[error("TDX_ATTEST_ERROR_INVALID_PARAMETER")] - InvalidParameter = _tdx_attest_error_t::TDX_ATTEST_ERROR_INVALID_PARAMETER, - #[error("TDX_ATTEST_ERROR_OUT_OF_MEMORY")] - OutOfMemory = _tdx_attest_error_t::TDX_ATTEST_ERROR_OUT_OF_MEMORY, - #[error("TDX_ATTEST_ERROR_VSOCK_FAILURE")] - VsockFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_VSOCK_FAILURE, - #[error("TDX_ATTEST_ERROR_REPORT_FAILURE")] - ReportFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_REPORT_FAILURE, - #[error("TDX_ATTEST_ERROR_EXTEND_FAILURE")] - ExtendFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_EXTEND_FAILURE, - #[error("TDX_ATTEST_ERROR_NOT_SUPPORTED")] - NotSupported = _tdx_attest_error_t::TDX_ATTEST_ERROR_NOT_SUPPORTED, - #[error("TDX_ATTEST_ERROR_QUOTE_FAILURE")] - QuoteFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_QUOTE_FAILURE, - #[error("TDX_ATTEST_ERROR_BUSY")] - Busy = _tdx_attest_error_t::TDX_ATTEST_ERROR_BUSY, - #[error("TDX_ATTEST_ERROR_DEVICE_FAILURE")] - DeviceFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_DEVICE_FAILURE, - #[error("TDX_ATTEST_ERROR_INVALID_RTMR_INDEX")] - InvalidRtmrIndex = _tdx_attest_error_t::TDX_ATTEST_ERROR_INVALID_RTMR_INDEX, - #[error("TDX_ATTEST_ERROR_UNSUPPORTED_ATT_KEY_ID")] - UnsupportedAttKeyId = _tdx_attest_error_t::TDX_ATTEST_ERROR_UNSUPPORTED_ATT_KEY_ID, - #[num_enum(catch_all)] - #[error("unknown tdx attest error ({0})")] - UnknownError(u32), -} - -pub fn get_quote( - report_data: &TdxReportData, - att_key_id_list: Option<&[TdxUuid]>, -) -> Result<(TdxUuid, Vec)> { - let mut att_key_id = TdxUuid([0; TDX_UUID_SIZE as usize]); - let mut quote_ptr = ptr::null_mut(); - let mut quote_size = 0; - - let error = unsafe { - let key_id_list_ptr = att_key_id_list - .map(|list| list.as_ptr() as *const tdx_uuid_t) - .unwrap_or(ptr::null()); - tdx_att_get_quote( - report_data as *const TdxReportData as *const tdx_report_data_t, - key_id_list_ptr, - att_key_id_list.map_or(0, |list| list.len() as u32), - &mut att_key_id as *mut TdxUuid as *mut tdx_uuid_t, - &mut quote_ptr, - &mut quote_size, - 0, - ) - }; - - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); - } - let quote = unsafe { slice::from_raw_parts(quote_ptr, quote_size as usize).to_vec() }; +mod configfs; - unsafe { - tdx_att_free_quote(quote_ptr); - } +/// TSM measurements sysfs paths for RTMR extend (kernel 6.17+) +const TSM_MEASUREMENTS_PATH: &str = "/sys/class/misc/tdx_guest/measurements"; - Ok((att_key_id, quote)) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum TdxAttestError { + #[error("unexpected error")] + Unexpected, + #[error("invalid parameter")] + InvalidParameter, + #[error("out of memory")] + OutOfMemory, + #[error("not supported")] + NotSupported, + #[error("quote generation failed")] + QuoteFailure, + #[error("busy")] + Busy, + #[error("device failure")] + DeviceFailure, } -pub fn get_report(report_data: &TdxReportData) -> Result { - let mut report = TdxReport([0; TDX_REPORT_SIZE as usize]); - - let error = unsafe { - tdx_att_get_report( - report_data as *const TdxReportData as *const tdx_report_data_t, - &mut report as *mut TdxReport as *mut tdx_report_t, - ) - }; - - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); - } - - Ok(report) +/// Get TDX quote using configfs interface +/// +/// This uses the kernel's TSM (Trusted Security Module) configfs interface +/// at `/sys/kernel/config/tsm/report/` to generate attestation quotes. +pub fn get_quote(report_data: &TdxReportData) -> Result> { + configfs::get_quote(report_data).map_err(|e| { + log::error!("failed to get quote via configfs: {}", e); + TdxAttestError::QuoteFailure + }) } -pub fn log_rtmr_event(log: &TdxEventLog) -> anyhow::Result<()> { - // Append to event log - let logline = serde_json::to_string(&log).context("Failed to serialize event log")?; - - let logfile_path = std::path::Path::new(cc_eventlog::RUNTIME_EVENT_LOG_FILE); - let logfile_dir = logfile_path - .parent() - .context("Failed to get event log directory")?; - fs::create_dir_all(logfile_dir).context("Failed to create event log directory")?; - - let mut logfile = fs::OpenOptions::new() - .append(true) - .create(true) - .open(logfile_path) - .context("Failed to open event log file")?; - logfile - .write_all(logline.as_bytes()) - .context("Failed to write to event log file")?; - logfile - .write_all(b"\n") - .context("Failed to write to event log file")?; - Ok(()) +/// Find the TSM measurements sysfs directory +fn find_tsm_measurements_dir() -> Option { + let path = PathBuf::from(TSM_MEASUREMENTS_PATH); + if !path.exists() { + return None; + } + Some(path) } -pub fn extend_rtmr(index: u32, event_type: u32, digest: [u8; 48]) -> Result<()> { - let event = tdx_rtmr_event_t { - version: 1, - rtmr_index: index as u64, - extend_data: digest, - event_type, - event_data_size: 0, - event_data: Default::default(), +/// Extend RTMR using TSM measurements sysfs interface (kernel 6.17+) +fn extend_rtmr_tsm(index: u32, digest: &[u8; 48]) -> anyhow::Result<()> { + let Some(measurements_dir) = find_tsm_measurements_dir() else { + bail!("TSM measurements sysfs not found") }; - let error = unsafe { tdx_att_extend(&event) }; - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); - } - Ok(()) -} -pub fn get_supported_att_key_ids() -> Result> { - let mut list_size = 0; - let error = unsafe { tdx_att_get_supported_att_key_ids(ptr::null_mut(), &mut list_size) }; + let rtmr_file = measurements_dir.join(format!("rtmr{}:sha384", index)); - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); + if !rtmr_file.exists() { + bail!("RTMR{} sysfs file not found: {:?}", index, rtmr_file); } - let mut att_key_id_list = vec![TdxUuid([0; TDX_UUID_SIZE as usize]); list_size as usize]; - - let error = unsafe { - tdx_att_get_supported_att_key_ids( - att_key_id_list.as_mut_ptr() as *mut tdx_uuid_t, - &mut list_size, - ) - }; - - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); - } + fs::write(&rtmr_file, digest)?; + Ok(()) +} - Ok(att_key_id_list) +/// Extend RTMR with automatic fallback +/// +/// Uses TSM measurements sysfs (kernel 6.17+) +pub fn extend_rtmr(index: u32, _event_type: u32, digest: [u8; 48]) -> Result<()> { + extend_rtmr_tsm(index, &digest).map_err(|e| { + log::error!("failed to extend RTMR{}: {}", index, e); + TdxAttestError::NotSupported + }) } diff --git a/tdx-attest/src/linux/configfs.rs b/tdx-attest/src/linux/configfs.rs new file mode 100644 index 00000000..3f37a7b9 --- /dev/null +++ b/tdx-attest/src/linux/configfs.rs @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Pure Rust implementation of TDX quote generation using Linux TSM configfs interface +//! +//! This module provides a native Rust implementation that directly uses the kernel's +//! TSM (Trusted Security Module) configfs interface at `/sys/kernel/config/tsm/report/`. + +use std::io::Read; +use std::path::Path; +use std::time::Duration; + +use anyhow::{bail, Context, Result}; +use fs_err as fs; + +use crate::TdxReportData; + +const CONFIGFS_PATH: &str = "/sys/kernel/config/tsm/report/com.intel.dcap"; +const QUOTE_BUF_SIZE: usize = 8 * 1024; // 8KB +const QUOTE_MIN_SIZE: usize = 1020; +const MAX_RETRIES: usize = 3; +const RETRY_DELAY: Duration = Duration::from_millis(100); + +/// Read generation counter from configfs +fn read_generation() -> Result { + let generation_str = fs::read_to_string(format!("{CONFIGFS_PATH}/generation")) + .context("failed to read generation")?; + let generation = generation_str + .trim() + .parse::() + .context("failed to parse generation")?; + Ok(generation) +} + +/// Get TDX quote via configfs interface +/// +/// This function uses generation counters to detect concurrent access: +/// 1. Read generation counter (gen1) +/// 2. Write report_data to inblob +/// 3. Wait for generation to increment (gen2 = gen1 + 1) +/// 4. Read quote from outblob +/// 5. Verify generation hasn't changed (gen3 = gen2) +pub fn get_quote(report_data: &TdxReportData) -> Result> { + // Verify configfs exists + if !Path::new(CONFIGFS_PATH).exists() { + bail!("TSM configfs not found at {CONFIGFS_PATH}. Is TSM_REPORT enabled in kernel?"); + } + + // Read initial generation + let gen1 = read_generation().context("failed to read generation (1)")?; + + // Write report_data to inblob with retry on EBUSY + let mut last_err = None; + for retry in 0..MAX_RETRIES { + match fs::write(format!("{CONFIGFS_PATH}/inblob"), report_data) { + Ok(_) => { + last_err = None; + break; + } + Err(e) => { + last_err = Some(e); + if retry < MAX_RETRIES - 1 { + std::thread::sleep(RETRY_DELAY); + } + } + } + } + + if let Some(err) = last_err { + bail!("failed to write inblob after {MAX_RETRIES} retries: {err}"); + } + + // Wait for generation to increment + let gen2 = loop { + let gen = read_generation().context("failed to read generation (2)")?; + if gen == gen1 { + // Generation not updated yet, sleep briefly + std::thread::sleep(Duration::from_micros(10)); + continue; + } + break gen; + }; + + // Verify generation incremented by exactly 1 + if gen2 != gen1 + 1 { + bail!("concurrent quote generation detected: gen1={gen1}, gen2={gen2}"); + } + + // Read quote from outblob with retry + let mut quote = vec![0u8; QUOTE_BUF_SIZE]; + let mut quote_len = 0; + let mut last_err = None; + + for retry in 0..MAX_RETRIES { + match fs::File::open(format!("{CONFIGFS_PATH}/outblob")) { + Ok(mut file) => match file.read(&mut quote) { + Ok(len) => { + quote_len = len; + last_err = None; + break; + } + Err(e) => { + last_err = Some(e); + if retry < MAX_RETRIES - 1 { + std::thread::sleep(RETRY_DELAY); + } + } + }, + Err(e) => { + last_err = Some(e); + if retry < MAX_RETRIES - 1 { + std::thread::sleep(RETRY_DELAY); + } + } + } + } + + if let Some(err) = last_err { + bail!("failed to read outblob after {MAX_RETRIES} retries: {err}"); + } + + // Validate quote size + if quote_len == 0 { + bail!("empty quote returned from configfs"); + } + + if quote_len < QUOTE_MIN_SIZE { + bail!("quote too small: got {quote_len} bytes, minimum {QUOTE_MIN_SIZE}"); + } + + if quote_len == QUOTE_BUF_SIZE { + bail!("quote may be truncated: exactly {QUOTE_BUF_SIZE} bytes"); + } + + // Verify generation hasn't changed (no concurrent access) + let gen3 = read_generation().context("failed to read generation (3)")?; + if gen3 != gen2 { + bail!("concurrent quote generation detected after read: gen2={gen2}, gen3={gen3}"); + } + + // Truncate to actual size + quote.truncate(quote_len); + + Ok(quote) +} diff --git a/tpm-attest/Cargo.toml b/tpm-attest/Cargo.toml new file mode 100644 index 00000000..931b5b63 --- /dev/null +++ b/tpm-attest/Cargo.toml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "tpm-attest" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +hex = { workspace = true, features = ["alloc"] } +serde = { workspace = true, features = ["derive"] } +serde-human-bytes.workspace = true +serde_json = { workspace = true, features = ["std"] } +sha2 = { workspace = true, features = ["oid"] } +tempfile.workspace = true +tracing.workspace = true +fs-err.workspace = true +tpm-types.workspace = true +dstack-types.workspace = true +tpm2.workspace = true +scale.workspace = true diff --git a/tpm-attest/src/esapi.rs b/tpm-attest/src/esapi.rs new file mode 100644 index 00000000..7b5c4185 --- /dev/null +++ b/tpm-attest/src/esapi.rs @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::{PcrSelection, PcrValue}; +use anyhow::{bail, Result}; +use tpm2::{TpmAlgId, TpmContext as RawTpmContext, TpmlPcrSelection, TpmsNvPublic}; + +pub struct EsapiContext { + context: RawTpmContext, +} + +impl EsapiContext { + /// Create a new ESAPI context with the given TCTI path + pub fn new(tcti_path: Option<&str>) -> Result { + let context = RawTpmContext::new(tcti_path)?; + Ok(Self { context }) + } + + // ==================== NV Operations ==================== + + /// Check if an NV index exists + pub fn nv_exists(&mut self, index: u32) -> Result { + self.context.nv_exists(index) + } + + /// Read NV public area (to determine the defined size, attributes, etc.) + pub fn nv_read_public(&mut self, index: u32) -> Result { + self.context.nv_read_public(index) + } + + /// Read data from an NV index + pub fn nv_read(&mut self, index: u32) -> Result>> { + self.context.nv_read(index) + } + + /// Write data to an NV index + pub fn nv_write(&mut self, index: u32, data: &[u8]) -> Result { + self.context.nv_write(index, data) + } + + /// Define a new NV index + pub fn nv_define(&mut self, index: u32, size: usize, owner_read_write: bool) -> Result { + self.context.nv_define(index, size, owner_read_write) + } + + /// Undefine (delete) an NV index + pub fn nv_undefine(&mut self, index: u32) -> Result { + self.context.nv_undefine(index) + } + + // ==================== PCR Operations ==================== + + /// Read PCR values for the given selection + pub fn pcr_read(&mut self, pcr_selection: &PcrSelection) -> Result> { + let hash_alg = Self::parse_hash_alg(&pcr_selection.bank)?; + let tpm_selection = TpmlPcrSelection::single(hash_alg, &pcr_selection.pcrs); + + let values = self.context.pcr_read(&tpm_selection)?; + + Ok(values + .into_iter() + .map(|(index, value)| PcrValue { + index, + algorithm: pcr_selection.bank.clone(), + value, + }) + .collect()) + } + + /// Extend a PCR with a hash value + pub fn pcr_extend(&mut self, pcr: u32, hash: &[u8], bank: &str) -> Result<()> { + let hash_alg = Self::parse_hash_alg(bank)?; + self.context.pcr_extend(pcr, hash, hash_alg) + } + + // ==================== Random Number Generation ==================== + + /// Generate random bytes using the TPM's hardware RNG + pub fn get_random(&mut self) -> Result<[u8; N]> { + self.context.get_random_array::() + } + + // ==================== Primary Key Operations ==================== + + /// Check if a persistent handle exists + pub fn handle_exists(&mut self, handle: u32) -> Result { + self.context.handle_exists(handle) + } + + /// Ensure a persistent primary key exists at the given handle + pub fn ensure_primary_key(&mut self, handle: u32) -> Result { + self.context.ensure_primary_key(handle) + } + + // ==================== Seal/Unseal Operations ==================== + + /// Seal data to TPM with PCR policy + pub fn seal( + &mut self, + data: &[u8], + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result<(Vec, Vec)> { + let hash_alg = Self::parse_hash_alg(&pcr_selection.bank)?; + let tpm_selection = TpmlPcrSelection::single(hash_alg, &pcr_selection.pcrs); + + self.context + .seal(data, parent_handle, &tpm_selection, hash_alg) + } + + /// Unseal data from TPM with PCR policy + pub fn unseal( + &mut self, + pub_bytes: &[u8], + priv_bytes: &[u8], + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result> { + let hash_alg = Self::parse_hash_alg(&pcr_selection.bank)?; + let tpm_selection = TpmlPcrSelection::single(hash_alg, &pcr_selection.pcrs); + + self.context.unseal( + pub_bytes, + priv_bytes, + parent_handle, + &tpm_selection, + hash_alg, + ) + } + + // ==================== Helper Functions ==================== + + fn parse_hash_alg(bank: &str) -> Result { + match bank { + "sha256" => Ok(TpmAlgId::Sha256), + "sha384" => Ok(TpmAlgId::Sha384), + "sha512" => Ok(TpmAlgId::Sha512), + "sha1" => Ok(TpmAlgId::Sha1), + _ => bail!("unsupported hash algorithm: {}", bank), + } + } +} diff --git a/tpm-attest/src/gcp_ak.rs b/tpm-attest/src/gcp_ak.rs new file mode 100644 index 00000000..56a70313 --- /dev/null +++ b/tpm-attest/src/gcp_ak.rs @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! GCP vTPM pre-provisioned AK loading +//! +//! This module provides native Rust implementation for loading GCP's +//! pre-provisioned Attestation Key without C library dependencies. + +use std::str::FromStr; + +use anyhow::{Context as _, Result}; +use tracing::debug; + +use crate::{PcrSelection, PcrValue, TpmEventLog, TpmQuote}; +use tpm2::{tpm_rh, TpmAlgId, TpmContext, TpmlPcrSelection}; + +/// GCP vTPM NV indices for pre-provisioned AK +pub mod gcp_nv_index { + /// RSA AK certificate (DER format) + pub const AK_RSA_CERT: u32 = 0x01C10000; + /// RSA AK template (TPM2B_PUBLIC format) + pub const AK_RSA_TEMPLATE: u32 = 0x01C10001; + /// ECC AK certificate (DER format) + pub const AK_ECC_CERT: u32 = 0x01C10002; + /// ECC AK template (TPM2B_PUBLIC format) + pub const AK_ECC_TEMPLATE: u32 = 0x01C10003; +} + +/// Loaded AK information +pub struct LoadedAk { + pub context: TpmContext, + pub handle: u32, + pub cert_nv_index: u32, +} + +/// Load GCP pre-provisioned ECC AK +/// +/// This function: +/// 1. Reads the AK template from NV index 0x01C10003 +/// 2. Creates a primary key under Endorsement hierarchy with the template +/// 3. TPM deterministically recreates the same key pair (same template + same parent) +pub fn load_gcp_ak_ecc(tcti_path: Option<&str>) -> Result { + debug!("loading GCP pre-provisioned ECC AK..."); + + let mut context = TpmContext::new(tcti_path)?; + + // Read AK template from NV + let template_bytes = context + .nv_read(gcp_nv_index::AK_ECC_TEMPLATE)? + .ok_or_else(|| anyhow::anyhow!("ECC AK template not found at NV 0x01C10003"))?; + + debug!( + "read ECC AK template from NV: {} bytes", + template_bytes.len() + ); + + // Create primary key under Endorsement hierarchy + let (handle, _public) = + context.create_primary_from_template(tpm_rh::ENDORSEMENT, &template_bytes)?; + + debug!( + "✓ successfully loaded GCP pre-provisioned ECC AK (handle: 0x{:08x})", + handle + ); + + Ok(LoadedAk { + context, + handle, + cert_nv_index: gcp_nv_index::AK_ECC_CERT, + }) +} + +/// Load GCP pre-provisioned RSA AK +/// +/// This function: +/// 1. Reads the AK template from NV index 0x01C10001 +/// 2. Creates a primary key under Endorsement hierarchy with the template +/// 3. TPM deterministically recreates the same key pair (same template + same parent) +pub fn load_gcp_ak_rsa(tcti_path: Option<&str>) -> Result { + debug!("loading GCP pre-provisioned RSA AK..."); + + let mut context = TpmContext::new(tcti_path)?; + + // Read AK template from NV + let template_bytes = context + .nv_read(gcp_nv_index::AK_RSA_TEMPLATE)? + .ok_or_else(|| anyhow::anyhow!("RSA AK template not found at NV 0x01C10001"))?; + + debug!( + "read RSA AK template from NV: {} bytes", + template_bytes.len() + ); + + // Create primary key under Endorsement hierarchy + let (handle, _public) = + context.create_primary_from_template(tpm_rh::ENDORSEMENT, &template_bytes)?; + + debug!( + "✓ successfully loaded GCP pre-provisioned RSA AK (handle: 0x{:08x})", + handle + ); + + Ok(LoadedAk { + context, + handle, + cert_nv_index: gcp_nv_index::AK_RSA_CERT, + }) +} + +/// Key algorithm preference for quote generation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyAlgorithm { + /// Prefer ECC, fallback to RSA + Auto, + /// Use ECC only (fails if not available) + Ecc, + /// Use RSA only (fails if not available) + Rsa, +} + +impl FromStr for KeyAlgorithm { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "auto" => Ok(KeyAlgorithm::Auto), + "ecc" | "ecdsa" => Ok(KeyAlgorithm::Ecc), + "rsa" | "rsassa" => Ok(KeyAlgorithm::Rsa), + _ => anyhow::bail!("invalid key algorithm: {s}. Use 'auto', 'ecc', or 'rsa'"), + } + } +} + +/// Generate a TPM quote using GCP pre-provisioned AK (prefers ECC) +pub fn create_quote_with_gcp_ak( + tcti_path: Option<&str>, + qualifying_data: &[u8; 32], + pcr_selection: &PcrSelection, +) -> Result { + create_quote_with_gcp_ak_algo( + tcti_path, + qualifying_data, + pcr_selection, + KeyAlgorithm::Auto, + ) +} + +/// Generate a TPM quote using GCP pre-provisioned AK with manual algorithm selection +pub fn create_quote_with_gcp_ak_algo( + tcti_path: Option<&str>, + qualifying_data: &[u8; 32], + pcr_selection: &PcrSelection, + key_algo: KeyAlgorithm, +) -> Result { + let platform = dstack_types::Platform::detect().context("Unsupported platform")?; + + debug!("generating TPM quote with GCP pre-provisioned AK..."); + + // Load GCP pre-provisioned AK based on algorithm preference + let mut loaded_ak = match key_algo { + KeyAlgorithm::Auto => { + // Try ECC first (better performance), fallback to RSA + match load_gcp_ak_ecc(tcti_path) { + Ok(ak) => { + debug!("✓ using ECC AK for quote"); + ak + } + Err(e) => { + debug!("ECC AK not available, falling back to RSA: {e}"); + let ak = load_gcp_ak_rsa(tcti_path)?; + debug!("✓ using RSA AK for quote"); + ak + } + } + } + KeyAlgorithm::Ecc => { + let ak = load_gcp_ak_ecc(tcti_path).context( + "failed to load ECC AK (use --key-algo=rsa or --key-algo=auto for fallback)", + )?; + debug!("✓ using ECC AK for quote"); + ak + } + KeyAlgorithm::Rsa => { + let ak = load_gcp_ak_rsa(tcti_path).context("failed to load RSA AK")?; + debug!("✓ using RSA AK for quote"); + ak + } + }; + + // Convert hash algorithm + let hash_alg = match pcr_selection.bank.as_str() { + "sha256" => TpmAlgId::Sha256, + "sha384" => TpmAlgId::Sha384, + "sha512" => TpmAlgId::Sha512, + _ => anyhow::bail!( + "unsupported hash algorithm: {bank}", + bank = pcr_selection.bank + ), + }; + + // Build PCR selection + let tpm_pcr_selection = TpmlPcrSelection::single(hash_alg, &pcr_selection.pcrs); + + // Generate quote + debug!("calling TPM Quote command..."); + let (message, signature) = + loaded_ak + .context + .quote(loaded_ak.handle, qualifying_data, &tpm_pcr_selection)?; + + debug!("✓ quote generated successfully"); + + // Read PCR values + let pcr_values_raw = loaded_ak.context.pcr_read(&tpm_pcr_selection)?; + let pcr_values: Vec = pcr_values_raw + .into_iter() + .map(|(index, value)| PcrValue { + index, + algorithm: pcr_selection.bank.clone(), + value, + }) + .collect(); + + // Read AK certificate from NV + let ak_cert = loaded_ak + .context + .nv_read(loaded_ak.cert_nv_index)? + .ok_or_else(|| { + anyhow::anyhow!( + "AK certificate not found at NV 0x{:08x}", + loaded_ak.cert_nv_index + ) + })?; + + debug!( + "✓ AK certificate read from NV 0x{:08x}: {} bytes", + loaded_ak.cert_nv_index, + ak_cert.len() + ); + + // Flush the AK handle + let _ = loaded_ak.context.flush_context(loaded_ak.handle); + + let event_log = TpmEventLog::from_kernel_file() + .context("Failed to read TPM event log")? + .events; + + Ok(TpmQuote { + message, + signature, + pcr_values, + ak_cert, + platform, + event_log, + }) +} diff --git a/tpm-attest/src/lib.rs b/tpm-attest/src/lib.rs new file mode 100644 index 00000000..10e179fd --- /dev/null +++ b/tpm-attest/src/lib.rs @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 Attestation Library +//! +//! This module provides functionality for generating TPM attestation quotes +//! on the device side. It handles PCR operations, sealing, unsealing, NV storage, +//! and quote generation. +//! +//! This follows the same architecture as tdx-attest: device-side attestation only. +//! For quote verification, see the tpm-qvl crate. + +use anyhow::{bail, Context, Result}; +use scale::{Decode, Encode}; +use std::path::Path; +use tracing::{debug, warn}; + +// Re-export tpm-types +pub use tpm_types::{PcrSelection, PcrValue, TpmEvent, TpmEventLog, TpmQuote}; + +mod esapi; +use esapi::EsapiContext; + +pub const PRIMARY_KEY_HANDLE: u32 = 0x81000100; +pub const SEALED_NV_INDEX: u32 = 0x01801101; + +/// PCR selection for DStack +/// 0: The firmware version and NonHostInfo (representing the memory encryption technology) +/// 2: The uki image (kernel + initrd + initramfs) +/// 14: The app compose hash +const APP_PCR: u32 = 14; +pub fn dstack_pcr_policy() -> PcrSelection { + PcrSelection::sha256(&[0, 2, APP_PCR]) +} + +pub struct TpmContext { + tcti: String, +} + +impl std::fmt::Debug for TpmContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TpmContext") + .field("tcti", &self.tcti) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct SealedBlob { + pub data: Vec, +} + +impl SealedBlob { + pub fn new(data: Vec) -> Self { + Self { data } + } + + pub fn from_parts(pub_data: &[u8], priv_data: &[u8]) -> Self { + let data = (pub_data, priv_data).encode(); + Self { data } + } + + pub fn split(&self) -> Result<(Vec, Vec)> { + if self.data.len() < 4 { + bail!("sealed blob too small"); + } + let (pub_data, priv_data) = Decode::decode(&mut &self.data[..])?; + Ok((pub_data, priv_data)) + } +} + +impl TpmContext { + pub fn open(tcti: Option<&str>) -> Result { + match tcti { + Some(t) => Self::new(t), + None => Self::detect(), + } + } + + pub fn detect() -> Result { + let tcti = if Path::new("/dev/tpmrm0").exists() { + "/dev/tpmrm0" + } else if Path::new("/dev/tpm0").exists() { + "/dev/tpm0" + } else { + bail!("TPM device not found"); + }; + Self::new(tcti) + } + + pub fn new(tcti: &str) -> Result { + Ok(Self { + tcti: tcti.to_string(), + }) + } + + fn create_esapi_context(&self) -> Result { + EsapiContext::new(Some(&self.tcti)) + } + + pub fn nv_exists(&self, index: u32) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.nv_exists(index) + } + + pub fn nv_define(&self, index: u32, size: usize, _attributes: &str) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.nv_define(index, size, true) + } + + pub fn nv_undefine(&self, index: u32) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.nv_undefine(index) + } + + pub fn nv_read(&self, index: u32) -> Result>> { + let mut ctx = self.create_esapi_context()?; + ctx.nv_read(index) + } + + pub fn nv_write(&self, index: u32, data: &[u8]) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.nv_write(index, data) + } + + pub fn handle_exists(&self, handle: u32) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.handle_exists(handle) + } + + pub fn ensure_primary_key(&self, handle: u32) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.ensure_primary_key(handle) + } + + pub fn pcr_extend(&self, pcr: u32, hash: &[u8], bank: &str) -> Result<()> { + let mut ctx = self.create_esapi_context()?; + ctx.pcr_extend(pcr, hash, bank) + } + + pub fn pcr_extend_sha256(&self, pcr: u32, hash: &[u8; 32]) -> Result<()> { + self.pcr_extend(pcr, hash, "sha256") + } + + pub fn dump_pcr_values(&self, selection: &PcrSelection) { + match self + .create_esapi_context() + .and_then(|mut ctx| ctx.pcr_read(selection)) + { + Ok(values) => { + debug!("PCR values ({}):", selection.to_arg()); + for pv in values { + debug!(" PCR[{}] = {}", pv.index, hex::encode(&pv.value)); + } + } + Err(e) => { + warn!("failed to read PCR values: {e}"); + } + } + } + + pub fn get_random(&self) -> Result<[u8; N]> { + let mut ctx = self.create_esapi_context()?; + ctx.get_random::() + } + + pub fn seal( + &self, + data: &[u8], + nv_index: u32, + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result<()> { + let mut ctx = self.create_esapi_context()?; + + ctx.ensure_primary_key(parent_handle)?; + + let (pub_bytes, priv_bytes) = ctx.seal(data, parent_handle, pcr_selection)?; + + let sealed_blob = SealedBlob::from_parts(&pub_bytes, &priv_bytes); + + let needed_size = sealed_blob.data.len(); + if ctx.nv_exists(nv_index)? { + let nv_public = ctx.nv_read_public(nv_index)?; + if (nv_public.data_size as usize) < needed_size { + ctx.nv_undefine(nv_index)?; + ctx.nv_define(nv_index, needed_size, true)?; + } + } else { + ctx.nv_define(nv_index, needed_size, true)?; + } + + ctx.nv_write(nv_index, &sealed_blob.data)?; + + debug!("sealed data to NV index 0x{nv_index:08x}"); + Ok(()) + } + + pub fn unseal_to_vec( + &self, + nv_index: u32, + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result>> { + let mut ctx = self.create_esapi_context()?; + + let sealed_data = match ctx.nv_read(nv_index)? { + Some(data) => data, + None => return Ok(None), + }; + + let sealed_blob = SealedBlob::new(sealed_data); + let (pub_bytes, priv_bytes) = match sealed_blob.split() { + Ok(v) => v, + Err(e) => { + warn!("sealed blob in NV index 0x{nv_index:08x} is invalid ({e}); regenerating"); + let _ = ctx.nv_undefine(nv_index); + return Ok(None); + } + }; + + let data = ctx.unseal(&pub_bytes, &priv_bytes, parent_handle, pcr_selection)?; + + debug!("unsealed data from NV index 0x{nv_index:08x}"); + Ok(Some(data)) + } + + pub fn unseal( + &self, + nv_index: u32, + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result> { + match self.unseal_to_vec(nv_index, parent_handle, pcr_selection)? { + Some(data) => { + let array: [u8; N] = data + .try_into() + .ok() + .context("unsealed data size mismatch")?; + Ok(Some(array)) + } + None => Ok(None), + } + } + + pub fn create_quote( + &self, + qualifying_data: &[u8; 32], + pcr_selection: &PcrSelection, + ) -> Result { + gcp_ak::create_quote_with_gcp_ak(Some(&self.tcti), qualifying_data, pcr_selection) + } + + pub fn create_quote_with_algo( + &self, + qualifying_data: &[u8; 32], + pcr_selection: &PcrSelection, + key_algo: KeyAlgorithm, + ) -> Result { + gcp_ak::create_quote_with_gcp_ak_algo( + Some(&self.tcti), + qualifying_data, + pcr_selection, + key_algo, + ) + } + + pub fn read_ak_cert(&self) -> Result>> { + const AK_RSA_CERT_NV_INDEX: u32 = 0x01C10000; + const AK_ECC_CERT_NV_INDEX: u32 = 0x01C10002; + + let mut ctx = self.create_esapi_context()?; + + if let Some(cert) = ctx.nv_read(AK_RSA_CERT_NV_INDEX)? { + debug!( + "read AK certificate from NV index 0x{AK_RSA_CERT_NV_INDEX:08x} ({} bytes)", + cert.len() + ); + return Ok(Some(cert)); + } + + if let Some(cert) = ctx.nv_read(AK_ECC_CERT_NV_INDEX)? { + debug!( + "read AK certificate from NV index 0x{AK_ECC_CERT_NV_INDEX:08x} ({} bytes)", + cert.len() + ); + return Ok(Some(cert)); + } + + warn!("AK certificate not found in TPM NV storage (expected on GCP vTPM)"); + Ok(None) + } + + pub fn read_event_log(&self, pcr_index: u32) -> Result> { + let event_log = + TpmEventLog::from_kernel_file().context("Failed to read TPM Event Log from kernel")?; + + Ok(event_log.filter_by_pcr(pcr_index)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pcr_selection_to_string() { + let sel = PcrSelection::sha256(&[0, 1, 2, 7]); + assert_eq!(sel.to_arg(), "sha256:0,1,2,7"); + } + + #[test] + fn test_sealed_blob_split() { + let pub_data = vec![0x01, 0x02, 0x03, 0x04, 0x05]; + let priv_data = vec![0xAA, 0xBB, 0xCC]; + + let blob = SealedBlob::from_parts(&pub_data, &priv_data); + let (pub_part, priv_part) = blob.split().unwrap(); + + assert_eq!(pub_part, pub_data); + assert_eq!(priv_part, priv_data); + } + + #[test] + fn test_default_pcr_policy() { + let policy = dstack_pcr_policy(); + assert_eq!(policy.to_arg(), "sha256:0,2,14"); + } +} + +mod gcp_ak; +pub use gcp_ak::{ + create_quote_with_gcp_ak, create_quote_with_gcp_ak_algo, gcp_nv_index, load_gcp_ak_ecc, + load_gcp_ak_rsa, KeyAlgorithm, +}; diff --git a/tpm-attest/tests/tpm_quote_sample.bin b/tpm-attest/tests/tpm_quote_sample.bin new file mode 100644 index 00000000..4866ce80 Binary files /dev/null and b/tpm-attest/tests/tpm_quote_sample.bin differ diff --git a/tpm-qvl/Cargo.toml b/tpm-qvl/Cargo.toml new file mode 100644 index 00000000..6f66be59 --- /dev/null +++ b/tpm-qvl/Cargo.toml @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "tpm-qvl" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +hex.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = ["std"] } +tracing.workspace = true +dstack-types.workspace = true + +# Cryptographic verification +rsa = "0.9" +p256 = { version = "0.13", features = ["ecdsa", "pem"] } +x509-parser = "0.16" +nom = "7.1" +base64 = "0.22" +sha2 = { workspace = true, features = ["oid"] } + +# Certificate chain verification +rustls-webpki = { version = "0.103.8", features = ["alloc", "ring"] } +rustls-pki-types = "1.13.1" +dcap-qvl-webpki = { version = "0.103", features = ["alloc", "ring"] } +pem = "3.0" + +# TPM quote data structures +tpm-types.workspace = true + +# CRL download (optional) +reqwest = { version = "0.12", default-features = false, features = [ + "rustls-tls", + "blocking", +], optional = true } + +[features] +default = ["crl-download"] +crl-download = ["reqwest"] diff --git a/tpm-qvl/certs/gcp-root-ca.pem b/tpm-qvl/certs/gcp-root-ca.pem new file mode 100644 index 00000000..080fdeaf --- /dev/null +++ b/tpm-qvl/certs/gcp-root-ca.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIUAKZdpPnjKPOANcOnPU9yQyvfFdwwDQYJKoZIhvcNAQEL +BQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv +b2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0EgUm9vdDAgFw0yMjA3MDgwMDQw +MzRaGA8yMTIyMDcwODA1NTcyM1owfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh +bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2ds +ZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0Eg +Um9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ0l9VCoyJZLSol8 +KyhNpbS7pBnuicE6ptrdtxAWIR2TnLxSgxNFiR7drtofxI0ruceoCIpsa9NHIKrz +3sM/N/E8mFNHiJAuyVf3pPpmDpLJZQ1qe8yHkpGSs3Kj3s5YYWtEecCVfzNs4MtK +vGfA+WKB49A6Noi8R9R1GonLIN6wSXX3kP1ibRn0NGgdqgfgRe5HC3kKAhjZ6scT +8Eb1SGlaByGzE5WoGTnNbyifkyx9oUZxXVJsqv2q611W3apbPxcgev8z5JXQUbrr +Q7EbO0StK1DsKRsKLuD+YLxjrBRQ4UeIN5WHp6G0vgYiOptHm6YKZxQemO/kVMLR +zsm1AYH7eNOFekcBIKRjSqpk5m4ud04qum6f0hBj3iE/Pe+DvIbVhLh9ItAunISG +QPA9dYEgfA/qWir+pU7LV3phpLeGhull8G/zYmQhF3heg0buIR70aavzT8iLAQrx +VMNRZJEGMwIN/tq8YiT3+3EZIcSqq6GAGjiuVw3NIsXC3+CuSJGQ5GbDp49Lc6VW +PHeWeFvwSUGgxKXq5r1+PRsoYgK6S4hhecgXEX5c7Rta6TcFlEFb0XK9fpy1dr89 +LeFGxUBpdDvKxDRLMm3FQen8rmR/PSReEcJsaqbUP/q7Pc7k0RfF9Mb6AfPZfnqg +pYJQ+IFSr9EjRSW1wPcL03zoTP47AgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIBBjAQ +BgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRJ50pb +Vin1nXm3pjA8A7KP5xTdTDAfBgNVHSMEGDAWgBRJ50pbVin1nXm3pjA8A7KP5xTd +TDANBgkqhkiG9w0BAQsFAAOCAgEAlfHRvOB3CJoLTl1YG/AvjGoZkpNMyp5X5je1 +ICCQ68b296En9hIUlcYY/+nuEPSPUjDA3izwJ8DAfV4REgpQzqoh6XhR3TgyfHXj +J6DC7puzEgtzF1+wHShUpBoe3HKuL4WhB3rvwk2SEsudBu92o9BuBjcDJ/GW5GRt +pD/H71HAE8rI9jJ41nS0FvkkjaX0glsntMVUXiwcta8GI0QOE2ijsJBwk41uQGt0 +YOj2SGlEwNAC5DBTB5kZ7+6X9xGE6/c+M3TAA0ONoX18rNfif94cCx/mPYOs8pUk +ANRAQ4aTRBvpBrryGT8R1ahTBkMeRQG3tdsLHRT8fJCFUANd5WLWsi83005y/WuM +z8/gFKc0PL+F+MubCsJ1ODPTRscH93QlS4zEMg5hDAIks+fDoRJ2QiROqo7GAqbT +c7STKfGcr9+pa63na7f3oy1sZPWPdxB8tx5z3lghiPP3ktQx/yK/1Fwf1hgxJHFy +/2UcaGuOXRRRTPyEnppZp82Kigs9aPHWtaVm2/LrXX2fvT9iM/k0CovNAj8rztHx +sUEoA0xJnSOJNPpe9PRdjsTj7/u3Xu6hQLNNidBHgI3Hcmi704HMMd/3yZ424OOr +S32ylpeU1oeQHFrLE6hYX4/ttMETbmESIKd2rTgstPotSvkuB5TljbKYPR+lq7hQ +av16U4E= +-----END CERTIFICATE----- diff --git a/tpm-qvl/src/collateral.rs b/tpm-qvl/src/collateral.rs new file mode 100644 index 00000000..2c08b6b5 --- /dev/null +++ b/tpm-qvl/src/collateral.rs @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Collateral retrieval module +//! +//! This module implements the first step of dcap-qvl architecture: +//! extracting certificate chain information and downloading CRLs. + +use anyhow::{bail, Context, Result}; +use tracing::{debug, warn}; +use x509_parser::{extensions::DistributionPointName, prelude::*}; + +use tpm_types::TpmQuote; + +use crate::{get_root_ca, verify::VerifiedReport, QuoteCollateral}; + +pub async fn get_collateral_and_verify(quote: &TpmQuote) -> Result { + let root_ca_pem = get_root_ca(quote.platform).context("failed to get root CA")?; + let collateral = get_collateral(quote, root_ca_pem).await?; + crate::verify::verify_quote_with_ca(quote, &collateral, root_ca_pem).map_err(Into::into) +} + +pub async fn get_collateral(quote: &TpmQuote, root_ca_pem: &str) -> Result { + debug!("fetching quote collateral (intermediate cert chain + CRLs)"); + + let ak_cert_der = "e.ak_cert; + debug!("AK certificate (leaf) found: {} bytes", ak_cert_der.len()); + + // Build certificate chain from device (via AIA) + let chain_ders = build_cert_chain(ak_cert_der)?; + // Download CRLs from device-provided cert chain + let crls = download_crls_for_certs(&chain_ders)?; + + // Download CRL from verifier-provided root CA + let root_ca_crl = { + let root_ca_der = + extract_certs_webpki(root_ca_pem.as_bytes()).context("failed to parse root CA PEM")?; + if root_ca_der.len() != 1 { + bail!("expected 1 root CA, found {}", root_ca_der.len()); + } + download_crl_for_cert(&root_ca_der[0])? + }; + + debug!( + "✓ collateral fetched: {} intermediate CRL(s), root CA CRL: {}", + crls.len(), + if root_ca_crl.is_some() { "yes" } else { "no" } + ); + let cert_chain_pem = ders_to_pem(&chain_ders)?; + Ok(QuoteCollateral { + cert_chain_pem, + crls, + root_ca_crl, + }) +} + +/// Build certificate chain by following AIA links (stops before root) +fn build_cert_chain(leaf_cert_der: &[u8]) -> Result>> { + let mut chain_ders = Vec::new(); + chain_ders.push(leaf_cert_der.to_vec()); + let mut current_cert_der = leaf_cert_der.to_vec(); + + loop { + let Some(url) = extract_aia_ca_issuers(¤t_cert_der)? else { + debug!("no AIA found - reached end of AIA chain"); + break; + }; + debug!("downloading parent cert from: {url}"); + let parent_der = download_cert(&url)?; + // Stop if we hit a self-signed cert (root CA) + if is_self_signed(&parent_der)? { + debug!("found self-signed cert - stopping (root CA should be provided by verifier)"); + break; + } + chain_ders.push(parent_der.clone()); + current_cert_der = parent_der; + } + + debug!("built chain with {} certificate(s)", chain_ders.len()); + Ok(chain_ders) +} + +/// Download CRLs for given certificates +fn download_crls_for_certs(certs: &[Vec]) -> Result>> { + debug!("downloading CRLs from device-provided cert chain..."); + + let mut crls = Vec::new(); + + for cert_der in certs { + let Some(crl) = download_crl_for_cert(cert_der).context("failed to download CRL")? else { + continue; + }; + crls.push(crl); + } + Ok(crls) +} + +/// Download CRL for verifier-provided root CA +fn download_crl_for_cert(cert: &[u8]) -> Result>> { + let crl_urls = extract_crl_urls(cert)?; + if crl_urls.is_empty() { + debug!("verifier root CA has no CRL DP - will skip root CA CRL check"); + return Ok(None); + } + + download_first_available_crl(&crl_urls).map(Some) +} + +/// Download first available CRL from a list of URLs +fn download_first_available_crl(urls: &[String]) -> Result> { + for url in urls { + debug!("downloading CRL from {url}"); + match download_crl(url) { + Ok(crl) => { + return Ok(crl); + } + Err(e) => { + warn!("✗ failed to download CRL from {url}: {e:?}"); + continue; + } + } + } + bail!("failed to download CRL") +} + +/// Convert DER certificates to PEM format +fn ders_to_pem(ders: &[Vec]) -> Result { + let mut pem = String::new(); + for der in ders.iter() { + pem.push_str(&der_to_pem(der, "CERTIFICATE")?); + } + Ok(pem) +} + +/// Check if certificate is self-signed +fn is_self_signed(cert_der: &[u8]) -> Result { + let (_, cert) = X509Certificate::from_der(cert_der).context("failed to parse certificate")?; + Ok(cert.subject() == cert.issuer()) +} + +fn extract_certs_webpki(cert_pem: &[u8]) -> Result>> { + use ::pem::parse_many; + + let pem_items = parse_many(cert_pem).context("failed to parse PEM")?; + + let certs = pem_items + .into_iter() + .map(|pem| rustls_pki_types::CertificateDer::from(pem.into_contents())) + .collect(); + + Ok(certs) +} + +fn download_crl(url: &str) -> Result> { + debug!("downloading CRL from {url}"); + + let response = + reqwest::blocking::get(url).context(format!("failed to download CRL from {url}"))?; + + if !response.status().is_success() { + bail!("CRL download failed with status: {}", response.status()); + } + + let crl_bytes = response + .bytes() + .context("failed to read CRL response body")? + .to_vec(); + + debug!("downloaded {} bytes CRL from {}", crl_bytes.len(), url); + + Ok(crl_bytes) +} + +fn extract_crl_urls(cert_der: &[u8]) -> Result> { + use x509_parser::extensions::ParsedExtension; + + let (_, cert) = X509Certificate::from_der(cert_der).context("failed to parse certificate")?; + + let mut crl_urls = Vec::new(); + + for ext in cert.extensions() { + let ParsedExtension::CRLDistributionPoints(crl_dist_points) = ext.parsed_extension() else { + continue; + }; + for dist_point in crl_dist_points.points.iter() { + let Some(dist_point_name) = &dist_point.distribution_point else { + continue; + }; + + let DistributionPointName::FullName(names) = dist_point_name else { + continue; + }; + for name in names.iter() { + let x509_parser::extensions::GeneralName::URI(uri) = name else { + continue; + }; + crl_urls.push(uri.to_string()); + debug!("found CRL URL: {uri}"); + } + } + } + + if crl_urls.is_empty() { + debug!("no CRL Distribution Points found in certificate"); + } + + Ok(crl_urls) +} + +fn extract_aia_ca_issuers(cert_der: &[u8]) -> Result> { + use x509_parser::extensions::ParsedExtension; + + let (_, cert) = X509Certificate::from_der(cert_der).context("failed to parse certificate")?; + + for ext in cert.extensions() { + let ParsedExtension::AuthorityInfoAccess(aia) = ext.parsed_extension() else { + continue; + }; + + for access_desc in &aia.accessdescs { + const OID_CA_ISSUERS: &[u64] = &[1, 3, 6, 1, 5, 5, 7, 48, 2]; + let oid_bytes: Vec = match access_desc.access_method.iter() { + Some(iter) => iter.collect(), + None => continue, + }; + + if oid_bytes == OID_CA_ISSUERS { + if let x509_parser::extensions::GeneralName::URI(uri) = &access_desc.access_location + { + debug!("found AIA CA Issuers URL: {uri}"); + return Ok(Some(uri.to_string())); + } + } + } + } + + debug!("no AIA CA Issuers URL found in certificate"); + Ok(None) +} + +fn download_cert(url: &str) -> Result> { + debug!("downloading certificate from {url}"); + + let response = reqwest::blocking::get(url) + .context(format!("failed to download certificate from {url}"))?; + + if !response.status().is_success() { + bail!( + "certificate download failed with status: {}", + response.status() + ); + } + + let cert_bytes = response + .bytes() + .context("failed to read certificate response body")? + .to_vec(); + + debug!( + "downloaded {} bytes certificate from {}", + cert_bytes.len(), + url + ); + + Ok(cert_bytes) +} + +fn der_to_pem(der: &[u8], label: &str) -> Result { + use base64::Engine; + + let b64 = base64::engine::general_purpose::STANDARD.encode(der); + + let mut pem = format!("-----BEGIN {label}-----\n"); + for chunk in b64.as_bytes().chunks(64) { + pem.push_str(std::str::from_utf8(chunk)?); + pem.push('\n'); + } + pem.push_str(&format!("-----END {label}-----\n")); + + Ok(pem) +} diff --git a/tpm-qvl/src/lib.rs b/tpm-qvl/src/lib.rs new file mode 100644 index 00000000..7e839289 --- /dev/null +++ b/tpm-qvl/src/lib.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM Quote Verification Library (QVL) +//! +//! This module provides quote verification and collateral management for TPM attestation. +//! It follows the dcap-qvl architecture for Intel TDX verification. +//! +//! # Architecture +//! - **Step 1**: `get_collateral()` - Extract cert chain and download CRLs +//! - **Step 2**: `verify_quote()` - Verify quote with collateral +//! +//! This crate is designed to run on the verifier side, while tpm-attest runs on the device side. + +use anyhow::{bail, Result}; +use dstack_types::Platform; +use serde::{Deserialize, Serialize}; + +/// GCP TPM Root CA certificate (embedded, valid 2022-2122) +/// +/// Subject: CN=EK/AK CA Root, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US +/// Valid: 2022-07-08 to 2122-07-08 (100 years) +pub const GCP_ROOT_CA: &str = include_str!("../certs/gcp-root-ca.pem"); + +/// Get TPM root CA certificate for the given platform +pub fn get_root_ca(platform: Platform) -> Result<&'static str> { + match platform { + Platform::Gcp => Ok(GCP_ROOT_CA), + Platform::Dstack => bail!("dstack platform does not use TPM attestation"), + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuoteCollateral { + /// Intermediate certificate chain (PEM format) from device + /// Does NOT include root CA (which must be provided independently by verifier) + pub cert_chain_pem: String, + /// All CRLs extracted from device-provided cert chain + pub crls: Vec>, + /// Root CA CRL extracted from verifier-provided root CA + pub root_ca_crl: Option>, +} + +#[derive(Debug)] +pub struct VerificationError { + pub status: VerificationStatus, + pub error: anyhow::Error, +} + +impl std::fmt::Display for VerificationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "verification failed: {}", self.error) + } +} + +impl std::error::Error for VerificationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.error.source() + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct VerificationStatus { + pub ak_verified: bool, + pub signature_verified: bool, + pub pcr_verified: bool, +} + +#[cfg(feature = "crl-download")] +pub use collateral::{get_collateral, get_collateral_and_verify}; + +pub use verify::verify_quote; + +pub mod verify; + +#[cfg(feature = "crl-download")] +pub mod collateral; diff --git a/tpm-qvl/src/verify.rs b/tpm-qvl/src/verify.rs new file mode 100644 index 00000000..9cd591eb --- /dev/null +++ b/tpm-qvl/src/verify.rs @@ -0,0 +1,670 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM Quote Verification Module + +use ::pem::parse_many; +use anyhow::{anyhow, bail, Context, Result}; +use dstack_types::Platform; +use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}; +use rsa::RsaPublicKey; +use sha2::{Digest, Sha256}; +use tracing::{debug, warn}; +use x509_parser::prelude::*; + +use rustls_pki_types::{CertificateDer, UnixTime}; +use webpki::{BorrowedCertRevocationList, CertRevocationList, EndEntityCert}; + +use tpm_types::{PcrValue, TpmEvent, TpmQuote}; + +use crate::{get_root_ca, QuoteCollateral, VerificationError, VerificationStatus}; + +#[derive(Clone)] +pub struct VerifiedReport { + pub attest: TpmAttest, + pub platform: Platform, + pub pcr_values: Vec, +} + +impl VerifiedReport { + pub fn get_pcr(&self, index: u32) -> Result> { + self.pcr_values + .iter() + .find(|p| p.index == index) + .map(|p| p.value.clone()) + .ok_or(anyhow!("PCR {} not found", index)) + } +} + +#[derive(Debug)] +enum PublicKey { + Rsa(RsaPublicKey), + Ecc(VerifyingKey), +} + +/// Verify quote with collateral and library-bundled root CA +pub fn verify_quote( + quote: &TpmQuote, + collateral: &QuoteCollateral, +) -> Result { + let ca = get_root_ca(quote.platform).map_err(|e| VerificationError { + status: VerificationStatus::default(), + error: e, + })?; + verify_quote_with_ca(quote, collateral, ca) +} + +/// Verify quote with collateral and user-provided root CA (recommended for security) +/// +/// The root CA is provided by the verifier as an independent trust anchor, +/// not derived from device-provided collateral. This prevents attacks where +/// a malicious device provides a fake certificate chain including a fake root CA. +pub fn verify_quote_with_ca( + quote: &TpmQuote, + collateral: &QuoteCollateral, + root_ca_pem: &str, +) -> Result { + let mut status = VerificationStatus::default(); + + let attest = match parse_tpm_attest("e.message) { + Ok(a) => a, + Err(e) => { + return Err(VerificationError { + status, + error: e.context("failed to parse TPMS_ATTEST"), + }); + } + }; + + let attested_pcr_indices: Vec = attest + .attested_quote_info + .pcr_selections + .iter() + .flat_map(|s| s.pcr_indices.iter().copied()) + .collect(); + let provided_pcr_indices: Vec = quote.pcr_values.iter().map(|p| p.index).collect(); + + if attested_pcr_indices != provided_pcr_indices { + return Err(VerificationError { + status, + error: anyhow!( + "PCR selection mismatch: TPMS_ATTEST has {:?}, but pcr_values has {:?}", + attested_pcr_indices, + provided_pcr_indices + ), + }); + } + + let computed_pcr_digest = + compute_pcr_digest("e.pcr_values).map_err(|e| VerificationError { + status: status.clone(), + error: e, + })?; + if attest.attested_quote_info.pcr_digest != computed_pcr_digest { + return Err(VerificationError { + status, + error: anyhow!("PCR digest mismatch"), + }); + } + + verify_event_log("e.pcr_values, "e.event_log).map_err(|e| VerificationError { + status: status.clone(), + error: e.context("event log verification failed"), + })?; + debug!("✓ Event Log replay verification successful"); + + status.pcr_verified = true; + + let ak_public_key = match extract_ak_public_key_from_cert("e.ak_cert) { + Ok(key) => { + debug!("extracted AK public key from certificate"); + key + } + Err(e) => { + return Err(VerificationError { + status, + error: e.context("failed to extract AK public key from certificate"), + }); + } + }; + + match verify_signature_with_key("e.message, "e.signature, &ak_public_key) { + Ok(true) => status.signature_verified = true, + Ok(false) => { + return Err(VerificationError { + status, + error: anyhow!("signature verification failed"), + }); + } + Err(e) => { + return Err(VerificationError { + status, + error: e.context("signature verification error"), + }); + } + } + + match verify_ak_chain_with_collateral("e.ak_cert, collateral, root_ca_pem) { + Ok(()) => {} + Err(e) => { + return Err(VerificationError { + status, + error: e.context("AK certificate chain verification error"), + }); + } + } + + Ok(VerifiedReport { + attest, + platform: quote.platform, + pcr_values: quote.pcr_values.clone(), + }) +} + +#[derive(Debug, Clone)] +pub struct TpmAttest { + pub magic: u32, + pub type_: u16, + pub qualified_signer: Vec, + pub qualified_data: Vec, + pub clock_info: ClockInfo, + pub firmware_version: u64, + pub attested_quote_info: QuoteInfo, +} + +#[derive(Debug, Clone)] +pub struct ClockInfo { + pub clock: u64, + pub reset_count: u32, + pub restart_count: u32, + pub safe: u8, +} + +/// PCR selection entry from TPM quote +#[derive(Debug, Clone)] +pub struct PcrSelection { + /// Hash algorithm (e.g., 0x000B for SHA-256) + pub hash_alg: u16, + /// Selected PCR indices + pub pcr_indices: Vec, +} + +#[derive(Debug, Clone)] +pub struct QuoteInfo { + /// PCR selections from the quote + pub pcr_selections: Vec, + /// PCR digest + pub pcr_digest: Vec, +} + +fn parse_tpm_attest(data: &[u8]) -> Result { + use nom::bytes::complete::take; + use nom::number::complete::{be_u16, be_u32, be_u64, be_u8}; + use nom::IResult; + + fn parse_sized_buffer(input: &[u8]) -> IResult<&[u8], Vec> { + let (input, size) = be_u16(input)?; + let (input, data) = take(size)(input)?; + Ok((input, data.to_vec())) + } + + fn parse_attest(input: &[u8]) -> IResult<&[u8], TpmAttest> { + let (input, magic) = be_u32(input)?; + let (input, type_) = be_u16(input)?; + let (input, qualified_signer) = parse_sized_buffer(input)?; + let (input, qualified_data) = parse_sized_buffer(input)?; + + let (input, clock) = be_u64(input)?; + let (input, reset_count) = be_u32(input)?; + let (input, restart_count) = be_u32(input)?; + let (input, safe) = be_u8(input)?; + + let (input, firmware_version) = be_u64(input)?; + + let (input, pcr_select_count) = be_u32(input)?; + + let mut pcr_selections = Vec::new(); + let mut current_input = input; + for _ in 0..pcr_select_count { + let (input, hash_alg) = be_u16(current_input)?; + let (input, sizeof_select) = be_u8(input)?; + let (input, pcr_bitmap) = take(sizeof_select)(input)?; + + // Parse PCR bitmap into indices + let mut pcr_indices = Vec::new(); + for (byte_idx, &byte) in pcr_bitmap.iter().enumerate() { + for bit_idx in 0..8 { + if (byte & (1 << bit_idx)) != 0 { + pcr_indices.push((byte_idx * 8 + bit_idx) as u32); + } + } + } + + pcr_selections.push(PcrSelection { + hash_alg, + pcr_indices, + }); + + current_input = input; + } + + let input = current_input; + let (input, pcr_digest) = parse_sized_buffer(input)?; + + Ok(( + input, + TpmAttest { + magic, + type_, + qualified_signer, + qualified_data, + clock_info: ClockInfo { + clock, + reset_count, + restart_count, + safe, + }, + firmware_version, + attested_quote_info: QuoteInfo { + pcr_selections, + pcr_digest, + }, + }, + )) + } + + let (_, attest) = parse_attest(data).map_err(|e| anyhow!("parse error: {e}"))?; + + if attest.magic != 0xff544347 { + bail!("invalid magic number: 0x{magic:08x}", magic = attest.magic); + } + + if attest.type_ != 0x8018 { + bail!("invalid attest type: 0x{type_:04x}", type_ = attest.type_); + } + + Ok(attest) +} + +fn compute_pcr_digest(pcr_values: &[PcrValue]) -> Result> { + let mut hasher = Sha256::new(); + for pcr in pcr_values { + hasher.update(&pcr.value); + } + Ok(hasher.finalize().to_vec()) +} + +fn verify_event_log(pcr_values: &[PcrValue], event_log: &[TpmEvent]) -> Result<()> { + for pcr in pcr_values { + let pcr_events: Vec<&TpmEvent> = event_log + .iter() + .filter(|e| e.pcr_index == pcr.index) + .collect(); + + if pcr_events.is_empty() { + continue; + } + + // Replay PCR extension to verify Event Log matches quote + let mut replayed_pcr = vec![0u8; 32]; + for event in &pcr_events { + let mut hasher = Sha256::new(); + hasher.update(&replayed_pcr); + hasher.update(&event.digest); + replayed_pcr = hasher.finalize().to_vec(); + } + + if replayed_pcr != pcr.value { + bail!( + "PCR {} replay mismatch: expected {}, got {}", + pcr.index, + hex::encode(&pcr.value), + hex::encode(&replayed_pcr) + ); + } + + debug!( + "✓ PCR {} replay verification successful ({} events)", + pcr.index, + pcr_events.len() + ); + + // For PCR 2: Extract Event 28 (UKI measurement) for image verification + // NOTE: Extracting the 3rd event (index 2) is GCP OVMF-specific behavior. + // On GCP, PCR 2 events are: [0]=EV_SEPARATOR, [1]=EV_EFI_GPT_EVENT, + // [2]=UKI (Event 28), [3]=Linux kernel (Event 41) + // Other platforms may have different event ordering. + if pcr.index == 2 && pcr_events.len() >= 3 { + let uki_digest = hex::encode(&pcr_events[2].digest); + debug!("Event 28 (UKI hash): {}", uki_digest); + debug!("To verify image: compare this against expected UKI Authenticode hash"); + } + } + + Ok(()) +} + +fn extract_ak_public_key_from_cert(ak_cert_der: &[u8]) -> Result { + let (_, cert) = + X509Certificate::from_der(ak_cert_der).context("failed to parse AK certificate")?; + + let spki = cert.public_key(); + + let algo_oid = &spki.algorithm.algorithm; + + const OID_RSA_ENCRYPTION: &[u64] = &[1, 2, 840, 113549, 1, 1, 1]; + const OID_EC_PUBLIC_KEY: &[u64] = &[1, 2, 840, 10045, 2, 1]; + + let oid_bytes: Vec = algo_oid + .iter() + .ok_or_else(|| anyhow::anyhow!("invalid OID"))? + .collect(); + + if oid_bytes == OID_RSA_ENCRYPTION { + use rsa::pkcs1::DecodeRsaPublicKey; + use rsa::traits::PublicKeyParts; + + let public_key = RsaPublicKey::from_pkcs1_der(spki.subject_public_key.data.as_ref()) + .context("failed to decode RSA public key from certificate")?; + + debug!( + "extracted RSA AK public key from certificate ({} bits)", + public_key.size() * 8 + ); + + Ok(PublicKey::Rsa(public_key)) + } else if oid_bytes == OID_EC_PUBLIC_KEY { + let public_key_bytes = spki.subject_public_key.data.as_ref(); + + let verifying_key = VerifyingKey::from_sec1_bytes(public_key_bytes) + .context("failed to decode ECC public key from certificate")?; + + debug!("extracted ECC P-256 AK public key from certificate"); + + Ok(PublicKey::Ecc(verifying_key)) + } else { + bail!("unsupported public key algorithm: {:?}", oid_bytes); + } +} + +fn verify_signature_with_key( + message: &[u8], + signature: &[u8], + public_key: &PublicKey, +) -> Result { + if signature.len() < 4 { + bail!("signature too short: {} bytes", signature.len()); + } + + let sig_alg = u16::from_be_bytes([signature[0], signature[1]]); + let hash_alg = u16::from_be_bytes([signature[2], signature[3]]); + + if hash_alg != 0x000B { + bail!("unsupported hash algorithm: 0x{hash_alg:04x}"); + } + + let actual_signature = &signature[4..]; + + debug!( + "message ({} bytes): {}", + message.len(), + hex::encode(message) + ); + debug!( + "signature ({} bytes): {}", + actual_signature.len(), + hex::encode(actual_signature) + ); + + let mut hasher = Sha256::new(); + hasher.update(message); + let message_hash = hasher.finalize(); + + debug!("message hash: {}", hex::encode(message_hash)); + + match public_key { + PublicKey::Rsa(rsa_key) => { + if sig_alg != 0x0014 { + bail!("expected RSASSA (0x0014), got 0x{sig_alg:04x}"); + } + + if actual_signature.len() < 2 { + bail!("RSA signature too short for size field"); + } + let rsa_sig_size = + u16::from_be_bytes([actual_signature[0], actual_signature[1]]) as usize; + if actual_signature.len() < 2 + rsa_sig_size { + bail!("RSA signature too short for signature data"); + } + let rsa_sig_data = &actual_signature[2..2 + rsa_sig_size]; + + debug!("RSA signature parsed: {rsa_sig_size} bytes"); + + let padding = rsa::Pkcs1v15Sign::new::(); + match rsa_key.verify(padding, &message_hash, rsa_sig_data) { + Ok(_) => { + debug!("✓ RSA signature verification successful"); + Ok(true) + } + Err(e) => { + warn!("RSA signature verification failed: {e}"); + Ok(false) + } + } + } + PublicKey::Ecc(ecc_key) => { + if sig_alg != 0x0018 { + bail!("expected ECDSA (0x0018), got 0x{sig_alg:04x}"); + } + + if actual_signature.len() < 2 { + bail!("ECDSA signature too short for signatureR size"); + } + let r_size = u16::from_be_bytes([actual_signature[0], actual_signature[1]]) as usize; + if actual_signature.len() < 2 + r_size { + bail!("ECDSA signature too short for signatureR data"); + } + let r_data = &actual_signature[2..2 + r_size]; + + let s_offset = 2 + r_size; + if actual_signature.len() < s_offset + 2 { + bail!("ECDSA signature too short for signatureS size"); + } + let s_size = + u16::from_be_bytes([actual_signature[s_offset], actual_signature[s_offset + 1]]) + as usize; + if actual_signature.len() < s_offset + 2 + s_size { + bail!("ECDSA signature too short for signatureS data"); + } + let s_data = &actual_signature[s_offset + 2..s_offset + 2 + s_size]; + + let mut sig_bytes = Vec::with_capacity(r_size + s_size); + sig_bytes.extend_from_slice(r_data); + sig_bytes.extend_from_slice(s_data); + + debug!("ECDSA signature parsed: r={r_size} bytes, s={s_size} bytes",); + + let signature = + Signature::from_slice(&sig_bytes).context("failed to parse ECDSA signature")?; + + match ecc_key.verify_prehash(&message_hash, &signature) { + Ok(_) => { + debug!("✓ ECC signature verification successful"); + Ok(true) + } + Err(e) => { + warn!("ECC signature verification failed: {e}"); + Ok(false) + } + } + } + } +} + +fn extract_certs_webpki(cert_pem: &[u8]) -> Result>> { + let pem_items = parse_many(cert_pem).context("failed to parse PEM")?; + + let certs = pem_items + .into_iter() + .map(|pem| CertificateDer::from(pem.into_contents())) + .collect(); + + Ok(certs) +} + +fn verify_ak_chain_with_collateral( + ak_cert_der: &[u8], + collateral: &QuoteCollateral, + root_ca_pem: &str, +) -> Result<()> { + debug!( + "verifying AK certificate chain with webpki ({} bytes leaf, {} intermediate CRLs, root CRL: {})", + ak_cert_der.len(), + collateral.crls.len(), + if collateral.root_ca_crl.is_some() { "yes" } else { "no" } + ); + + let ak_cert_der_owned = CertificateDer::from(ak_cert_der.to_vec()); + let ak_cert = + EndEntityCert::try_from(&ak_cert_der_owned).context("failed to parse AK certificate")?; + + // Load intermediate certs from device-provided collateral + let intermediate_certs = extract_certs_webpki(collateral.cert_chain_pem.as_bytes())?; + + debug!( + "loaded {} intermediate certificate(s) from collateral", + intermediate_certs.len() + ); + for (i, cert_der) in intermediate_certs.iter().enumerate() { + if let Ok((_, cert)) = X509Certificate::from_der(cert_der.as_ref()) { + debug!( + " intermediate[{i}]: subject={}, issuer={}", + cert.subject(), + cert.issuer() + ); + } + } + + // Load root CA from verifier-provided trust anchor (CRITICAL: independent from device) + let root_ca_certs = extract_certs_webpki(root_ca_pem.as_bytes())?; + if root_ca_certs.is_empty() { + bail!("failed to parse root CA PEM - no certificates found"); + } + let root_cert_der = &root_ca_certs[0]; + + if let Ok((_, cert)) = X509Certificate::from_der(root_cert_der.as_ref()) { + debug!( + "trust anchor (verifier-provided): subject={}, issuer={}", + cert.subject(), + cert.issuer() + ); + } + + let trust_anchor = webpki::anchor_from_trusted_cert(root_cert_der) + .context("failed to create trust anchor from verifier root CA")?; + + debug!( + "trust anchor created, {} intermediate(s)", + intermediate_certs.len() + ); + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .context("failed to get current time")?; + let time = UnixTime::since_unix_epoch(now); + + let trust_anchors = [trust_anchor]; + + // Check root CA against CRL if CRL was provided + if let Some(root_ca_crl) = &collateral.root_ca_crl { + debug!("checking root CA against its CRL (dcap-qvl-webpki)"); + let crl_refs = vec![root_ca_crl.as_slice()]; + dcap_qvl_webpki::check_single_cert_crl(root_cert_der.as_ref(), &crl_refs, time) + .context("root CA revoked or invalid CRL")?; + debug!("✓ root CA CRL check passed"); + } else { + debug!("root CA has no CRL - skipping root CA CRL check"); + } + + let result = if !collateral.crls.is_empty() { + debug!( + "parsing {} intermediate CRL(s) for revocation checking", + collateral.crls.len() + ); + let crls: Vec = collateral + .crls + .iter() + .enumerate() + .map(|(i, der)| { + BorrowedCertRevocationList::from_der(der) + .map(|crl| crl.into()) + .with_context(|| format!("failed to parse intermediate CRL #{i}")) + }) + .collect::>>()?; + let crl_refs: Vec<&CertRevocationList> = crls.iter().collect(); + + debug!("creating revocation options (CRL enforcement)"); + let revocation_builder = webpki::RevocationOptionsBuilder::new(&crl_refs) + .map_err(|_| anyhow::anyhow!("failed to create RevocationOptionsBuilder"))?; + + let revocation = revocation_builder + .with_depth(webpki::RevocationCheckDepth::Chain) + .with_status_policy(webpki::UnknownStatusPolicy::Allow) + .with_expiration_policy(webpki::ExpirationPolicy::Enforce) + .build(); + + debug!("verifying certificate chain with CRL revocation checking"); + + const TCG_KP_AIK_CERTIFICATE: &[u8] = &[0x67, 0x81, 0x05, 0x08, 0x01]; + let key_usage = webpki::KeyUsage::required_if_present(TCG_KP_AIK_CERTIFICATE); + + ak_cert + .verify_for_usage( + webpki::ALL_VERIFICATION_ALGS, + &trust_anchors, + &intermediate_certs, + time, + key_usage, + Some(revocation), + None, + ) + .context("certificate chain verification failed") + } else { + debug!("no CRLs available (no certificates have CRL Distribution Points)"); + debug!("verifying certificate chain WITHOUT CRL checking"); + + const TCG_KP_AIK_CERTIFICATE: &[u8] = &[0x67, 0x81, 0x05, 0x08, 0x01]; + let key_usage = webpki::KeyUsage::required_if_present(TCG_KP_AIK_CERTIFICATE); + + ak_cert + .verify_for_usage( + webpki::ALL_VERIFICATION_ALGS, + &trust_anchors, + &intermediate_certs, + time, + key_usage, + None, + None, + ) + .context("certificate chain verification failed") + }; + + match result { + Ok(_) => { + if collateral.crls.is_empty() { + debug!("✓ AK certificate chain verification successful (webpki, no CRLs)"); + } else { + debug!( + "✓ AK certificate chain verification successful (webpki + {} intermediate CRL(s))", + collateral.crls.len() + ); + } + Ok(()) + } + Err(e) => { + warn!("✗ AK certificate chain verification failed: {e:?}"); + Err(e) + } + } +} diff --git a/tpm-types/Cargo.toml b/tpm-types/Cargo.toml new file mode 100644 index 00000000..cb81483c --- /dev/null +++ b/tpm-types/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "tpm-types" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } +serde-human-bytes.workspace = true +dstack-types.workspace = true +scale = { workspace = true, features = ["derive"] } +cc-eventlog.workspace = true diff --git a/tpm-types/src/lib.rs b/tpm-types/src/lib.rs new file mode 100644 index 00000000..18dd23f2 --- /dev/null +++ b/tpm-types/src/lib.rs @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM Types - Common TPM-related type definitions +//! +//! This crate contains type definitions shared across TPM-related crates: +//! - tpm-attest (device side - generates quotes) +//! - tpm-qvl (verifier side - verifies quotes) +//! - ra-tls (uses TPM quotes in attestation) + +use dstack_types::Platform; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use serde_human_bytes as hex_bytes; + +/// TPM Quote structure containing attestation data +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] +pub struct TpmQuote { + /// TPMS_ATTEST message + #[serde(with = "hex_bytes")] + pub message: Vec, + + /// Quote signature + #[serde(with = "hex_bytes")] + pub signature: Vec, + + /// PCR values included in the quote + pub pcr_values: Vec, + + /// Attestation Key (AK) certificate (DER format) + #[serde(with = "hex_bytes")] + pub ak_cert: Vec, + + /// Platform where quote was generated + pub platform: Platform, + + /// Event Log (optional, used for PCR replay verification) + pub event_log: Vec, +} + +impl TpmQuote { + pub fn from_scale(mut input: &[u8]) -> Result { + Self::decode(&mut input) + } + + pub fn to_scale(&self) -> Vec { + self.encode() + } +} + +/// PCR (Platform Configuration Register) value +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] +pub struct PcrValue { + /// PCR index (0-23) + pub index: u32, + + /// Hash algorithm (e.g., "sha256", "sha384") + pub algorithm: String, + + /// PCR value (hash) + #[serde(with = "hex_bytes")] + pub value: Vec, +} + +/// PCR selection specifying which PCRs to include +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PcrSelection { + /// Hash bank (e.g., "sha256") + pub bank: String, + + /// List of PCR indices + pub pcrs: Vec, +} + +impl PcrSelection { + pub fn new(bank: &str, pcrs: &[u32]) -> Self { + Self { + bank: bank.to_string(), + pcrs: pcrs.to_vec(), + } + } + + pub fn sha256(pcrs: &[u32]) -> Self { + Self::new("sha256", pcrs) + } + + pub fn to_arg(&self) -> String { + let pcr_list: Vec = self.pcrs.iter().map(|p| p.to_string()).collect(); + format!( + "{}:{pcr_list_joined}", + self.bank, + pcr_list_joined = pcr_list.join(",") + ) + } +} + +impl Default for PcrSelection { + fn default() -> Self { + Self::sha256(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + } +} + +// Re-export TPM Event types from cc-eventlog +pub use cc_eventlog::tpm::{TpmEvent, TpmEventLog}; diff --git a/tpm2/Cargo.toml b/tpm2/Cargo.toml new file mode 100644 index 00000000..f51a0789 --- /dev/null +++ b/tpm2/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "tpm2" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +description = "Pure Rust TPM 2.0 implementation" +keywords = ["tpm", "tpm2", "security", "attestation"] +categories = ["cryptography", "hardware-support"] + +[dependencies] +anyhow.workspace = true +sha2 = { workspace = true, features = ["oid"] } +tracing.workspace = true + +[dev-dependencies] +tempfile.workspace = true + +[[bin]] +name = "tpm2-test" +path = "src/bin/tpm2-test.rs" + +[dependencies.hex] +workspace = true +features = ["alloc"] diff --git a/tpm2/src/bin/tpm2-test.rs b/tpm2/src/bin/tpm2-test.rs new file mode 100644 index 00000000..6afb2d82 --- /dev/null +++ b/tpm2/src/bin/tpm2-test.rs @@ -0,0 +1,952 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 Test CLI +//! +//! A simple CLI tool to test TPM 2.0 operations on real hardware. +//! +//! Usage: +//! tpm2-test [command] +//! +//! Commands: +//! info - Show TPM device info +//! random - Generate random bytes +//! pcr-read - Read PCR values +//! pcr-extend - Test PCR extend +//! nv-test - Test NV read/write operations +//! nv-full - Full NV test (define/write/read/undefine) +//! primary - Test primary key creation +//! evict - Test EvictControl (persistent key) +//! seal - Test seal/unseal operations (no PCR policy) +//! seal-pcr - Test seal/unseal operations with PCR policy +//! quote - Generate a TPM quote with RSA AK (requires GCP vTPM) +//! quote-ecc - Generate a TPM quote with ECC AK (requires GCP vTPM) +//! all - Run all tests + +use std::env; +use tpm2::{tpm_rh, ResponseBuffer, TpmAlgId, TpmContext, TpmlPcrSelection, TpmtPublic, Unmarshal}; + +fn main() { + let args: Vec = env::args().collect(); + let command = args.get(1).map(|s| s.as_str()).unwrap_or("all"); + + println!("=== TPM 2.0 Pure Rust Test Tool ===\n"); + + match command { + "info" => test_info(), + "random" => test_random(), + "pcr-read" => test_pcr_read(), + "pcr-extend" => test_pcr_extend(), + "nv-test" => test_nv_operations(), + "nv-full" => test_nv_full(), + "primary" => test_primary_key(), + "evict" => test_evict_control(), + "seal" => test_seal_unseal(), + "quote" => test_quote_rsa(), + "quote-ecc" => test_quote_ecc(), + "seal-pcr" => test_seal_unseal_with_pcr(), + "all" => { + test_info(); + test_random(); + test_pcr_read(); + test_primary_key(); + test_nv_operations(); + test_seal_unseal(); + test_seal_unseal_with_pcr(); + test_quote_rsa(); + test_quote_ecc(); + } + _ => { + eprintln!("Unknown command: {}", command); + eprintln!("Available commands: info, random, pcr-read, pcr-extend, nv-test, nv-full, primary, evict, seal, seal-pcr, seal-nv, quote, quote-ecc, all"); + std::process::exit(1); + } + } +} + +fn test_info() { + println!("--- Test: Device Info ---"); + + match TpmContext::new(None) { + Ok(ctx) => { + println!("✓ TPM device opened: {}", ctx.device_path()); + } + Err(e) => { + println!("✗ Failed to open TPM device: {}", e); + } + } + println!(); +} + +fn test_random() { + println!("--- Test: Random Number Generation ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Test getting 32 random bytes + match ctx.get_random(32) { + Ok(bytes) => { + println!("✓ Generated 32 random bytes:"); + println!(" {}", hex::encode(&bytes)); + } + Err(e) => { + println!("✗ GetRandom failed: {}", e); + } + } + + // Test getting 64 random bytes (tests chunking) + match ctx.get_random(64) { + Ok(bytes) => { + println!("✓ Generated 64 random bytes:"); + println!(" {}...", &hex::encode(&bytes)[..64]); + } + Err(e) => { + println!("✗ GetRandom (64 bytes) failed: {}", e); + } + } + println!(); +} + +fn test_pcr_read() { + println!("--- Test: PCR Read ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Read PCRs 0, 1, 2, 7 + let pcr_selection = TpmlPcrSelection::single(TpmAlgId::Sha256, &[0, 1, 2, 7]); + + match ctx.pcr_read(&pcr_selection) { + Ok(values) => { + println!("✓ Read {} PCR values:", values.len()); + for (idx, value) in values { + println!(" PCR[{}] = {}", idx, hex::encode(&value)); + } + } + Err(e) => { + println!("✗ PCR_Read failed: {}", e); + } + } + println!(); +} + +fn test_primary_key() { + println!("--- Test: Primary Key ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Check if a persistent handle exists + let test_handle: u32 = 0x81000100; + + match ctx.handle_exists(test_handle) { + Ok(exists) => { + println!(" Handle 0x{:08x} exists: {}", test_handle, exists); + } + Err(e) => { + println!("✗ ReadPublic failed: {}", e); + } + } + + // Try to create a transient primary key + println!(" Creating transient primary key under Owner hierarchy..."); + let template = tpm2::TpmtPublic::rsa_storage_key(); + match ctx.create_primary(tpm_rh::OWNER, &template) { + Ok((handle, public)) => { + println!("✓ Created primary key:"); + println!(" Handle: 0x{:08x}", handle); + println!(" Public size: {} bytes", public.len()); + + // Flush the transient handle + if let Err(e) = ctx.flush_context(handle) { + println!(" Warning: Failed to flush handle: {}", e); + } else { + println!(" Flushed transient handle"); + } + } + Err(e) => { + println!("✗ CreatePrimary failed: {}", e); + } + } + println!(); +} + +fn test_nv_operations() { + println!("--- Test: NV Operations ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Test NV index (use a test index in the owner range) + let test_nv_index: u32 = 0x01800100; + + // Check if NV index exists + match ctx.nv_exists(test_nv_index) { + Ok(exists) => { + println!(" NV index 0x{:08x} exists: {}", test_nv_index, exists); + + if exists { + // Try to read it + match ctx.nv_read(test_nv_index) { + Ok(Some(data)) => { + println!("✓ Read {} bytes from NV", data.len()); + if data.len() <= 64 { + println!(" Data: {}", hex::encode(&data)); + } + } + Ok(None) => { + println!(" NV index exists but couldn't read (auth required?)"); + } + Err(e) => { + println!("✗ NV_Read failed: {}", e); + } + } + } + } + Err(e) => { + println!("✗ NV_ReadPublic failed: {}", e); + } + } + + // Try to read GCP AK certificate (if on GCP) + let gcp_ak_cert_index: u32 = 0x01C10000; + println!( + "\n Checking GCP AK certificate at 0x{:08x}...", + gcp_ak_cert_index + ); + + match ctx.nv_exists(gcp_ak_cert_index) { + Ok(true) => { + println!(" GCP AK certificate NV index exists!"); + match ctx.nv_read(gcp_ak_cert_index) { + Ok(Some(data)) => { + println!("✓ Read GCP AK certificate: {} bytes", data.len()); + } + Ok(None) => { + println!(" Couldn't read certificate data"); + } + Err(e) => { + println!("✗ Failed to read certificate: {}", e); + } + } + } + Ok(false) => { + println!(" GCP AK certificate not found (not on GCP vTPM?)"); + } + Err(e) => { + println!("✗ NV check failed: {}", e); + } + } + println!(); +} + +fn test_quote_rsa() { + println!("--- Test: Quote Generation with RSA AK (GCP vTPM) ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Check if GCP AK template exists + let gcp_ak_template_index: u32 = 0x01C10001; // RSA AK template + + match ctx.nv_exists(gcp_ak_template_index) { + Ok(true) => { + println!(" GCP AK template found, attempting to load AK..."); + + // Read template + match ctx.nv_read(gcp_ak_template_index) { + Ok(Some(template)) => { + println!(" Read AK template: {} bytes", template.len()); + + // Create primary with template + match ctx.create_primary_from_template(tpm_rh::ENDORSEMENT, &template) { + Ok((handle, _public)) => { + println!("✓ Loaded GCP AK: handle 0x{:08x}", handle); + + // Generate quote + let qualifying_data = [0u8; 32]; // Test nonce + let pcr_selection = + TpmlPcrSelection::single(TpmAlgId::Sha256, &[0, 2, 14]); + + match ctx.quote(handle, &qualifying_data, &pcr_selection) { + Ok((quoted, signature)) => { + println!("✓ Generated quote:"); + println!(" Quoted size: {} bytes", quoted.len()); + println!(" Signature size: {} bytes", signature.len()); + + match verify_quote_pcr_digest(&mut ctx, "ed, &pcr_selection) + { + Ok(()) => println!( + "✓ Quote PCR digest matches current PCR values" + ), + Err(e) => println!( + "✗ Quote PCR digest verification failed: {}", + e + ), + } + } + Err(e) => { + println!("✗ Quote failed: {}", e); + } + } + + // Flush handle + let _ = ctx.flush_context(handle); + } + Err(e) => { + println!("✗ Failed to load AK: {}", e); + } + } + } + Ok(None) => { + println!(" Couldn't read AK template"); + } + Err(e) => { + println!("✗ Failed to read template: {}", e); + } + } + } + Ok(false) => { + println!(" GCP AK template not found (not on GCP vTPM)"); + println!(" Skipping quote test"); + } + Err(e) => { + println!("✗ NV check failed: {}", e); + } + } + println!(); +} + +fn test_quote_ecc() { + println!("--- Test: Quote Generation with ECC AK (GCP vTPM) ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Check if GCP ECC AK template exists + let gcp_ak_template_index: u32 = 0x01C10003; // ECC AK template + + match ctx.nv_exists(gcp_ak_template_index) { + Ok(true) => { + println!(" GCP ECC AK template found, attempting to load AK..."); + + // Read template + match ctx.nv_read(gcp_ak_template_index) { + Ok(Some(template)) => { + println!(" Read ECC AK template: {} bytes", template.len()); + + // Create primary with template + match ctx.create_primary_from_template(tpm_rh::ENDORSEMENT, &template) { + Ok((handle, _public)) => { + println!("✓ Loaded GCP ECC AK: handle 0x{:08x}", handle); + + // Generate quote + let qualifying_data = [0u8; 32]; // Test nonce + let pcr_selection = + TpmlPcrSelection::single(TpmAlgId::Sha256, &[0, 2, 14]); + + match ctx.quote(handle, &qualifying_data, &pcr_selection) { + Ok((quoted, signature)) => { + println!("✓ Generated ECC quote:"); + println!(" Quoted size: {} bytes", quoted.len()); + println!(" Signature size: {} bytes", signature.len()); + + match verify_quote_pcr_digest(&mut ctx, "ed, &pcr_selection) + { + Ok(()) => println!( + "✓ Quote PCR digest matches current PCR values" + ), + Err(e) => println!( + "✗ Quote PCR digest verification failed: {}", + e + ), + } + } + Err(e) => { + println!("✗ Quote failed: {}", e); + } + } + + // Flush handle + let _ = ctx.flush_context(handle); + } + Err(e) => { + println!("✗ Failed to load ECC AK: {}", e); + } + } + } + Ok(None) => { + println!(" Couldn't read ECC AK template"); + } + Err(e) => { + println!("✗ Failed to read template: {}", e); + } + } + } + Ok(false) => { + println!(" GCP ECC AK template not found (not on GCP vTPM)"); + println!(" Skipping ECC quote test"); + } + Err(e) => { + println!("✗ NV check failed: {}", e); + } + } + println!(); +} + +fn test_pcr_extend() { + println!("--- Test: PCR Extend ---"); + println!(" Note: This test extends PCR 23 which is typically resettable"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Read PCR 23 before extend + let pcr_selection = TpmlPcrSelection::single(TpmAlgId::Sha256, &[23]); + let before = match ctx.pcr_read(&pcr_selection) { + Ok(values) => { + if let Some((_, value)) = values.first() { + println!(" PCR[23] before: {}", hex::encode(value)); + value.clone() + } else { + println!("✗ No PCR value returned"); + return; + } + } + Err(e) => { + println!("✗ PCR_Read failed: {}", e); + return; + } + }; + + // Extend PCR 23 with test data + let test_hash = [0x42u8; 32]; // Test hash value + match ctx.pcr_extend(23, &test_hash, TpmAlgId::Sha256) { + Ok(()) => { + println!("✓ PCR_Extend succeeded"); + } + Err(e) => { + println!("✗ PCR_Extend failed: {}", e); + return; + } + } + + // Read PCR 23 after extend + match ctx.pcr_read(&pcr_selection) { + Ok(values) => { + if let Some((_, value)) = values.first() { + println!(" PCR[23] after: {}", hex::encode(value)); + if value != &before { + println!("✓ PCR value changed as expected"); + } else { + println!("✗ PCR value did not change!"); + } + } + } + Err(e) => { + println!("✗ PCR_Read after extend failed: {}", e); + } + } + println!(); +} + +fn test_nv_full() { + println!("--- Test: Full NV Operations (Define/Write/Read/Undefine) ---"); + println!(" Warning: This test creates and deletes NV index 0x01800200"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + let test_nv_index: u32 = 0x01800200; + let test_data = b"Hello TPM NV!"; + + // Clean up if index exists from previous failed test + if ctx.nv_exists(test_nv_index).unwrap_or(false) { + println!(" Cleaning up existing NV index..."); + let _ = ctx.nv_undefine(test_nv_index); + } + + // Define NV index + println!( + " Defining NV index 0x{:08x} with size {}...", + test_nv_index, + test_data.len() + ); + match ctx.nv_define(test_nv_index, test_data.len(), true) { + Ok(true) => { + println!("✓ NV_DefineSpace succeeded"); + } + Ok(false) => { + println!("✗ NV_DefineSpace returned false"); + return; + } + Err(e) => { + println!("✗ NV_DefineSpace failed: {}", e); + return; + } + } + + // Write to NV index + println!(" Writing {} bytes to NV...", test_data.len()); + match ctx.nv_write(test_nv_index, test_data) { + Ok(true) => { + println!("✓ NV_Write succeeded"); + } + Ok(false) => { + println!("✗ NV_Write returned false"); + } + Err(e) => { + println!("✗ NV_Write failed: {}", e); + } + } + + // Read from NV index + println!(" Reading from NV..."); + match ctx.nv_read(test_nv_index) { + Ok(Some(data)) => { + println!("✓ NV_Read succeeded: {} bytes", data.len()); + if data == test_data { + println!("✓ Data matches!"); + } else { + println!("✗ Data mismatch!"); + println!(" Expected: {:?}", String::from_utf8_lossy(test_data)); + println!(" Got: {:?}", String::from_utf8_lossy(&data)); + } + } + Ok(None) => { + println!("✗ NV_Read returned None"); + } + Err(e) => { + println!("✗ NV_Read failed: {}", e); + } + } + + // Undefine NV index + println!(" Undefining NV index..."); + match ctx.nv_undefine(test_nv_index) { + Ok(true) => { + println!("✓ NV_UndefineSpace succeeded"); + } + Ok(false) => { + println!("✗ NV_UndefineSpace returned false"); + } + Err(e) => { + println!("✗ NV_UndefineSpace failed: {}", e); + } + } + + // Verify it's gone + match ctx.nv_exists(test_nv_index) { + Ok(false) => { + println!("✓ NV index successfully removed"); + } + Ok(true) => { + println!("✗ NV index still exists after undefine!"); + } + Err(e) => { + println!("✗ NV check failed: {}", e); + } + } + println!(); +} + +fn test_evict_control() { + println!("--- Test: EvictControl (Persistent Key) ---"); + println!(" Warning: This test creates and removes persistent key at 0x81000200"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + let persistent_handle: u32 = 0x81000200; + + // Clean up if handle exists from previous failed test + if ctx.handle_exists(persistent_handle).unwrap_or(false) { + println!(" Cleaning up existing persistent handle..."); + // Need to evict it first - create a dummy transient and evict to remove + let _ = ctx.evict_control(persistent_handle, persistent_handle); + } + + // Create a transient primary key + println!(" Creating transient primary key..."); + let template = TpmtPublic::rsa_storage_key(); + let (transient_handle, _public) = match ctx.create_primary(tpm_rh::OWNER, &template) { + Ok(result) => { + println!("✓ Created transient key: 0x{:08x}", result.0); + result + } + Err(e) => { + println!("✗ CreatePrimary failed: {}", e); + return; + } + }; + + // Make it persistent + println!(" Making key persistent at 0x{:08x}...", persistent_handle); + match ctx.evict_control(transient_handle, persistent_handle) { + Ok(true) => { + println!("✓ EvictControl succeeded - key is now persistent"); + } + Ok(false) => { + println!("✗ EvictControl returned false"); + let _ = ctx.flush_context(transient_handle); + return; + } + Err(e) => { + println!("✗ EvictControl failed: {}", e); + let _ = ctx.flush_context(transient_handle); + return; + } + } + + // Flush the transient handle (no longer needed) + let _ = ctx.flush_context(transient_handle); + + // Verify persistent handle exists + match ctx.handle_exists(persistent_handle) { + Ok(true) => { + println!("✓ Persistent handle exists"); + } + Ok(false) => { + println!("✗ Persistent handle not found!"); + return; + } + Err(e) => { + println!("✗ Handle check failed: {}", e); + return; + } + } + + // Remove the persistent key + println!(" Removing persistent key..."); + match ctx.evict_control(persistent_handle, persistent_handle) { + Ok(true) => { + println!("✓ Persistent key removed"); + } + Ok(false) => { + println!("✗ EvictControl (remove) returned false"); + } + Err(e) => { + println!("✗ EvictControl (remove) failed: {}", e); + } + } + + // Verify it's gone + match ctx.handle_exists(persistent_handle) { + Ok(false) => { + println!("✓ Persistent handle successfully removed"); + } + Ok(true) => { + println!("✗ Persistent handle still exists!"); + } + Err(_) => { + // Expected - handle doesn't exist + println!("✓ Persistent handle successfully removed"); + } + } + println!(); +} + +fn test_seal_unseal() { + println!("--- Test: Seal/Unseal Operations ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // First, ensure we have a primary key + println!(" Creating primary storage key..."); + let template = TpmtPublic::rsa_storage_key(); + let (parent_handle, _) = match ctx.create_primary(tpm_rh::OWNER, &template) { + Ok(result) => { + println!("✓ Created parent key: 0x{:08x}", result.0); + result + } + Err(e) => { + println!("✗ CreatePrimary failed: {}", e); + return; + } + }; + + // Data to seal + let secret_data = b"This is my secret data for TPM sealing test!"; + println!(" Sealing {} bytes of data...", secret_data.len()); + + // Seal the data without PCR policy (simpler test) + let empty_pcr_selection = TpmlPcrSelection::default(); + let (pub_blob, priv_blob) = match ctx.seal( + secret_data, + parent_handle, + &empty_pcr_selection, + TpmAlgId::Sha256, + ) { + Ok(result) => { + println!("✓ Seal succeeded:"); + println!(" Public blob: {} bytes", result.0.len()); + println!(" Private blob: {} bytes", result.1.len()); + result + } + Err(e) => { + println!("✗ Seal failed: {}", e); + let _ = ctx.flush_context(parent_handle); + return; + } + }; + + // Unseal the data (use same empty PCR selection as seal) + println!(" Unsealing data..."); + match ctx.unseal( + &pub_blob, + &priv_blob, + parent_handle, + &empty_pcr_selection, + TpmAlgId::Sha256, + ) { + Ok(unsealed) => { + println!("✓ Unseal succeeded: {} bytes", unsealed.len()); + if unsealed == secret_data { + println!("✓ Data matches original!"); + println!(" Content: {:?}", String::from_utf8_lossy(&unsealed)); + } else { + println!("✗ Data mismatch!"); + println!(" Expected: {:?}", String::from_utf8_lossy(secret_data)); + println!(" Got: {:?}", String::from_utf8_lossy(&unsealed)); + } + } + Err(e) => { + println!("✗ Unseal failed: {}", e); + } + } + + // Clean up + let _ = ctx.flush_context(parent_handle); + println!(); +} + +fn parse_quote_attestation(quoted: &[u8]) -> anyhow::Result<(TpmlPcrSelection, Vec)> { + let mut buf = ResponseBuffer::new(quoted); + + let magic = buf.get_u32()?; + let attest_type = buf.get_u16()?; + if magic != 0xff544347 { + anyhow::bail!("unexpected TPMS_ATTEST.magic: 0x{:08x}", magic); + } + if attest_type != 0x8018 { + anyhow::bail!("unexpected TPMS_ATTEST.type: 0x{:04x}", attest_type); + } + let _qualified_signer = buf.get_tpm2b()?; + let _extra_data = buf.get_tpm2b()?; + + let _clock = buf.get_u64()?; + let _reset_count = buf.get_u32()?; + let _restart_count = buf.get_u32()?; + let _safe = buf.get_u8()?; + + let _firmware_version = buf.get_u64()?; + + let pcr_select = TpmlPcrSelection::unmarshal(&mut buf)?; + let pcr_digest = buf.get_tpm2b()?; + + Ok((pcr_select, pcr_digest)) +} + +fn verify_quote_pcr_digest( + ctx: &mut TpmContext, + quoted: &[u8], + requested_selection: &TpmlPcrSelection, +) -> anyhow::Result<()> { + use sha2::{Digest, Sha256, Sha384, Sha512}; + + let (attested_selection, attested_digest) = parse_quote_attestation(quoted)?; + + if attested_selection.pcr_selections.len() != requested_selection.pcr_selections.len() { + anyhow::bail!("quote returned unexpected PCR selection count"); + } + for (a, r) in attested_selection + .pcr_selections + .iter() + .zip(requested_selection.pcr_selections.iter()) + { + if a.hash.to_u16() != r.hash.to_u16() || a.pcr_select != r.pcr_select { + anyhow::bail!("quote returned PCR selection different from request"); + } + } + + if requested_selection.pcr_selections.len() != 1 { + anyhow::bail!("quote PCR verification only supports a single PCR bank selection"); + } + let hash_alg = requested_selection.pcr_selections[0].hash; + + let mut values = ctx.pcr_read(requested_selection)?; + values.sort_by_key(|(idx, _)| *idx); + let mut concat = Vec::new(); + for (_, v) in values { + concat.extend_from_slice(&v); + } + + let computed = match hash_alg { + TpmAlgId::Sha256 => Sha256::digest(&concat).to_vec(), + TpmAlgId::Sha384 => Sha384::digest(&concat).to_vec(), + TpmAlgId::Sha512 => Sha512::digest(&concat).to_vec(), + _ => anyhow::bail!("unsupported hash algorithm for quote PCR digest verification"), + }; + if computed != attested_digest { + anyhow::bail!("pcrDigest mismatch"); + } + + Ok(()) +} + +fn test_seal_unseal_with_pcr() { + println!("--- Test: Seal/Unseal Operations with PCR Policy ---"); + println!(" This seals data bound to PCR[23] (SHA256)"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + println!(" Creating primary storage key..."); + let template = TpmtPublic::rsa_storage_key(); + let (parent_handle, _) = match ctx.create_primary(tpm_rh::OWNER, &template) { + Ok(result) => { + println!("✓ Created parent key: 0x{:08x}", result.0); + result + } + Err(e) => { + println!("✗ CreatePrimary failed: {}", e); + return; + } + }; + + let secret_data = b"PCR protected secret data!"; + let pcr_selection = TpmlPcrSelection::single(TpmAlgId::Sha256, &[23]); + println!( + " Sealing {} bytes of data with PCR policy...", + secret_data.len() + ); + + let (pub_blob, priv_blob) = + match ctx.seal(secret_data, parent_handle, &pcr_selection, TpmAlgId::Sha256) { + Ok(result) => { + println!("✓ Seal succeeded with PCR policy"); + result + } + Err(e) => { + println!("✗ Seal failed: {}", e); + let _ = ctx.flush_context(parent_handle); + return; + } + }; + + println!(" Reading PCR values for verification..."); + match ctx.pcr_read(&pcr_selection) { + Ok(values) => { + for (idx, value) in values { + println!(" PCR[{}] = {}", idx, hex::encode(value)); + } + } + Err(e) => println!(" Warning: failed to read PCRs: {}", e), + } + + println!(" Attempting to unseal data (PCRs must match)..."); + let unsealed_ok = match ctx.unseal( + &pub_blob, + &priv_blob, + parent_handle, + &pcr_selection, + TpmAlgId::Sha256, + ) { + Ok(unsealed) => { + println!("✓ Unseal succeeded: {} bytes", unsealed.len()); + if unsealed == secret_data { + println!("✓ Data matches original!"); + println!(" Content: {:?}", String::from_utf8_lossy(&unsealed)); + } else { + println!("✗ Data mismatch!"); + } + true + } + Err(e) => { + println!("✗ Unseal failed (PCR mismatch?): {}", e); + false + } + }; + + if unsealed_ok { + println!(" Extending PCR 23 to ensure unseal fails in a different PCR environment..."); + let extend_value = [0xA5u8; 32]; + match ctx.pcr_extend(23, &extend_value, TpmAlgId::Sha256) { + Ok(()) => println!("✓ PCR_Extend succeeded"), + Err(e) => println!("✗ PCR_Extend failed: {}", e), + } + + println!(" Attempting to unseal again (must FAIL after PCR change)..."); + match ctx.unseal( + &pub_blob, + &priv_blob, + parent_handle, + &pcr_selection, + TpmAlgId::Sha256, + ) { + Ok(_) => println!("✗ Unseal unexpectedly succeeded after PCR change!"), + Err(_) => println!("✓ Unseal failed after PCR change (expected)"), + } + } + + let _ = ctx.flush_context(parent_handle); + println!(); +} diff --git a/tpm2/src/commands.rs b/tpm2/src/commands.rs new file mode 100644 index 00000000..4eb4b4e4 --- /dev/null +++ b/tpm2/src/commands.rs @@ -0,0 +1,648 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 command implementations +//! +//! This module provides high-level TPM operations. + +use anyhow::{Context, Result}; +use tracing::debug; + +use super::constants::*; +use super::device::*; +use super::marshal::*; +use super::session::*; +use super::types::*; + +/// Pure Rust TPM context +pub struct TpmContext { + device: TpmDevice, +} + +impl TpmContext { + /// Create a new TPM context with the given device path + pub fn new(tcti_path: Option<&str>) -> Result { + let device = match tcti_path { + Some(path) => TpmDevice::open(path)?, + None => TpmDevice::detect()?, + }; + + Ok(Self { device }) + } + + /// Get the device path + pub fn device_path(&self) -> &str { + self.device.path() + } + + // ==================== NV Operations ==================== + + /// Check if an NV index exists + pub fn nv_exists(&mut self, index: u32) -> Result { + let mut cmd = TpmCommand::new(TpmCc::NvReadPublic); + cmd.add_handle(index); + + let response = self.device.execute(&cmd.finalize())?; + + // If successful, the NV index exists + Ok(response.is_success()) + } + + /// Read NV public area to get size + pub fn nv_read_public(&mut self, index: u32) -> Result { + let mut cmd = TpmCommand::new(TpmCc::NvReadPublic); + cmd.add_handle(index); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("NV_ReadPublic failed")?; + + let mut buf = response.data_buffer(); + let nv_public = Tpm2bNvPublic::unmarshal(&mut buf)?; + + Ok(nv_public.nv_public) + } + + /// Read data from an NV index + pub fn nv_read(&mut self, index: u32) -> Result>> { + // First get the NV public to know the size + let nv_public = match self.nv_read_public(index) { + Ok(p) => p, + Err(_) => return Ok(None), // NV index doesn't exist + }; + + let total_size = nv_public.data_size as usize; + let mut result = Vec::with_capacity(total_size); + let mut offset = 0u16; + + // Read in chunks (max ~1024 bytes per read) + const MAX_READ_SIZE: u16 = 1024; + + while (offset as usize) < total_size { + let remaining = total_size - offset as usize; + let read_size = (remaining as u16).min(MAX_READ_SIZE); + + let mut cmd = TpmCommand::with_sessions(TpmCc::NvRead); + // authHandle (owner for owner-readable NV) + cmd.add_handle(tpm_rh::OWNER); + // nvIndex + cmd.add_handle(index); + // Authorization area (null auth) + cmd.add_null_auth_area(); + // size + cmd.add_u16(read_size); + // offset + cmd.add_u16(offset); + + let response = self.device.execute(&cmd.finalize())?; + if !response.is_success() { + // Try with NV index as auth handle instead + let mut cmd = TpmCommand::with_sessions(TpmCc::NvRead); + cmd.add_handle(index); + cmd.add_handle(index); + cmd.add_null_auth_area(); + cmd.add_u16(read_size); + cmd.add_u16(offset); + + let response = self.device.execute(&cmd.finalize())?; + if !response.is_success() { + return Ok(None); + } + + let mut buf = response.skip_parameter_size()?; + let data = buf.get_tpm2b()?; + result.extend_from_slice(&data); + } else { + let mut buf = response.skip_parameter_size()?; + let data = buf.get_tpm2b()?; + result.extend_from_slice(&data); + } + + offset += read_size; + } + + Ok(Some(result)) + } + + /// Write data to an NV index + pub fn nv_write(&mut self, index: u32, data: &[u8]) -> Result { + const MAX_WRITE_SIZE: usize = 1024; + let mut offset = 0u16; + + while (offset as usize) < data.len() { + let remaining = data.len() - offset as usize; + let write_size = remaining.min(MAX_WRITE_SIZE); + let chunk = &data[offset as usize..offset as usize + write_size]; + + let mut cmd = TpmCommand::with_sessions(TpmCc::NvWrite); + // authHandle + cmd.add_handle(tpm_rh::OWNER); + // nvIndex + cmd.add_handle(index); + // Authorization area + cmd.add_null_auth_area(); + // data + cmd.add_tpm2b(chunk); + // offset + cmd.add_u16(offset); + + let response = self.device.execute(&cmd.finalize())?; + response + .ensure_success() + .with_context(|| format!("NV_Write failed at offset {}", offset))?; + + offset += write_size as u16; + } + + debug!("wrote {} bytes to NV index 0x{:08x}", data.len(), index); + Ok(true) + } + + /// Define a new NV index + pub fn nv_define(&mut self, index: u32, size: usize, owner_read_write: bool) -> Result { + let mut attributes = TpmaNv::new(); + if owner_read_write { + attributes = attributes.with_owner_write().with_owner_read(); + } + + let nv_public = TpmsNvPublic::new(index, size as u16, attributes); + + let mut cmd = TpmCommand::with_sessions(TpmCc::NvDefineSpace); + // authHandle (owner) + cmd.add_handle(tpm_rh::OWNER); + // Authorization area + cmd.add_null_auth_area(); + // auth (empty) + cmd.add_tpm2b_empty(); + // publicInfo + cmd.add(&Tpm2bNvPublic { nv_public }); + + let response = self.device.execute(&cmd.finalize())?; + + if response.is_success() { + debug!("defined NV index 0x{:08x} with size {}", index, size); + Ok(true) + } else { + Ok(false) + } + } + + /// Undefine (delete) an NV index + pub fn nv_undefine(&mut self, index: u32) -> Result { + let mut cmd = TpmCommand::with_sessions(TpmCc::NvUndefineSpace); + // authHandle (owner) + cmd.add_handle(tpm_rh::OWNER); + // nvIndex + cmd.add_handle(index); + // Authorization area + cmd.add_null_auth_area(); + + let response = self.device.execute(&cmd.finalize())?; + + if response.is_success() { + debug!("undefined NV index 0x{:08x}", index); + Ok(true) + } else { + Ok(false) + } + } + + // ==================== PCR Operations ==================== + + /// Read PCR values for the given selection + pub fn pcr_read(&mut self, pcr_selection: &TpmlPcrSelection) -> Result)>> { + let mut cmd = TpmCommand::new(TpmCc::PcrRead); + cmd.add(pcr_selection); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("PCR_Read failed")?; + + let mut buf = response.data_buffer(); + let _update_counter = buf.get_u32()?; + let pcr_selection_out = TpmlPcrSelection::unmarshal(&mut buf)?; + let digest_list = TpmlDigest::unmarshal(&mut buf)?; + + // Map digests to PCR indices + let mut result = Vec::new(); + let mut digest_idx = 0; + + for sel in &pcr_selection_out.pcr_selections { + for (byte_idx, &byte) in sel.pcr_select.iter().enumerate() { + for bit in 0..8 { + if byte & (1 << bit) != 0 { + let pcr_idx = (byte_idx * 8 + bit) as u32; + if digest_idx < digest_list.digests.len() { + result.push((pcr_idx, digest_list.digests[digest_idx].buffer.clone())); + digest_idx += 1; + } + } + } + } + } + + Ok(result) + } + + /// Read a single PCR value + pub fn pcr_read_single(&mut self, pcr_idx: u32, hash_alg: TpmAlgId) -> Result> { + let selection = TpmlPcrSelection::single(hash_alg, &[pcr_idx]); + let values = self.pcr_read(&selection)?; + + values + .into_iter() + .find(|(idx, _)| *idx == pcr_idx) + .map(|(_, v)| v) + .ok_or_else(|| anyhow::anyhow!("PCR {} not found in response", pcr_idx)) + } + + /// Extend a PCR with a hash value + pub fn pcr_extend(&mut self, pcr: u32, hash: &[u8], hash_alg: TpmAlgId) -> Result<()> { + let digest_values = TpmlDigestValues::single(TpmtHa { + hash_alg, + digest: hash.to_vec(), + }); + + let mut cmd = TpmCommand::with_sessions(TpmCc::PcrExtend); + // pcrHandle + cmd.add_handle(pcr); + // Authorization area + cmd.add_null_auth_area(); + // digests + cmd.add(&digest_values); + + let response = self.device.execute(&cmd.finalize())?; + response + .ensure_success() + .with_context(|| format!("PCR_Extend failed for PCR {}", pcr))?; + + debug!("extended PCR {}", pcr); + Ok(()) + } + + // ==================== Random Number Generation ==================== + + /// Generate random bytes using the TPM's hardware RNG + pub fn get_random(&mut self, num_bytes: usize) -> Result> { + let mut result = Vec::with_capacity(num_bytes); + + // TPM may return fewer bytes than requested, so loop + while result.len() < num_bytes { + let remaining = num_bytes - result.len(); + let request_size = remaining.min(48) as u16; // TPM typically limits to 48-64 bytes + + let mut cmd = TpmCommand::new(TpmCc::GetRandom); + cmd.add_u16(request_size); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("GetRandom failed")?; + + let mut buf = response.data_buffer(); + let random_bytes = buf.get_tpm2b()?; + result.extend_from_slice(&random_bytes); + } + + result.truncate(num_bytes); + Ok(result) + } + + /// Generate random bytes into a fixed-size array + pub fn get_random_array(&mut self) -> Result<[u8; N]> { + let bytes = self.get_random(N)?; + bytes + .try_into() + .map_err(|_| anyhow::anyhow!("unexpected random bytes length")) + } + + // ==================== Primary Key Operations ==================== + + /// Check if a persistent handle exists + pub fn handle_exists(&mut self, handle: u32) -> Result { + let mut cmd = TpmCommand::new(TpmCc::ReadPublic); + cmd.add_handle(handle); + + let response = self.device.execute(&cmd.finalize())?; + Ok(response.is_success()) + } + + /// Create a primary key in the specified hierarchy + pub fn create_primary( + &mut self, + hierarchy: u32, + template: &TpmtPublic, + ) -> Result<(u32, Vec)> { + let public = Tpm2bPublic::from_template(template); + + let mut cmd = TpmCommand::with_sessions(TpmCc::CreatePrimary); + // primaryHandle (hierarchy) + cmd.add_handle(hierarchy); + // Authorization area + cmd.add_null_auth_area(); + // inSensitive (empty) + cmd.add(&Tpm2bSensitiveCreate::empty()); + // inPublic + cmd.add(&public); + // outsideInfo (empty) + cmd.add_tpm2b_empty(); + // creationPCR (empty) + cmd.add(&TpmlPcrSelection::default()); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("CreatePrimary failed")?; + + // For commands with sessions, the response format is: + // - Handle (4 bytes) - BEFORE parameter size + // - Parameter size (4 bytes) + // - Parameters... + let mut buf = response.data_buffer(); + let handle = buf.get_u32()?; + + // Skip parameter size + let _param_size = buf.get_u32()?; + + let out_public = Tpm2bPublic::unmarshal(&mut buf)?; + + debug!("created primary key with handle 0x{:08x}", handle); + Ok((handle, out_public.public_area)) + } + + /// Create a primary key from raw public template bytes (for GCP AK) + pub fn create_primary_from_template( + &mut self, + hierarchy: u32, + template_bytes: &[u8], + ) -> Result<(u32, Vec)> { + let mut cmd = TpmCommand::with_sessions(TpmCc::CreatePrimary); + // primaryHandle (hierarchy) + cmd.add_handle(hierarchy); + // Authorization area + cmd.add_null_auth_area(); + // inSensitive (empty) + cmd.add(&Tpm2bSensitiveCreate::empty()); + // inPublic (raw template with size prefix) + cmd.add_tpm2b(template_bytes); + // outsideInfo (empty) + cmd.add_tpm2b_empty(); + // creationPCR (empty) + cmd.add(&TpmlPcrSelection::default()); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("CreatePrimary failed")?; + + // For commands with sessions, the response format is: + // - Handle (4 bytes) - BEFORE parameter size + // - Parameter size (4 bytes) + // - Parameters... + let mut buf = response.data_buffer(); + let handle = buf.get_u32()?; + + // Skip parameter size + let _param_size = buf.get_u32()?; + + let out_public = Tpm2bPublic::unmarshal(&mut buf)?; + + debug!("created primary key with handle 0x{:08x}", handle); + Ok((handle, out_public.public_area)) + } + + /// Make a key persistent at a given handle + pub fn evict_control(&mut self, object_handle: u32, persistent_handle: u32) -> Result { + let mut cmd = TpmCommand::with_sessions(TpmCc::EvictControl); + // auth (owner) + cmd.add_handle(tpm_rh::OWNER); + // objectHandle + cmd.add_handle(object_handle); + // Authorization area + cmd.add_null_auth_area(); + // persistentHandle + cmd.add_handle(persistent_handle); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("EvictControl failed")?; + + debug!("made key persistent at 0x{:08x}", persistent_handle); + Ok(true) + } + + /// Ensure a persistent primary key exists at the given handle + pub fn ensure_primary_key(&mut self, handle: u32) -> Result { + if self.handle_exists(handle)? { + return Ok(true); + } + + debug!("creating TPM primary key at 0x{:08x}...", handle); + let template = TpmtPublic::rsa_storage_key(); + let (transient, _) = self.create_primary(tpm_rh::OWNER, &template)?; + self.evict_control(transient, handle)?; + + // Flush the transient handle + self.flush_context(transient)?; + + Ok(true) + } + + /// Flush a context (handle) + pub fn flush_context(&mut self, handle: u32) -> Result<()> { + let mut cmd = TpmCommand::new(TpmCc::FlushContext); + cmd.add_handle(handle); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("FlushContext failed")?; + + Ok(()) + } + + // ==================== Seal/Unseal Operations ==================== + + /// Seal data to TPM with PCR policy + pub fn seal( + &mut self, + data: &[u8], + parent_handle: u32, + pcr_selection: &TpmlPcrSelection, + hash_alg: TpmAlgId, + ) -> Result<(Vec, Vec)> { + // Compute policy digest if PCR selection is not empty + let policy_digest = if pcr_selection.pcr_selections.is_empty() { + // No PCR policy - use empty authPolicy (zero length, not zero-filled) + vec![] + } else { + // First, compute the policy digest using a trial session + let trial_session = AuthSession::start_trial(&mut self.device, hash_alg)?; + + // Compute PCR digest + let pcr_digest = compute_pcr_digest(&mut self.device, pcr_selection, hash_alg)?; + + // Apply PCR policy to trial session + trial_session.policy_pcr(&mut self.device, &pcr_digest, pcr_selection)?; + + // Get the policy digest + let digest = trial_session.get_digest(&mut self.device)?; + + // Flush trial session + trial_session.flush(&mut self.device)?; + + digest + }; + + // Create sealed object template + let template = TpmtPublic::sealed_object(Tpm2bDigest::new(policy_digest)); + let public = Tpm2bPublic::from_template(&template); + + // Create the sealed object + let mut cmd = TpmCommand::with_sessions(TpmCc::Create); + // parentHandle + cmd.add_handle(parent_handle); + // Authorization area + cmd.add_null_auth_area(); + // inSensitive (contains the data to seal) + cmd.add(&Tpm2bSensitiveCreate::with_data(data.to_vec())); + // inPublic + cmd.add(&public); + // outsideInfo (empty) + cmd.add_tpm2b_empty(); + // creationPCR (empty) + cmd.add(&TpmlPcrSelection::default()); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("Create (seal) failed")?; + + let mut buf = response.skip_parameter_size()?; + let out_private = Tpm2bPrivate::unmarshal(&mut buf)?; + let out_public = Tpm2bPublic::unmarshal(&mut buf)?; + + debug!("sealed {} bytes to TPM with PCR policy", data.len()); + + Ok((out_public.public_area, out_private.buffer)) + } + + /// Unseal data from TPM with PCR policy + pub fn unseal( + &mut self, + pub_bytes: &[u8], + priv_bytes: &[u8], + parent_handle: u32, + pcr_selection: &TpmlPcrSelection, + hash_alg: TpmAlgId, + ) -> Result> { + // Load the sealed object + let mut cmd = TpmCommand::with_sessions(TpmCc::Load); + // parentHandle + cmd.add_handle(parent_handle); + // Authorization area + cmd.add_null_auth_area(); + // inPrivate + cmd.add_tpm2b(priv_bytes); + // inPublic + cmd.add_tpm2b(pub_bytes); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("Load failed")?; + + // For commands with sessions, handle comes BEFORE parameter size + let mut buf = response.data_buffer(); + let object_handle = buf.get_u32()?; + let _param_size = buf.get_u32()?; // Skip parameter size + + debug!("loaded sealed object with handle 0x{:08x}", object_handle); + + // Unseal - use policy session if PCR selection is not empty + let response = if pcr_selection.pcr_selections.is_empty() { + // No PCR policy - use null auth + let mut cmd = TpmCommand::with_sessions(TpmCc::Unseal); + cmd.add_handle(object_handle); + cmd.add_null_auth_area(); + self.device.execute(&cmd.finalize())? + } else { + // Start a policy session + let policy_session = AuthSession::start_policy(&mut self.device, hash_alg)?; + + // Compute and apply PCR policy + let pcr_digest = compute_pcr_digest(&mut self.device, pcr_selection, hash_alg)?; + policy_session.policy_pcr(&mut self.device, &pcr_digest, pcr_selection)?; + + // Unseal with policy session + let mut cmd = TpmCommand::with_sessions(TpmCc::Unseal); + cmd.add_handle(object_handle); + cmd.add_policy_auth(policy_session.handle); + + let response = self.device.execute(&cmd.finalize())?; + let _ = policy_session.flush(&mut self.device); + response + }; + + // Clean up object handle + let _ = self.flush_context(object_handle); + + if !response.is_success() { + anyhow::bail!( + "Unseal failed with TPM error: 0x{:08x}", + response.response_code + ); + } + + let mut buf = response.skip_parameter_size()?; + let data = buf.get_tpm2b()?; + + debug!("unsealed {} bytes from TPM", data.len()); + Ok(data) + } + + // ==================== Quote Operations ==================== + + /// Generate a TPM quote + pub fn quote( + &mut self, + sign_handle: u32, + qualifying_data: &[u8], + pcr_selection: &TpmlPcrSelection, + ) -> Result<(Vec, Vec)> { + let mut cmd = TpmCommand::with_sessions(TpmCc::Quote); + // signHandle + cmd.add_handle(sign_handle); + // Authorization area + cmd.add_null_auth_area(); + // qualifyingData + cmd.add_tpm2b(qualifying_data); + // inScheme (NULL - use key's default scheme) + cmd.add(&TpmtSigScheme::null()); + // PCRselect + cmd.add(pcr_selection); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("Quote failed")?; + + let mut buf = response.skip_parameter_size()?; + let quoted = buf.get_tpm2b()?; // TPM2B_ATTEST + let signature = buf.get_remaining(); // TPMT_SIGNATURE + + debug!("generated TPM quote"); + Ok((quoted, signature)) + } + + /// Read public area of a key + pub fn read_public(&mut self, handle: u32) -> Result> { + let mut cmd = TpmCommand::new(TpmCc::ReadPublic); + cmd.add_handle(handle); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("ReadPublic failed")?; + + let mut buf = response.data_buffer(); + let out_public = Tpm2bPublic::unmarshal(&mut buf)?; + + Ok(out_public.public_area) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pcr_selection() { + let sel = TpmsPcrSelection::sha256(&[0, 1, 2, 7]); + assert_eq!(sel.hash, TpmAlgId::Sha256); + // PCR 0, 1, 2, 7 = bits 0, 1, 2, 7 = 0b10000111 = 0x87 + assert_eq!(sel.pcr_select[0], 0x87); + } +} diff --git a/tpm2/src/constants.rs b/tpm2/src/constants.rs new file mode 100644 index 00000000..c562fc01 --- /dev/null +++ b/tpm2/src/constants.rs @@ -0,0 +1,380 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 constants and command codes + +/// TPM 2.0 Command Codes (TPM_CC) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum TpmCc { + NvDefineSpace = 0x0000012A, + NvUndefineSpace = 0x00000122, + NvRead = 0x0000014E, + NvWrite = 0x00000137, + NvReadPublic = 0x00000169, + PcrRead = 0x0000017E, + PcrExtend = 0x00000182, + GetRandom = 0x0000017B, + CreatePrimary = 0x00000131, + Create = 0x00000153, + Load = 0x00000157, + Unseal = 0x0000015E, + Quote = 0x00000158, + StartAuthSession = 0x00000176, + PolicyPcr = 0x0000017F, + PolicyGetDigest = 0x00000189, + FlushContext = 0x00000165, + EvictControl = 0x00000120, + ReadPublic = 0x00000173, + GetCapability = 0x0000017A, +} + +impl TpmCc { + pub fn to_u32(self) -> u32 { + self as u32 + } +} + +/// TPM 2.0 Response Codes (TPM_RC) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum TpmRc { + Success = 0x00000000, + // Format 0 errors + Initialize = 0x00000100, + Failure = 0x00000101, + // Format 1 errors (parameter errors) + Value = 0x00000184, + Handle = 0x0000008B, + // NV errors + NvDefined = 0x0000014C, + NvNotDefined = 0x0000014B, + NvLocked = 0x00000148, + NvRange = 0x00000146, + // Auth errors + AuthFail = 0x0000098E, + PolicyFail = 0x0000099D, + // PCR errors + Locality = 0x00000107, +} + +impl TpmRc { + pub fn from_u32(code: u32) -> Self { + match code { + 0x00000000 => TpmRc::Success, + 0x00000100 => TpmRc::Initialize, + 0x00000101 => TpmRc::Failure, + 0x00000184 => TpmRc::Value, + 0x0000008B => TpmRc::Handle, + 0x0000014C => TpmRc::NvDefined, + 0x0000014B => TpmRc::NvNotDefined, + 0x00000148 => TpmRc::NvLocked, + 0x00000146 => TpmRc::NvRange, + 0x0000098E => TpmRc::AuthFail, + 0x0000099D => TpmRc::PolicyFail, + 0x00000107 => TpmRc::Locality, + _ => TpmRc::Failure, // Unknown error + } + } + + pub fn is_success(self) -> bool { + matches!(self, TpmRc::Success) + } +} + +/// TPM 2.0 Algorithm IDs (TPM_ALG_ID) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum TpmAlgId { + Null = 0x0010, + Sha1 = 0x0004, + Sha256 = 0x000B, + Sha384 = 0x000C, + Sha512 = 0x000D, + Rsa = 0x0001, + Ecc = 0x0023, + Aes = 0x0006, + Cfb = 0x0043, + RsaSsa = 0x0014, + RsaPss = 0x0016, + EcDsa = 0x0018, + KeyedHash = 0x0008, + SymCipher = 0x0025, +} + +impl TpmAlgId { + pub fn to_u16(self) -> u16 { + self as u16 + } + + pub fn from_u16(v: u16) -> Option { + match v { + 0x0010 => Some(TpmAlgId::Null), + 0x0004 => Some(TpmAlgId::Sha1), + 0x000B => Some(TpmAlgId::Sha256), + 0x000C => Some(TpmAlgId::Sha384), + 0x000D => Some(TpmAlgId::Sha512), + 0x0001 => Some(TpmAlgId::Rsa), + 0x0023 => Some(TpmAlgId::Ecc), + 0x0006 => Some(TpmAlgId::Aes), + 0x0043 => Some(TpmAlgId::Cfb), + 0x0014 => Some(TpmAlgId::RsaSsa), + 0x0016 => Some(TpmAlgId::RsaPss), + 0x0018 => Some(TpmAlgId::EcDsa), + 0x0008 => Some(TpmAlgId::KeyedHash), + 0x0025 => Some(TpmAlgId::SymCipher), + _ => None, + } + } + + pub fn digest_size(self) -> usize { + match self { + TpmAlgId::Sha1 => 20, + TpmAlgId::Sha256 => 32, + TpmAlgId::Sha384 => 48, + TpmAlgId::Sha512 => 64, + _ => 0, + } + } +} + +/// TPM 2.0 Handle Types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum TpmHt { + Pcr = 0x00, + NvIndex = 0x01, + HmacSession = 0x02, + PolicySession = 0x03, + Permanent = 0x40, + Transient = 0x80, + Persistent = 0x81, +} + +/// TPM 2.0 Permanent Handles +pub mod tpm_rh { + pub const OWNER: u32 = 0x40000001; + pub const NULL: u32 = 0x40000007; + pub const ENDORSEMENT: u32 = 0x4000000B; + pub const PLATFORM: u32 = 0x4000000C; + pub const PW: u32 = 0x40000009; // Password authorization +} + +/// TPM 2.0 Session Types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum TpmSe { + Hmac = 0x00, + Policy = 0x01, + Trial = 0x03, +} + +/// TPM 2.0 Startup Types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum TpmSu { + Clear = 0x0000, + State = 0x0001, +} + +/// TPM 2.0 Capability Types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum TpmCap { + Handles = 0x00000001, + Commands = 0x00000002, + PpCommands = 0x00000003, + AuditCommands = 0x00000004, + Pcrs = 0x00000005, + TpmProperties = 0x00000006, + PcrProperties = 0x00000007, + EccCurves = 0x00000008, + AuthPolicies = 0x00000009, +} + +/// TPM 2.0 Object Attributes +#[derive(Debug, Clone, Copy, Default)] +pub struct TpmaObject(pub u32); + +impl TpmaObject { + pub const FIXED_TPM: u32 = 1 << 1; + pub const ST_CLEAR: u32 = 1 << 2; + pub const FIXED_PARENT: u32 = 1 << 4; + pub const SENSITIVE_DATA_ORIGIN: u32 = 1 << 5; + pub const USER_WITH_AUTH: u32 = 1 << 6; + pub const ADMIN_WITH_POLICY: u32 = 1 << 7; + pub const NO_DA: u32 = 1 << 10; + pub const ENCRYPTED_DUPLICATION: u32 = 1 << 11; + pub const RESTRICTED: u32 = 1 << 16; + pub const DECRYPT: u32 = 1 << 17; + pub const SIGN_ENCRYPT: u32 = 1 << 18; + + pub fn new() -> Self { + Self(0) + } + + pub fn with_fixed_tpm(mut self) -> Self { + self.0 |= Self::FIXED_TPM; + self + } + + pub fn with_fixed_parent(mut self) -> Self { + self.0 |= Self::FIXED_PARENT; + self + } + + pub fn with_sensitive_data_origin(mut self) -> Self { + self.0 |= Self::SENSITIVE_DATA_ORIGIN; + self + } + + pub fn with_user_with_auth(mut self) -> Self { + self.0 |= Self::USER_WITH_AUTH; + self + } + + pub fn with_admin_with_policy(mut self) -> Self { + self.0 |= Self::ADMIN_WITH_POLICY; + self + } + + pub fn with_restricted(mut self) -> Self { + self.0 |= Self::RESTRICTED; + self + } + + pub fn with_decrypt(mut self) -> Self { + self.0 |= Self::DECRYPT; + self + } + + pub fn with_sign_encrypt(mut self) -> Self { + self.0 |= Self::SIGN_ENCRYPT; + self + } +} + +/// TPM 2.0 NV Attributes +#[derive(Debug, Clone, Copy, Default)] +pub struct TpmaNv(pub u32); + +impl TpmaNv { + pub const PP_WRITE: u32 = 1 << 0; + pub const OWNER_WRITE: u32 = 1 << 1; + pub const AUTH_WRITE: u32 = 1 << 2; + pub const POLICY_WRITE: u32 = 1 << 3; + pub const PP_READ: u32 = 1 << 16; + pub const OWNER_READ: u32 = 1 << 17; + pub const AUTH_READ: u32 = 1 << 18; + pub const POLICY_READ: u32 = 1 << 19; + pub const NO_DA: u32 = 1 << 25; + pub const ORDERLY: u32 = 1 << 26; + pub const CLEAR_STCLEAR: u32 = 1 << 27; + pub const READ_LOCKED: u32 = 1 << 28; + pub const WRITTEN: u32 = 1 << 29; + pub const PLATFORM_CREATE: u32 = 1 << 30; + pub const READ_STCLEAR: u32 = 1 << 31; + + pub fn new() -> Self { + Self(0) + } + + pub fn with_owner_write(mut self) -> Self { + self.0 |= Self::OWNER_WRITE; + self + } + + pub fn with_owner_read(mut self) -> Self { + self.0 |= Self::OWNER_READ; + self + } + + pub fn with_auth_write(mut self) -> Self { + self.0 |= Self::AUTH_WRITE; + self + } + + pub fn with_auth_read(mut self) -> Self { + self.0 |= Self::AUTH_READ; + self + } +} + +/// TPM 2.0 Session Attributes +#[derive(Debug, Clone, Copy, Default)] +pub struct TpmaSa(pub u8); + +impl TpmaSa { + pub const CONTINUE_SESSION: u8 = 1 << 0; + pub const AUDIT_EXCLUSIVE: u8 = 1 << 1; + pub const AUDIT_RESET: u8 = 1 << 2; + pub const DECRYPT: u8 = 1 << 5; + pub const ENCRYPT: u8 = 1 << 6; + pub const AUDIT: u8 = 1 << 7; + + pub fn new() -> Self { + Self(0) + } + + pub fn with_continue_session(mut self) -> Self { + self.0 |= Self::CONTINUE_SESSION; + self + } +} + +/// TPM command header tag +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum TpmSt { + NoSessions = 0x8001, + Sessions = 0x8002, + RspCommand = 0x00C4, +} + +impl TpmSt { + pub fn to_u16(self) -> u16 { + self as u16 + } + + pub fn from_u16(v: u16) -> Option { + match v { + 0x8001 => Some(TpmSt::NoSessions), + 0x8002 => Some(TpmSt::Sessions), + 0x00C4 => Some(TpmSt::RspCommand), + _ => None, + } + } +} + +/// ECC Curve IDs +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum TpmEccCurve { + None = 0x0000, + NistP256 = 0x0003, + NistP384 = 0x0004, + NistP521 = 0x0005, +} + +impl TpmEccCurve { + pub fn to_u16(self) -> u16 { + self as u16 + } +} + +/// RSA Key Bits +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum RsaKeyBits { + Rsa1024 = 1024, + Rsa2048 = 2048, + Rsa3072 = 3072, + Rsa4096 = 4096, +} + +impl RsaKeyBits { + pub fn to_u16(self) -> u16 { + self as u16 + } +} diff --git a/tpm2/src/device.rs b/tpm2/src/device.rs new file mode 100644 index 00000000..f57a7d1f --- /dev/null +++ b/tpm2/src/device.rs @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM device communication layer +//! +//! Provides low-level communication with TPM devices via /dev/tpmrm0 or /dev/tpm0. + +use anyhow::{bail, Context, Result}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::Path; + +use super::constants::*; +use super::marshal::*; + +/// Maximum TPM command/response size +const TPM_MAX_COMMAND_SIZE: usize = 4096; + +/// TPM device handle +pub struct TpmDevice { + file: File, + path: String, +} + +impl TpmDevice { + /// Open a TPM device + pub fn open(path: &str) -> Result { + // Strip "device:" prefix if present + let device_path = path.strip_prefix("device:").unwrap_or(path); + + let file = OpenOptions::new() + .read(true) + .write(true) + .open(device_path) + .with_context(|| format!("failed to open TPM device: {}", device_path))?; + + Ok(Self { + file, + path: device_path.to_string(), + }) + } + + /// Detect and open the default TPM device + pub fn detect() -> Result { + if Path::new("/dev/tpmrm0").exists() { + Self::open("/dev/tpmrm0") + } else if Path::new("/dev/tpm0").exists() { + Self::open("/dev/tpm0") + } else { + bail!("TPM device not found") + } + } + + /// Get the device path + pub fn path(&self) -> &str { + &self.path + } + + /// Send a command to the TPM and receive the response + pub fn transmit(&mut self, command: &[u8]) -> Result> { + // Write command + self.file + .write_all(command) + .context("failed to write TPM command")?; + + // Read response + let mut response = vec![0u8; TPM_MAX_COMMAND_SIZE]; + let n = self + .file + .read(&mut response) + .context("failed to read TPM response")?; + + response.truncate(n); + Ok(response) + } + + /// Execute a TPM command and parse the response + pub fn execute(&mut self, command: &[u8]) -> Result { + let response_bytes = self.transmit(command)?; + TpmResponse::parse(&response_bytes) + } +} + +/// TPM command builder +pub struct TpmCommand { + buf: CommandBuffer, +} + +impl TpmCommand { + /// Create a new command without sessions + pub fn new(command_code: TpmCc) -> Self { + let mut buf = CommandBuffer::with_capacity(256); + + // Header: tag (2) + size (4) + command code (4) + buf.put_u16(TpmSt::NoSessions.to_u16()); + buf.put_u32(0); // Size placeholder + buf.put_u32(command_code.to_u32()); + + Self { buf } + } + + /// Create a new command with sessions + pub fn with_sessions(command_code: TpmCc) -> Self { + let mut buf = CommandBuffer::with_capacity(256); + + // Header: tag (2) + size (4) + command code (4) + buf.put_u16(TpmSt::Sessions.to_u16()); + buf.put_u32(0); // Size placeholder + buf.put_u32(command_code.to_u32()); + + Self { buf } + } + + /// Add a handle to the command + pub fn add_handle(&mut self, handle: u32) { + self.buf.put_u32(handle); + } + + /// Add raw bytes to the command + pub fn add_bytes(&mut self, data: &[u8]) { + self.buf.put_bytes(data); + } + + /// Add a u8 value + pub fn add_u8(&mut self, v: u8) { + self.buf.put_u8(v); + } + + /// Add a u16 value + pub fn add_u16(&mut self, v: u16) { + self.buf.put_u16(v); + } + + /// Add a u32 value + pub fn add_u32(&mut self, v: u32) { + self.buf.put_u32(v); + } + + /// Add a TPM2B structure + pub fn add_tpm2b(&mut self, data: &[u8]) { + self.buf.put_tpm2b(data); + } + + /// Add an empty TPM2B structure + pub fn add_tpm2b_empty(&mut self) { + self.buf.put_tpm2b_empty(); + } + + /// Add a marshallable structure + pub fn add(&mut self, value: &T) { + value.marshal(&mut self.buf); + } + + /// Add password authorization session (null auth) + pub fn add_null_auth_area(&mut self) { + // Authorization area size (4 bytes) + // Session handle (4) + nonce (2) + attributes (1) + auth (2) = 9 bytes minimum + let auth_size: u32 = 4 + 2 + 1 + 2; // 9 bytes for null auth + + self.buf.put_u32(auth_size); + self.buf.put_u32(tpm_rh::PW); // Password session handle + self.buf.put_u16(0); // Empty nonce + self.buf.put_u8(0); // Session attributes (continue = 0) + self.buf.put_u16(0); // Empty auth value + } + + /// Add a policy session authorization + pub fn add_policy_auth(&mut self, session_handle: u32) { + let auth_size: u32 = 4 + 2 + 1 + 2; + + self.buf.put_u32(auth_size); + self.buf.put_u32(session_handle); + self.buf.put_u16(0); // Empty nonce + self.buf.put_u8(TpmaSa::CONTINUE_SESSION); // Continue session + self.buf.put_u16(0); // Empty auth value + } + + /// Finalize the command and return the bytes + pub fn finalize(mut self) -> Vec { + // Update the size field + let size = self.buf.len() as u32; + self.buf.update_u32(2, size); + self.buf.into_vec() + } + + /// Get current buffer for inspection + pub fn buffer(&self) -> &CommandBuffer { + &self.buf + } +} + +/// TPM response parser +#[derive(Debug)] +pub struct TpmResponse { + pub tag: TpmSt, + pub response_code: u32, + pub data: Vec, +} + +impl TpmResponse { + /// Parse a TPM response + pub fn parse(response: &[u8]) -> Result { + if response.len() < 10 { + bail!("TPM response too short: {} bytes", response.len()); + } + + let mut buf = ResponseBuffer::new(response); + + let tag_raw = buf.get_u16()?; + let tag = TpmSt::from_u16(tag_raw) + .ok_or_else(|| anyhow::anyhow!("invalid response tag: 0x{:04x}", tag_raw))?; + + let size = buf.get_u32()? as usize; + if response.len() < size { + bail!( + "TPM response size mismatch: expected {}, got {}", + size, + response.len() + ); + } + + let response_code = buf.get_u32()?; + + // Remaining data after header + let data = response[10..size].to_vec(); + + Ok(Self { + tag, + response_code, + data, + }) + } + + /// Check if the response indicates success + pub fn is_success(&self) -> bool { + self.response_code == 0 + } + + /// Get error description + pub fn error_description(&self) -> String { + if self.is_success() { + "success".to_string() + } else { + format!("TPM error: 0x{:08x}", self.response_code) + } + } + + /// Ensure the response is successful + pub fn ensure_success(&self) -> Result<()> { + if self.is_success() { + Ok(()) + } else { + bail!("{}", self.error_description()) + } + } + + /// Get a response buffer for parsing the data + pub fn data_buffer(&self) -> ResponseBuffer<'_> { + ResponseBuffer::new(&self.data) + } + + /// Skip the parameter size field (for commands with sessions) + pub fn skip_parameter_size(&self) -> Result> { + let mut buf = self.data_buffer(); + if self.tag == TpmSt::Sessions { + let _param_size = buf.get_u32()?; + } + Ok(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_command_builder() { + let mut cmd = TpmCommand::new(TpmCc::GetRandom); + cmd.add_u16(32); // Request 32 random bytes + + let bytes = cmd.finalize(); + + // Check header + assert_eq!(&bytes[0..2], &[0x80, 0x01]); // TPM_ST_NO_SESSIONS + assert_eq!(&bytes[6..10], &[0x00, 0x00, 0x01, 0x7B]); // TPM_CC_GetRandom + + // Check size + let size = u32::from_be_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]); + assert_eq!(size as usize, bytes.len()); + } + + #[test] + fn test_response_parse() { + // Minimal success response + let response = vec![ + 0x80, 0x01, // TPM_ST_NO_SESSIONS + 0x00, 0x00, 0x00, 0x0A, // Size = 10 + 0x00, 0x00, 0x00, 0x00, // TPM_RC_SUCCESS + ]; + + let parsed = TpmResponse::parse(&response).unwrap(); + assert!(parsed.is_success()); + assert!(parsed.data.is_empty()); + } +} diff --git a/tpm2/src/lib.rs b/tpm2/src/lib.rs new file mode 100644 index 00000000..ab5db337 --- /dev/null +++ b/tpm2/src/lib.rs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Pure Rust TPM 2.0 implementation +//! +//! This crate provides TPM 2.0 commands, communicating directly with the TPM +//! device without C library dependencies. +//! +//! ## Features +//! +//! - **Cross-compilation friendly**: Easy to cross-compile for different targets +//! - **Direct device communication**: Talks directly to `/dev/tpmrm0` or `/dev/tpm0` +//! +//! ## Supported Commands +//! +//! - NV operations: `NV_Read`, `NV_Write`, `NV_DefineSpace`, `NV_UndefineSpace` +//! - PCR operations: `PCR_Read`, `PCR_Extend` +//! - Key operations: `CreatePrimary`, `Create`, `Load`, `EvictControl` +//! - Sealing: `Seal`, `Unseal` with PCR policy +//! - Attestation: `Quote` +//! - Random: `GetRandom` +//! - Sessions: Policy sessions for PCR-based authorization +//! +//! ## Example +//! +//! ```no_run +//! use tpm2::TpmContext; +//! +//! let mut ctx = TpmContext::new(None)?; // Auto-detect TPM device +//! let random_bytes = ctx.get_random(32)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +mod commands; +mod constants; +mod device; +mod marshal; +mod session; +mod types; + +pub use commands::TpmContext; +pub use constants::*; +pub use types::*; + +// Re-export device for advanced usage +pub use device::{TpmCommand, TpmDevice, TpmResponse}; +pub use marshal::{CommandBuffer, Marshal, ResponseBuffer, Unmarshal}; +pub use session::AuthSession; diff --git a/tpm2/src/marshal.rs b/tpm2/src/marshal.rs new file mode 100644 index 00000000..e46eedd5 --- /dev/null +++ b/tpm2/src/marshal.rs @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 marshalling/unmarshalling utilities +//! +//! Provides serialization and deserialization for TPM structures. + +use anyhow::{bail, Result}; + +/// Buffer for building TPM commands +#[derive(Debug, Default)] +pub struct CommandBuffer { + data: Vec, +} + +impl CommandBuffer { + pub fn new() -> Self { + Self { data: Vec::new() } + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + data: Vec::with_capacity(capacity), + } + } + + pub fn put_u8(&mut self, v: u8) { + self.data.push(v); + } + + pub fn put_u16(&mut self, v: u16) { + self.data.extend_from_slice(&v.to_be_bytes()); + } + + pub fn put_u32(&mut self, v: u32) { + self.data.extend_from_slice(&v.to_be_bytes()); + } + + pub fn put_u64(&mut self, v: u64) { + self.data.extend_from_slice(&v.to_be_bytes()); + } + + pub fn put_bytes(&mut self, bytes: &[u8]) { + self.data.extend_from_slice(bytes); + } + + /// Put a TPM2B structure (2-byte size prefix + data) + pub fn put_tpm2b(&mut self, data: &[u8]) { + self.put_u16(data.len() as u16); + self.put_bytes(data); + } + + /// Put an empty TPM2B structure + pub fn put_tpm2b_empty(&mut self) { + self.put_u16(0); + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + pub fn into_vec(self) -> Vec { + self.data + } + + /// Update a u32 at a specific position (for size fields) + pub fn update_u32(&mut self, pos: usize, v: u32) { + self.data[pos..pos + 4].copy_from_slice(&v.to_be_bytes()); + } +} + +/// Buffer for parsing TPM responses +#[derive(Debug)] +pub struct ResponseBuffer<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> ResponseBuffer<'a> { + pub fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + pub fn remaining(&self) -> usize { + self.data.len().saturating_sub(self.pos) + } + + pub fn position(&self) -> usize { + self.pos + } + + pub fn get_u8(&mut self) -> Result { + if self.pos >= self.data.len() { + bail!("buffer underflow reading u8"); + } + let v = self.data[self.pos]; + self.pos += 1; + Ok(v) + } + + pub fn get_u16(&mut self) -> Result { + if self.pos + 2 > self.data.len() { + bail!("buffer underflow reading u16"); + } + let v = u16::from_be_bytes([self.data[self.pos], self.data[self.pos + 1]]); + self.pos += 2; + Ok(v) + } + + pub fn get_u32(&mut self) -> Result { + if self.pos + 4 > self.data.len() { + bail!("buffer underflow reading u32"); + } + let v = u32::from_be_bytes([ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + ]); + self.pos += 4; + Ok(v) + } + + pub fn get_u64(&mut self) -> Result { + if self.pos + 8 > self.data.len() { + bail!("buffer underflow reading u64"); + } + let v = u64::from_be_bytes([ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + self.data[self.pos + 4], + self.data[self.pos + 5], + self.data[self.pos + 6], + self.data[self.pos + 7], + ]); + self.pos += 8; + Ok(v) + } + + pub fn get_bytes(&mut self, len: usize) -> Result> { + if self.pos + len > self.data.len() { + bail!( + "buffer underflow reading {} bytes (remaining: {})", + len, + self.remaining() + ); + } + let v = self.data[self.pos..self.pos + len].to_vec(); + self.pos += len; + Ok(v) + } + + /// Get a TPM2B structure (2-byte size prefix + data) + pub fn get_tpm2b(&mut self) -> Result> { + let size = self.get_u16()? as usize; + self.get_bytes(size) + } + + /// Get remaining bytes + pub fn get_remaining(&mut self) -> Vec { + let v = self.data[self.pos..].to_vec(); + self.pos = self.data.len(); + v + } + + /// Skip bytes + pub fn skip(&mut self, len: usize) -> Result<()> { + if self.pos + len > self.data.len() { + bail!("buffer underflow skipping {} bytes", len); + } + self.pos += len; + Ok(()) + } + + /// Peek at bytes without advancing position + pub fn peek_bytes(&self, len: usize) -> Result<&[u8]> { + if self.pos + len > self.data.len() { + bail!("buffer underflow peeking {} bytes", len); + } + Ok(&self.data[self.pos..self.pos + len]) + } +} + +/// Trait for types that can be marshalled to TPM format +pub trait Marshal { + fn marshal(&self, buf: &mut CommandBuffer); + + fn to_bytes(&self) -> Vec { + let mut buf = CommandBuffer::new(); + self.marshal(&mut buf); + buf.into_vec() + } +} + +/// Trait for types that can be unmarshalled from TPM format +pub trait Unmarshal: Sized { + fn unmarshal(buf: &mut ResponseBuffer) -> Result; + + fn from_bytes(data: &[u8]) -> Result { + let mut buf = ResponseBuffer::new(data); + Self::unmarshal(&mut buf) + } +} + +// Implement Marshal for primitive types +impl Marshal for u8 { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u8(*self); + } +} + +impl Marshal for u16 { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(*self); + } +} + +impl Marshal for u32 { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u32(*self); + } +} + +impl Marshal for u64 { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u64(*self); + } +} + +// Implement Unmarshal for primitive types +impl Unmarshal for u8 { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + buf.get_u8() + } +} + +impl Unmarshal for u16 { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + buf.get_u16() + } +} + +impl Unmarshal for u32 { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + buf.get_u32() + } +} + +impl Unmarshal for u64 { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + buf.get_u64() + } +} diff --git a/tpm2/src/session.rs b/tpm2/src/session.rs new file mode 100644 index 00000000..b706990a --- /dev/null +++ b/tpm2/src/session.rs @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 session management + +use anyhow::{Context, Result}; + +use super::constants::*; +use super::device::*; +use super::marshal::*; +use super::types::*; + +/// Authorization session handle +#[derive(Debug, Clone, Copy)] +pub struct AuthSession { + pub handle: u32, + pub session_type: TpmSe, + pub hash_alg: TpmAlgId, +} + +impl AuthSession { + /// Start a new authorization session + pub fn start(device: &mut TpmDevice, session_type: TpmSe, hash_alg: TpmAlgId) -> Result { + // TPM2_StartAuthSession command + let mut cmd = TpmCommand::new(TpmCc::StartAuthSession); + + const ZERO_NONCE: [u8; 16] = [0u8; 16]; + + // tpmKey (TPM_RH_NULL for unbound session) + cmd.add_handle(tpm_rh::NULL); + // bind (TPM_RH_NULL for unbound session) + cmd.add_handle(tpm_rh::NULL); + // nonceCaller (16-byte nonce as required by TPM spec) + cmd.add_tpm2b(&ZERO_NONCE); + // encryptedSalt (empty - no salt) + cmd.add_tpm2b_empty(); + // sessionType + cmd.add_u8(session_type as u8); + // symmetric (AES-128-CFB, matches TPM default expectation) + cmd.add(&TpmtSymDef::aes_128_cfb()); + // authHash + cmd.add_u16(hash_alg.to_u16()); + + let cmd_bytes = cmd.finalize(); + tracing::debug!("StartAuthSession command: {} bytes", cmd_bytes.len()); + let response = device.execute(&cmd_bytes)?; + if !response.is_success() { + anyhow::bail!( + "StartAuthSession failed with TPM error: 0x{:08x}", + response.response_code + ); + } + + let mut buf = response.data_buffer(); + let handle = buf.get_u32()?; + let _nonce_tpm = buf.get_tpm2b()?; // nonceTPM + + Ok(Self { + handle, + session_type, + hash_alg, + }) + } + + /// Start a policy session + pub fn start_policy(device: &mut TpmDevice, hash_alg: TpmAlgId) -> Result { + Self::start(device, TpmSe::Policy, hash_alg) + } + + /// Start a trial policy session (for computing policy digest) + pub fn start_trial(device: &mut TpmDevice, hash_alg: TpmAlgId) -> Result { + Self::start(device, TpmSe::Trial, hash_alg) + } + + /// Apply PCR policy to this session + pub fn policy_pcr( + &self, + device: &mut TpmDevice, + pcr_digest: &[u8], + pcr_selection: &TpmlPcrSelection, + ) -> Result<()> { + let mut cmd = TpmCommand::new(TpmCc::PolicyPcr); + + // policySession + cmd.add_handle(self.handle); + // pcrDigest + cmd.add_tpm2b(pcr_digest); + // pcrs + cmd.add(pcr_selection); + + let response = device.execute(&cmd.finalize())?; + response.ensure_success().context("PolicyPCR failed")?; + + Ok(()) + } + + /// Get the current policy digest + pub fn get_digest(&self, device: &mut TpmDevice) -> Result> { + let mut cmd = TpmCommand::new(TpmCc::PolicyGetDigest); + cmd.add_handle(self.handle); + + let response = device.execute(&cmd.finalize())?; + response + .ensure_success() + .context("PolicyGetDigest failed")?; + + let mut buf = response.data_buffer(); + let digest = buf.get_tpm2b()?; + + Ok(digest) + } + + /// Flush (close) this session + pub fn flush(self, device: &mut TpmDevice) -> Result<()> { + let mut cmd = TpmCommand::new(TpmCc::FlushContext); + cmd.add_handle(self.handle); + + let response = device.execute(&cmd.finalize())?; + response.ensure_success().context("FlushContext failed")?; + + Ok(()) + } +} + +/// Compute the PCR digest for a given PCR selection +pub fn compute_pcr_digest( + device: &mut TpmDevice, + pcr_selection: &TpmlPcrSelection, + hash_alg: TpmAlgId, +) -> Result> { + use sha2::{Digest, Sha256, Sha384, Sha512}; + + // Read PCR values + let pcr_values = read_pcr_values(device, pcr_selection)?; + + // Concatenate all PCR values + let mut concat = Vec::new(); + for value in &pcr_values { + concat.extend_from_slice(value); + } + + // Hash the concatenated values + let digest = match hash_alg { + TpmAlgId::Sha256 => { + let mut hasher = Sha256::new(); + hasher.update(&concat); + hasher.finalize().to_vec() + } + TpmAlgId::Sha384 => { + let mut hasher = Sha384::new(); + hasher.update(&concat); + hasher.finalize().to_vec() + } + TpmAlgId::Sha512 => { + let mut hasher = Sha512::new(); + hasher.update(&concat); + hasher.finalize().to_vec() + } + _ => anyhow::bail!("unsupported hash algorithm for PCR digest"), + }; + + Ok(digest) +} + +/// Read PCR values for a selection +fn read_pcr_values( + device: &mut TpmDevice, + pcr_selection: &TpmlPcrSelection, +) -> Result>> { + let mut cmd = TpmCommand::new(TpmCc::PcrRead); + cmd.add(pcr_selection); + + let response = device.execute(&cmd.finalize())?; + response.ensure_success().context("PCR_Read failed")?; + + let mut buf = response.data_buffer(); + let _update_counter = buf.get_u32()?; + let _pcr_selection_out = TpmlPcrSelection::unmarshal(&mut buf)?; + let digest_list = TpmlDigest::unmarshal(&mut buf)?; + + Ok(digest_list.digests.into_iter().map(|d| d.buffer).collect()) +} diff --git a/tpm2/src/types.rs b/tpm2/src/types.rs new file mode 100644 index 00000000..65d9c709 --- /dev/null +++ b/tpm2/src/types.rs @@ -0,0 +1,876 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 data types + +use anyhow::{bail, Result}; + +use super::constants::*; +use super::marshal::*; + +/// TPM2B_DIGEST - Variable length digest +#[derive(Debug, Clone, Default)] +pub struct Tpm2bDigest { + pub buffer: Vec, +} + +impl Tpm2bDigest { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } + + pub fn empty() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Marshal for Tpm2bDigest { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bDigest { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPM2B_DATA - Variable length data +#[derive(Debug, Clone, Default)] +pub struct Tpm2bData { + pub buffer: Vec, +} + +impl Tpm2bData { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } + + pub fn empty() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Marshal for Tpm2bData { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bData { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPM2B_SENSITIVE_DATA - Sensitive data for sealing +#[derive(Debug, Clone, Default)] +pub struct Tpm2bSensitiveData { + pub buffer: Vec, +} + +impl Tpm2bSensitiveData { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } + + pub fn empty() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Marshal for Tpm2bSensitiveData { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +/// TPM2B_AUTH - Authorization value +#[derive(Debug, Clone, Default)] +pub struct Tpm2bAuth { + pub buffer: Vec, +} + +impl Tpm2bAuth { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } + + pub fn empty() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Marshal for Tpm2bAuth { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bAuth { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPM2B_NONCE - Nonce value +pub type Tpm2bNonce = Tpm2bDigest; + +/// TPM2B_MAX_NV_BUFFER - NV buffer +#[derive(Debug, Clone, Default)] +pub struct Tpm2bMaxNvBuffer { + pub buffer: Vec, +} + +impl Tpm2bMaxNvBuffer { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } +} + +impl Marshal for Tpm2bMaxNvBuffer { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bMaxNvBuffer { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPMS_PCR_SELECTION - PCR selection for a single hash algorithm +#[derive(Debug, Clone)] +pub struct TpmsPcrSelection { + pub hash: TpmAlgId, + pub pcr_select: Vec, // Bitmap of selected PCRs +} + +impl TpmsPcrSelection { + pub fn new(hash: TpmAlgId, pcrs: &[u32]) -> Self { + // Calculate required size (at least 3 bytes for PCR 0-23) + let max_pcr = pcrs.iter().max().copied().unwrap_or(0); + let size = ((max_pcr / 8) + 1).max(3) as usize; + let mut pcr_select = vec![0u8; size]; + + for &pcr in pcrs { + let byte_idx = (pcr / 8) as usize; + let bit_idx = pcr % 8; + if byte_idx < pcr_select.len() { + pcr_select[byte_idx] |= 1 << bit_idx; + } + } + + Self { hash, pcr_select } + } + + pub fn sha256(pcrs: &[u32]) -> Self { + Self::new(TpmAlgId::Sha256, pcrs) + } +} + +impl Marshal for TpmsPcrSelection { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.hash.to_u16()); + buf.put_u8(self.pcr_select.len() as u8); + buf.put_bytes(&self.pcr_select); + } +} + +impl Unmarshal for TpmsPcrSelection { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let hash_alg = buf.get_u16()?; + let hash = TpmAlgId::from_u16(hash_alg) + .ok_or_else(|| anyhow::anyhow!("unknown hash algorithm: 0x{:04x}", hash_alg))?; + let size = buf.get_u8()? as usize; + let pcr_select = buf.get_bytes(size)?; + Ok(Self { hash, pcr_select }) + } +} + +/// TPML_PCR_SELECTION - List of PCR selections +#[derive(Debug, Clone, Default)] +pub struct TpmlPcrSelection { + pub pcr_selections: Vec, +} + +impl TpmlPcrSelection { + pub fn new(selections: Vec) -> Self { + Self { + pcr_selections: selections, + } + } + + pub fn single(hash: TpmAlgId, pcrs: &[u32]) -> Self { + Self { + pcr_selections: vec![TpmsPcrSelection::new(hash, pcrs)], + } + } +} + +impl Marshal for TpmlPcrSelection { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u32(self.pcr_selections.len() as u32); + for sel in &self.pcr_selections { + sel.marshal(buf); + } + } +} + +impl Unmarshal for TpmlPcrSelection { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let count = buf.get_u32()? as usize; + let mut pcr_selections = Vec::with_capacity(count); + for _ in 0..count { + pcr_selections.push(TpmsPcrSelection::unmarshal(buf)?); + } + Ok(Self { pcr_selections }) + } +} + +/// TPML_DIGEST - List of digests +#[derive(Debug, Clone, Default)] +pub struct TpmlDigest { + pub digests: Vec, +} + +impl Unmarshal for TpmlDigest { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let count = buf.get_u32()? as usize; + let mut digests = Vec::with_capacity(count); + for _ in 0..count { + digests.push(Tpm2bDigest::unmarshal(buf)?); + } + Ok(Self { digests }) + } +} + +/// TPMS_NV_PUBLIC - NV index public area +#[derive(Debug, Clone)] +pub struct TpmsNvPublic { + pub nv_index: u32, + pub name_alg: TpmAlgId, + pub attributes: TpmaNv, + pub auth_policy: Tpm2bDigest, + pub data_size: u16, +} + +impl TpmsNvPublic { + pub fn new(nv_index: u32, data_size: u16, attributes: TpmaNv) -> Self { + Self { + nv_index, + name_alg: TpmAlgId::Sha256, + attributes, + auth_policy: Tpm2bDigest::empty(), + data_size, + } + } +} + +impl Marshal for TpmsNvPublic { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u32(self.nv_index); + buf.put_u16(self.name_alg.to_u16()); + buf.put_u32(self.attributes.0); + self.auth_policy.marshal(buf); + buf.put_u16(self.data_size); + } +} + +impl Unmarshal for TpmsNvPublic { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let nv_index = buf.get_u32()?; + let name_alg_raw = buf.get_u16()?; + let name_alg = TpmAlgId::from_u16(name_alg_raw) + .ok_or_else(|| anyhow::anyhow!("unknown algorithm: 0x{:04x}", name_alg_raw))?; + let attributes = TpmaNv(buf.get_u32()?); + let auth_policy = Tpm2bDigest::unmarshal(buf)?; + let data_size = buf.get_u16()?; + Ok(Self { + nv_index, + name_alg, + attributes, + auth_policy, + data_size, + }) + } +} + +/// TPM2B_NV_PUBLIC - NV public with size prefix +#[derive(Debug, Clone)] +pub struct Tpm2bNvPublic { + pub nv_public: TpmsNvPublic, +} + +impl Marshal for Tpm2bNvPublic { + fn marshal(&self, buf: &mut CommandBuffer) { + let mut inner = CommandBuffer::new(); + self.nv_public.marshal(&mut inner); + buf.put_tpm2b(inner.as_bytes()); + } +} + +impl Unmarshal for Tpm2bNvPublic { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let size = buf.get_u16()? as usize; + if size == 0 { + bail!("empty NV public"); + } + let data = buf.get_bytes(size)?; + let mut inner = ResponseBuffer::new(&data); + let nv_public = TpmsNvPublic::unmarshal(&mut inner)?; + Ok(Self { nv_public }) + } +} + +/// TPMT_SYM_DEF - Symmetric algorithm definition +#[derive(Debug, Clone, Copy)] +pub struct TpmtSymDef { + pub algorithm: TpmAlgId, + pub key_bits: u16, + pub mode: TpmAlgId, +} + +impl TpmtSymDef { + pub fn null() -> Self { + Self { + algorithm: TpmAlgId::Null, + key_bits: 0, + mode: TpmAlgId::Null, + } + } + + pub fn aes_128_cfb() -> Self { + Self { + algorithm: TpmAlgId::Aes, + key_bits: 128, + mode: TpmAlgId::Cfb, + } + } +} + +impl Marshal for TpmtSymDef { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.algorithm.to_u16()); + if self.algorithm != TpmAlgId::Null { + buf.put_u16(self.key_bits); + buf.put_u16(self.mode.to_u16()); + } + } +} + +impl Unmarshal for TpmtSymDef { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let alg = buf.get_u16()?; + let algorithm = TpmAlgId::from_u16(alg) + .ok_or_else(|| anyhow::anyhow!("unknown algorithm: 0x{:04x}", alg))?; + if algorithm == TpmAlgId::Null { + Ok(Self::null()) + } else { + let key_bits = buf.get_u16()?; + let mode_raw = buf.get_u16()?; + let mode = TpmAlgId::from_u16(mode_raw) + .ok_or_else(|| anyhow::anyhow!("unknown mode: 0x{:04x}", mode_raw))?; + Ok(Self { + algorithm, + key_bits, + mode, + }) + } + } +} + +/// TPMT_SYM_DEF_OBJECT - Symmetric definition for objects +pub type TpmtSymDefObject = TpmtSymDef; + +/// TPMS_SCHEME_HASH - Hash scheme +#[derive(Debug, Clone, Copy)] +pub struct TpmsSchemeHash { + pub hash_alg: TpmAlgId, +} + +/// TPMT_RSA_SCHEME - RSA signature scheme +#[derive(Debug, Clone, Copy)] +pub struct TpmtRsaScheme { + pub scheme: TpmAlgId, + pub hash_alg: Option, +} + +impl TpmtRsaScheme { + pub fn null() -> Self { + Self { + scheme: TpmAlgId::Null, + hash_alg: None, + } + } + + pub fn rsassa(hash: TpmAlgId) -> Self { + Self { + scheme: TpmAlgId::RsaSsa, + hash_alg: Some(hash), + } + } +} + +impl Marshal for TpmtRsaScheme { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.scheme.to_u16()); + if let Some(hash) = self.hash_alg { + buf.put_u16(hash.to_u16()); + } + } +} + +/// TPMT_ECC_SCHEME - ECC signature scheme +#[derive(Debug, Clone, Copy)] +pub struct TpmtEccScheme { + pub scheme: TpmAlgId, + pub hash_alg: Option, +} + +impl TpmtEccScheme { + pub fn null() -> Self { + Self { + scheme: TpmAlgId::Null, + hash_alg: None, + } + } + + pub fn ecdsa(hash: TpmAlgId) -> Self { + Self { + scheme: TpmAlgId::EcDsa, + hash_alg: Some(hash), + } + } +} + +impl Marshal for TpmtEccScheme { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.scheme.to_u16()); + if let Some(hash) = self.hash_alg { + buf.put_u16(hash.to_u16()); + } + } +} + +/// TPMT_SIG_SCHEME - Signature scheme (for Quote) +#[derive(Debug, Clone, Copy)] +pub struct TpmtSigScheme { + pub scheme: TpmAlgId, + pub hash_alg: Option, +} + +impl TpmtSigScheme { + pub fn null() -> Self { + Self { + scheme: TpmAlgId::Null, + hash_alg: None, + } + } +} + +impl Marshal for TpmtSigScheme { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.scheme.to_u16()); + if let Some(hash) = self.hash_alg { + buf.put_u16(hash.to_u16()); + } + } +} + +/// TPMS_RSA_PARMS - RSA key parameters +#[derive(Debug, Clone)] +pub struct TpmsRsaParms { + pub symmetric: TpmtSymDefObject, + pub scheme: TpmtRsaScheme, + pub key_bits: u16, + pub exponent: u32, +} + +impl TpmsRsaParms { + pub fn storage_key() -> Self { + Self { + symmetric: TpmtSymDef::aes_128_cfb(), + scheme: TpmtRsaScheme::null(), + key_bits: 2048, + exponent: 0, // Default exponent (65537) + } + } +} + +impl Marshal for TpmsRsaParms { + fn marshal(&self, buf: &mut CommandBuffer) { + self.symmetric.marshal(buf); + self.scheme.marshal(buf); + buf.put_u16(self.key_bits); + buf.put_u32(self.exponent); + } +} + +/// TPMS_ECC_PARMS - ECC key parameters +#[derive(Debug, Clone)] +pub struct TpmsEccParms { + pub symmetric: TpmtSymDefObject, + pub scheme: TpmtEccScheme, + pub curve_id: TpmEccCurve, + pub kdf: TpmAlgId, +} + +impl Marshal for TpmsEccParms { + fn marshal(&self, buf: &mut CommandBuffer) { + self.symmetric.marshal(buf); + self.scheme.marshal(buf); + buf.put_u16(self.curve_id.to_u16()); + buf.put_u16(self.kdf.to_u16()); // KDF scheme (usually NULL) + } +} + +/// TPMS_KEYEDHASH_PARMS - Keyed hash parameters (for sealed data) +#[derive(Debug, Clone, Copy)] +pub struct TpmsKeyedHashParms { + pub scheme: TpmAlgId, +} + +impl TpmsKeyedHashParms { + pub fn null() -> Self { + Self { + scheme: TpmAlgId::Null, + } + } +} + +impl Marshal for TpmsKeyedHashParms { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.scheme.to_u16()); + } +} + +/// TPMT_PUBLIC - Public area template +#[derive(Debug, Clone)] +pub struct TpmtPublic { + pub type_alg: TpmAlgId, + pub name_alg: TpmAlgId, + pub object_attributes: TpmaObject, + pub auth_policy: Tpm2bDigest, + pub parameters: TpmtPublicParms, + pub unique: TpmtPublicUnique, +} + +/// TPMU_PUBLIC_PARMS - Public parameters union +#[derive(Debug, Clone)] +pub enum TpmtPublicParms { + Rsa(TpmsRsaParms), + Ecc(TpmsEccParms), + KeyedHash(TpmsKeyedHashParms), +} + +impl Marshal for TpmtPublicParms { + fn marshal(&self, buf: &mut CommandBuffer) { + match self { + TpmtPublicParms::Rsa(p) => p.marshal(buf), + TpmtPublicParms::Ecc(p) => p.marshal(buf), + TpmtPublicParms::KeyedHash(p) => p.marshal(buf), + } + } +} + +/// TPMU_PUBLIC_ID - Unique identifier union +#[derive(Debug, Clone)] +pub enum TpmtPublicUnique { + Rsa(Vec), // TPM2B_PUBLIC_KEY_RSA + Ecc(Vec, Vec), // TPMS_ECC_POINT (x, y) + KeyedHash(Vec), // TPM2B_DIGEST +} + +impl Marshal for TpmtPublicUnique { + fn marshal(&self, buf: &mut CommandBuffer) { + match self { + TpmtPublicUnique::Rsa(n) => buf.put_tpm2b(n), + TpmtPublicUnique::Ecc(x, y) => { + buf.put_tpm2b(x); + buf.put_tpm2b(y); + } + TpmtPublicUnique::KeyedHash(d) => buf.put_tpm2b(d), + } + } +} + +impl TpmtPublic { + /// Create an RSA storage key template (SRK) + pub fn rsa_storage_key() -> Self { + Self { + type_alg: TpmAlgId::Rsa, + name_alg: TpmAlgId::Sha256, + object_attributes: TpmaObject::new() + .with_fixed_tpm() + .with_fixed_parent() + .with_sensitive_data_origin() + .with_user_with_auth() + .with_restricted() + .with_decrypt(), + auth_policy: Tpm2bDigest::empty(), + parameters: TpmtPublicParms::Rsa(TpmsRsaParms::storage_key()), + unique: TpmtPublicUnique::Rsa(Vec::new()), + } + } + + /// Create a sealed data object template + pub fn sealed_object(policy_digest: Tpm2bDigest) -> Self { + // If policy_digest is empty, use userWithAuth; otherwise use adminWithPolicy + let object_attributes = if policy_digest.buffer.is_empty() { + TpmaObject::new() + .with_fixed_tpm() + .with_fixed_parent() + .with_user_with_auth() + } else { + TpmaObject::new() + .with_fixed_tpm() + .with_fixed_parent() + .with_admin_with_policy() + }; + + Self { + type_alg: TpmAlgId::KeyedHash, + name_alg: TpmAlgId::Sha256, + object_attributes, + auth_policy: policy_digest, + parameters: TpmtPublicParms::KeyedHash(TpmsKeyedHashParms::null()), + unique: TpmtPublicUnique::KeyedHash(Vec::new()), + } + } +} + +impl Marshal for TpmtPublic { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.type_alg.to_u16()); + buf.put_u16(self.name_alg.to_u16()); + buf.put_u32(self.object_attributes.0); + self.auth_policy.marshal(buf); + self.parameters.marshal(buf); + self.unique.marshal(buf); + } +} + +/// TPM2B_PUBLIC - Public area with size prefix +#[derive(Debug, Clone)] +pub struct Tpm2bPublic { + pub public_area: Vec, // Raw marshalled TPMT_PUBLIC +} + +impl Tpm2bPublic { + pub fn from_template(template: &TpmtPublic) -> Self { + Self { + public_area: template.to_bytes(), + } + } +} + +impl Marshal for Tpm2bPublic { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.public_area); + } +} + +impl Unmarshal for Tpm2bPublic { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let public_area = buf.get_tpm2b()?; + Ok(Self { public_area }) + } +} + +/// TPM2B_PRIVATE - Private area +#[derive(Debug, Clone)] +pub struct Tpm2bPrivate { + pub buffer: Vec, +} + +impl Tpm2bPrivate { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } +} + +impl Marshal for Tpm2bPrivate { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bPrivate { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPM2B_SENSITIVE_CREATE - Sensitive data for object creation +#[derive(Debug, Clone, Default)] +pub struct Tpm2bSensitiveCreate { + pub user_auth: Tpm2bAuth, + pub data: Tpm2bSensitiveData, +} + +impl Tpm2bSensitiveCreate { + pub fn with_data(data: Vec) -> Self { + Self { + user_auth: Tpm2bAuth::empty(), + data: Tpm2bSensitiveData::new(data), + } + } + + pub fn empty() -> Self { + Self::default() + } +} + +impl Marshal for Tpm2bSensitiveCreate { + fn marshal(&self, buf: &mut CommandBuffer) { + // First marshal the inner structure + let mut inner = CommandBuffer::new(); + self.user_auth.marshal(&mut inner); + self.data.marshal(&mut inner); + // Then wrap with size + buf.put_tpm2b(inner.as_bytes()); + } +} + +/// TPMS_ATTEST - Attestation structure (returned by Quote) +#[derive(Debug, Clone)] +pub struct TpmsAttest { + pub raw: Vec, // Keep raw bytes for signature verification +} + +impl Unmarshal for TpmsAttest { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + // For Quote, we need the raw bytes for verification + // The structure is variable length, so we capture everything + let raw = buf.get_remaining(); + Ok(Self { raw }) + } +} + +/// TPM2B_ATTEST - Attestation data with size prefix +#[derive(Debug, Clone)] +pub struct Tpm2bAttest { + pub attestation_data: Vec, +} + +impl Unmarshal for Tpm2bAttest { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let attestation_data = buf.get_tpm2b()?; + Ok(Self { attestation_data }) + } +} + +/// TPMT_SIGNATURE - Signature structure +#[derive(Debug, Clone)] +pub struct TpmtSignature { + pub raw: Vec, // Keep raw bytes for verification +} + +impl Unmarshal for TpmtSignature { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + // Signature format depends on algorithm, capture remaining bytes + let raw = buf.get_remaining(); + Ok(Self { raw }) + } +} + +/// TPMT_TK_CREATION - Creation ticket +#[derive(Debug, Clone)] +pub struct TpmtTkCreation { + pub tag: u16, + pub hierarchy: u32, + pub digest: Tpm2bDigest, +} + +impl Unmarshal for TpmtTkCreation { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let tag = buf.get_u16()?; + let hierarchy = buf.get_u32()?; + let digest = Tpm2bDigest::unmarshal(buf)?; + Ok(Self { + tag, + hierarchy, + digest, + }) + } +} + +/// TPM2B_CREATION_DATA - Creation data +#[derive(Debug, Clone)] +pub struct Tpm2bCreationData { + pub data: Vec, +} + +impl Unmarshal for Tpm2bCreationData { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let data = buf.get_tpm2b()?; + Ok(Self { data }) + } +} + +/// TPMS_SENSITIVE_CREATE - Inner sensitive create structure +#[derive(Debug, Clone, Default)] +pub struct TpmsSensitiveCreate { + pub user_auth: Tpm2bAuth, + pub data: Tpm2bSensitiveData, +} + +/// TPMT_HA - Hash value with algorithm +#[derive(Debug, Clone)] +pub struct TpmtHa { + pub hash_alg: TpmAlgId, + pub digest: Vec, +} + +impl TpmtHa { + pub fn sha256(digest: Vec) -> Self { + Self { + hash_alg: TpmAlgId::Sha256, + digest, + } + } +} + +impl Marshal for TpmtHa { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.hash_alg.to_u16()); + buf.put_bytes(&self.digest); + } +} + +/// TPML_DIGEST_VALUES - List of digest values for PCR extend +#[derive(Debug, Clone)] +pub struct TpmlDigestValues { + pub digests: Vec, +} + +impl TpmlDigestValues { + pub fn single(digest: TpmtHa) -> Self { + Self { + digests: vec![digest], + } + } +} + +impl Marshal for TpmlDigestValues { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u32(self.digests.len() as u32); + for d in &self.digests { + d.marshal(buf); + } + } +} diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml index 78706da9..0b8917b7 100644 --- a/verifier/Cargo.toml +++ b/verifier/Cargo.toml @@ -11,18 +11,26 @@ license.workspace = true homepage.workspace = true repository.workspace = true +[lib] +name = "dstack_verifier" +path = "src/lib.rs" + +[[bin]] +name = "dstack-verifier" +path = "src/main.rs" + [dependencies] anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } -figment.workspace = true +clap = { workspace = true, features = ["derive"], optional = true } +figment = { workspace = true, optional = true } fs-err.workspace = true hex.workspace = true -rocket = { workspace = true, features = ["json"] } +rocket = { workspace = true, features = ["json"], optional = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true tokio = { workspace = true, features = ["full"] } tracing.workspace = true -tracing-subscriber.workspace = true +tracing-subscriber = { workspace = true, optional = true } reqwest.workspace = true tempfile.workspace = true @@ -35,3 +43,11 @@ dstack-mr.workspace = true dcap-qvl.workspace = true cc-eventlog.workspace = true sha2.workspace = true +tpm-qvl.workspace = true +tpm-types.workspace = true +serde-human-bytes.workspace = true +hex-literal.workspace = true + +[features] +default = ["binary"] +binary = ["dep:clap", "dep:figment", "dep:rocket", "dep:tracing-subscriber"] diff --git a/verifier/builder/Dockerfile b/verifier/builder/Dockerfile index cef0d128..2f7d6a7e 100644 --- a/verifier/builder/Dockerfile +++ b/verifier/builder/Dockerfile @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -FROM rust:1.86.0@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 AS verifier-builder +FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS verifier-builder COPY builder/shared /build/shared ARG DSTACK_REV ARG DSTACK_SRC_URL=https://github.com/Dstack-TEE/dstack.git diff --git a/verifier/src/lib.rs b/verifier/src/lib.rs new file mode 100644 index 00000000..532d1758 --- /dev/null +++ b/verifier/src/lib.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! CVM verification library +//! +//! This library provides functionality to verify Confidential VM (CVM) attestations, +//! including TDX quote verification, event log replay, and OS image hash validation. +//! +//! Can be used both as a library and as a standalone binary/HTTP server. + +mod types; +mod verification; + +// Re-export TdxMeasurements from dstack-mr for convenience +pub use dstack_mr::TdxMeasurements; + +pub use types::{ + AcpiTables, ErrorResponse, RtmrEventEntry, RtmrEventStatus, RtmrMismatch, VerificationDetails, + VerificationRequest, VerificationResponse, +}; +pub use verification::CvmVerifier; diff --git a/verifier/src/main.rs b/verifier/src/main.rs index f145d71d..1bd72a91 100644 --- a/verifier/src/main.rs +++ b/verifier/src/main.rs @@ -6,6 +6,9 @@ use std::sync::Arc; use anyhow::{Context, Result}; use clap::Parser; +use dstack_verifier::{ + CvmVerifier, VerificationDetails, VerificationRequest, VerificationResponse, +}; use figment::{ providers::{Env, Format, Toml}, Figment, @@ -14,12 +17,6 @@ use rocket::{fairing::AdHoc, get, post, serde::json::Json, State}; use serde::{Deserialize, Serialize}; use tracing::{error, info}; -mod types; -mod verification; - -use types::{VerificationRequest, VerificationResponse}; -use verification::CvmVerifier; - #[derive(Parser)] #[command(name = "dstack-verifier")] #[command(about = "HTTP server providing CVM verification services")] @@ -47,13 +44,13 @@ async fn verify_cvm( verifier: &State>, request: Json, ) -> Json { - match verifier.verify(&request.into_inner()).await { + match verifier.verify(request.into_inner()).await { Ok(response) => Json(response), Err(e) => { error!("Verification failed: {:?}", e); Json(VerificationResponse { is_valid: false, - details: types::VerificationDetails { + details: VerificationDetails { quote_verified: false, event_log_verified: false, os_image_hash_verified: false, @@ -103,7 +100,7 @@ async fn run_oneshot(file_path: &str, config: &Config) -> anyhow::Result<()> { // Run verification info!("Starting verification..."); - let response = verifier.verify(&request).await?; + let response = verifier.verify(request).await?; // Persist response next to the input file for convenience let output_path = format!("{file_path}.verification.json"); @@ -152,10 +149,6 @@ async fn run_oneshot(file_path: &str, config: &Config) -> anyhow::Result<()> { println!("App ID: {}", hex::encode(&app_info.app_id)); println!("Instance ID: {}", hex::encode(&app_info.instance_id)); println!("Compose hash: {}", hex::encode(&app_info.compose_hash)); - println!("MRTD: {}", hex::encode(app_info.mrtd)); - println!("RTMR0: {}", hex::encode(app_info.rtmr0)); - println!("RTMR1: {}", hex::encode(app_info.rtmr1)); - println!("RTMR2: {}", hex::encode(app_info.rtmr2)); } // Exit with appropriate code @@ -183,14 +176,10 @@ async fn main() -> Result<()> { // Check for oneshot mode if let Some(file_path) = cli.verify { - // Run oneshot verification and exit - let rt = tokio::runtime::Runtime::new().context("Failed to create runtime")?; - rt.block_on(async { - if let Err(e) = run_oneshot(&file_path, &config).await { - error!("Oneshot verification failed: {:#}", e); - std::process::exit(1); - } - }); + if let Err(e) = run_oneshot(&file_path, &config).await { + error!("Oneshot verification failed: {:#}", e); + std::process::exit(1); + } std::process::exit(0); } diff --git a/verifier/src/types.rs b/verifier/src/types.rs index e4e5d2c5..d1102479 100644 --- a/verifier/src/types.rs +++ b/verifier/src/types.rs @@ -5,11 +5,16 @@ use ra_tls::attestation::AppInfo; use serde::{Deserialize, Serialize}; +use serde_human_bytes as serde_bytes; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VerificationRequest { - pub quote: String, - pub event_log: String, - pub vm_config: String, + #[serde(with = "serde_bytes")] + pub quote: Option>, + pub event_log: Option, + pub vm_config: Option, + #[serde(with = "serde_bytes")] + pub attestation: Option>, pub pccs_url: Option, pub debug: Option, } @@ -21,7 +26,7 @@ pub struct VerificationResponse { pub reason: Option, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Default, Serialize)] pub struct VerificationDetails { pub quote_verified: bool, pub event_log_verified: bool, diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index a53da571..2ef33542 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -9,12 +9,15 @@ use std::{ }; use anyhow::{anyhow, bail, Context, Result}; -use cc_eventlog::TdxEventLog as EventLog; +use cc_eventlog::TdxEvent; use dstack_mr::{RtmrLog, TdxMeasurementDetails, TdxMeasurements}; use dstack_types::VmConfig; -use ra_tls::attestation::{Attestation, VerifiedAttestation}; +use hex_literal::hex; +use ra_tls::attestation::{ + Attestation, AttestationMode, TpmQuote, VerifiedAttestation, VersionedAttestation, +}; use serde::{Deserialize, Serialize}; -use sha2::{Digest as _, Sha256, Sha384}; +use sha2::{Digest as _, Sha256}; use tokio::{io::AsyncWriteExt, process::Command}; use tracing::{debug, info, warn}; @@ -23,45 +26,13 @@ use crate::types::{ VerificationRequest, VerificationResponse, }; -#[derive(Debug, Clone)] -struct RtmrComputationResult { - event_indices: [Vec; 4], - rtmrs: [[u8; 48]; 4], -} - -fn replay_event_logs(eventlog: &[EventLog]) -> Result { - let mut event_indices: [Vec; 4] = Default::default(); - let mut rtmrs: [[u8; 48]; 4] = [[0u8; 48]; 4]; - - for idx in 0..4 { - for (event_idx, event) in eventlog.iter().enumerate() { - event - .validate() - .context("Failed to validate event digest")?; - - if event.imr == idx { - event_indices[idx as usize].push(event_idx); - let mut hasher = Sha384::new(); - hasher.update(rtmrs[idx as usize]); - hasher.update(event.digest); - rtmrs[idx as usize] = hasher.finalize().into(); - } - } - } - - Ok(RtmrComputationResult { - event_indices, - rtmrs, - }) -} - fn collect_rtmr_mismatch( rtmr_label: &str, expected: &[u8], actual: &[u8], expected_sequence: &RtmrLog, actual_indices: &[usize], - event_log: &[EventLog], + event_log: &[TdxEvent], ) -> RtmrMismatch { let expected_hex = hex::encode(expected); let actual_hex = hex::encode(actual); @@ -76,7 +47,7 @@ fn collect_rtmr_mismatch( } else { event.event.clone() }; - let status = if event.digest == expected_digest.as_slice() { + let status = if event.digest() == expected_digest.as_slice() { RtmrEventStatus::Match } else { RtmrEventStatus::Mismatch @@ -85,7 +56,7 @@ fn collect_rtmr_mismatch( index: idx, event_type: event.event_type, event_name, - actual_digest: hex::encode(event.digest), + actual_digest: hex::encode(event.digest()), expected_digest: Some(hex::encode(expected_digest)), payload_len: event.event_payload.len(), status, @@ -114,7 +85,7 @@ fn collect_rtmr_mismatch( } else { event.event.clone() }, - hex::encode(event.digest), + hex::encode(event.digest()), event.event_payload.len(), ), None => (0, "(missing)".to_string(), String::new(), 0), @@ -156,6 +127,13 @@ struct CachedMeasurement { measurements: TdxMeasurements, } +struct ImagePaths { + fw_path: PathBuf, + kernel_path: PathBuf, + initrd_path: PathBuf, + kernel_cmdline: String, +} + pub struct CvmVerifier { pub image_cache_dir: String, pub download_url: String, @@ -286,6 +264,7 @@ impl CvmVerifier { .hugepages(vm_config.hugepages) .num_gpus(vm_config.num_gpus) .num_nvswitches(vm_config.num_nvswitches) + .host_share_mode(vm_config.host_share_mode.clone()) .build() .measure_with_logs() .context("Failed to compute expected MRs")?; @@ -343,17 +322,79 @@ impl CvmVerifier { Ok(measurements) } - pub async fn verify(&self, request: &VerificationRequest) -> Result { - let quote = hex::decode(&request.quote).context("Failed to decode quote hex")?; + /// Helper method to ensure image is downloaded and return image paths + async fn ensure_image_downloaded(&self, vm_config: &VmConfig) -> Result { + let hex_os_image_hash = hex::encode(&vm_config.os_image_hash); - // Event log is always JSON string - let event_log = request.event_log.as_bytes().to_vec(); + // Get image directory + let image_dir = Path::new(&self.image_cache_dir) + .join("images") + .join(&hex_os_image_hash); - let attestation = Attestation::new(quote, event_log) - .context("Failed to create attestation from quote and event log")?; + let metadata_path = image_dir.join("metadata.json"); + if !metadata_path.exists() { + info!("Image {hex_os_image_hash} not found, downloading"); + tokio::time::timeout( + self.download_timeout, + self.download_image(&hex_os_image_hash, &image_dir), + ) + .await + .context("Download image timeout")? + .with_context(|| format!("Failed to download image {hex_os_image_hash}"))?; + } - let debug = request.debug.unwrap_or(false); + let image_info = + fs_err::read_to_string(metadata_path).context("Failed to read image metadata")?; + let image_info: dstack_types::ImageInfo = + serde_json::from_str(&image_info).context("Failed to parse image metadata")?; + + let fw_path = image_dir.join(&image_info.bios); + let kernel_path = image_dir.join(&image_info.kernel); + let initrd_path = image_dir.join(&image_info.initrd); + let kernel_cmdline = image_info.cmdline + " initrd=initrd"; + + Ok(ImagePaths { + fw_path, + kernel_path, + initrd_path, + kernel_cmdline, + }) + } + /// Compute expected TDX measurements for a given VM configuration. + /// + /// This method downloads the OS image if needed (using the configured cache), + /// then computes the expected MRTD and RTMRs based on the VM configuration. + /// Results are cached automatically. + pub async fn compute_measurements_for_config( + &self, + vm_config: &VmConfig, + ) -> Result { + let image_paths = self.ensure_image_downloaded(vm_config).await?; + + self.load_or_compute_measurements( + vm_config, + &image_paths.fw_path, + &image_paths.kernel_path, + &image_paths.initrd_path, + &image_paths.kernel_cmdline, + ) + } + + pub async fn verify(&self, request: VerificationRequest) -> Result { + let attestation = if let Some(attestation) = &request.attestation { + VersionedAttestation::from_scale(attestation).context("Failed to decode attestaion")? + } else if let Some(tdx_quote) = request.quote { + let event_log = request + .event_log + .as_ref() + .context("Event log is required")?; + Attestation::from_tdx_quote(tdx_quote, event_log.as_bytes()) + .context("Failed to create attestation")? + .into_versioned() + } else { + bail!("Quote is required"); + }; let mut details = VerificationDetails { quote_verified: false, event_log_verified: false, @@ -366,41 +407,49 @@ impl CvmVerifier { rtmr_debug: None, }; - let vm_config: VmConfig = - serde_json::from_str(&request.vm_config).context("Failed to decode VM config JSON")?; - - // Step 1: Verify the TDX quote using dcap-qvl - let verified_attestation = match self.verify_quote(attestation, &request.pccs_url).await { + let attestation = attestation.into_inner(); + let debug = request.debug.unwrap_or(false); + let verified = attestation.verify(request.pccs_url.as_deref()).await; + let verified_attestation = match verified { Ok(att) => { details.quote_verified = true; - details.tcb_status = Some(att.report.status.clone()); - details.advisory_ids = att.report.advisory_ids.clone(); - // Extract and store report_data - if let Ok(report_data) = att.decode_report_data() { - details.report_data = Some(hex::encode(report_data)); - } + details.tcb_status = att.report.tdx_report.as_ref().map(|r| r.status.clone()); + details.advisory_ids = att + .report + .tdx_report + .as_ref() + .map(|r| r.advisory_ids.clone()) + .unwrap_or_default(); + details.report_data = Some(hex::encode(att.report_data)); att } Err(e) => { return Ok(VerificationResponse { is_valid: false, details, - reason: Some(format!("Quote verification failed: {}", e)), + reason: Some(format!("Quote verification failed: {e:#}")), }); } }; - // Step 3: Verify os-image-hash matches using dstack-mr - if let Err(e) = self - .verify_os_image_hash(&vm_config, &verified_attestation, debug, &mut details) - .await - { - return Ok(VerificationResponse { - is_valid: false, - details, - reason: Some(format!("OS image hash verification failed: {e:#}")), - }); - } + let verified = self + .verify_os_image_hash( + request.vm_config.clone().unwrap_or_default(), + &verified_attestation, + debug, + &mut details, + ) + .await; + let vm_config = match verified { + Ok(vm_config) => vm_config, + Err(e) => { + return Ok(VerificationResponse { + is_valid: false, + details, + reason: Some(format!("OS image hash verification failed: {e:#}")), + }); + } + }; details.os_image_hash_verified = true; match verified_attestation.decode_app_info(false) { Ok(mut info) => { @@ -424,32 +473,51 @@ impl CvmVerifier { }) } - async fn verify_quote( + pub async fn verify_os_image_hash( &self, - attestation: Attestation, - pccs_url: &Option, - ) -> Result { - // Extract report data from quote - let report_data = attestation.decode_report_data()?; - - attestation - .verify(&report_data, pccs_url.as_deref()) - .await - .context("Quote verification failed") + mut vm_config: String, + attestation: &VerifiedAttestation, + debug: bool, + details: &mut VerificationDetails, + ) -> Result { + if vm_config.is_empty() { + vm_config = attestation.config.clone() + } + let vm_config: VmConfig = + serde_json::from_str(&vm_config).context("Failed to decode VM config JSON")?; + match attestation.mode { + AttestationMode::GcpTdx => { + let Some(tpm_quote) = &attestation.tpm_quote else { + bail!("No TPM quote"); + }; + self.verify_os_image_hash_for_gcp_tdx(&vm_config, tpm_quote) + .await?; + } + AttestationMode::DstackTdx => { + self.verify_os_image_hash_for_dstack_tdx(&vm_config, attestation, debug, details) + .await?; + } + AttestationMode::DstackNitro => bail!("Nitro not supported"), + } + Ok(vm_config) } - async fn verify_os_image_hash( + async fn verify_os_image_hash_for_dstack_tdx( &self, vm_config: &VmConfig, attestation: &VerifiedAttestation, debug: bool, details: &mut VerificationDetails, ) -> Result<()> { - let hex_os_image_hash = hex::encode(&vm_config.os_image_hash); - + let Some(report) = &attestation.report.tdx_report else { + bail!("No TDX report"); + }; + let Some(tdx_quote) = &attestation.tdx_quote else { + bail!("No TDX quote"); + }; + let event_log = &tdx_quote.event_log; // Get boot info from attestation - let report = attestation - .report + let report = report .report .as_td10() .context("Failed to decode TD report")?; @@ -462,35 +530,11 @@ impl CvmVerifier { rtmr2: report.rt_mr2.to_vec(), }; - // Get image directory - let image_dir = Path::new(&self.image_cache_dir) - .join("images") - .join(&hex_os_image_hash); - - let metadata_path = image_dir.join("metadata.json"); - if !metadata_path.exists() { - info!("Image {} not found, downloading", hex_os_image_hash); - tokio::time::timeout( - self.download_timeout, - self.download_image(&hex_os_image_hash, &image_dir), - ) - .await - .context("Download image timeout")? - .with_context(|| format!("Failed to download image {hex_os_image_hash}"))?; - } - - let image_info = - fs_err::read_to_string(metadata_path).context("Failed to read image metadata")?; - let image_info: dstack_types::ImageInfo = - serde_json::from_str(&image_info).context("Failed to parse image metadata")?; - - let fw_path = image_dir.join(&image_info.bios); - let kernel_path = image_dir.join(&image_info.kernel); - let initrd_path = image_dir.join(&image_info.initrd); - let kernel_cmdline = image_info.cmdline + " initrd=initrd"; - - // Use dstack-mr to compute expected MRs + // Compute expected measurements (reusing the public API) let (mrs, expected_logs) = if debug { + // For debug mode, we need detailed logs and ACPI tables + let image_paths = self.ensure_image_downloaded(vm_config).await?; + let TdxMeasurementDetails { measurements, rtmr_logs, @@ -498,10 +542,10 @@ impl CvmVerifier { } = self .compute_measurement_details( vm_config, - &fw_path, - &kernel_path, - &initrd_path, - &kernel_cmdline, + &image_paths.fw_path, + &image_paths.kernel_path, + &image_paths.initrd_path, + &image_paths.kernel_cmdline, ) .context("Failed to compute expected measurements")?; @@ -513,15 +557,11 @@ impl CvmVerifier { (measurements, Some(rtmr_logs)) } else { + // For non-debug mode, reuse the public API with caching ( - self.load_or_compute_measurements( - vm_config, - &fw_path, - &kernel_path, - &initrd_path, - &kernel_cmdline, - ) - .context("Failed to obtain expected measurements")?, + self.compute_measurements_for_config(vm_config) + .await + .context("Failed to compute expected measurements")?, None, ) }; @@ -533,16 +573,6 @@ impl CvmVerifier { rtmr2: mrs.rtmr2.clone(), }; - let event_log: Vec = serde_json::from_slice(&attestation.raw_event_log) - .context("Failed to parse event log for mismatch analysis")?; - - let computation_result = replay_event_logs(&event_log) - .context("Failed to replay event logs for mismatch analysis")?; - - if computation_result.rtmrs[3] != report.rt_mr3 { - bail!("RTMR3 mismatch"); - } - match expected_mrs.assert_eq(&verified_mrs) { Ok(()) => Ok(()), Err(e) => { @@ -561,8 +591,8 @@ impl CvmVerifier { &expected_mrs.rtmr0, &verified_mrs.rtmr0, &expected_logs[0], - &computation_result.event_indices[0], - &event_log, + &[], + event_log, )); } @@ -572,8 +602,8 @@ impl CvmVerifier { &expected_mrs.rtmr1, &verified_mrs.rtmr1, &expected_logs[1], - &computation_result.event_indices[1], - &event_log, + &[], + event_log, )); } @@ -583,8 +613,8 @@ impl CvmVerifier { &expected_mrs.rtmr2, &verified_mrs.rtmr2, &expected_logs[2], - &computation_result.event_indices[2], - &event_log, + &[], + event_log, )); } @@ -597,7 +627,66 @@ impl CvmVerifier { } } - async fn download_image(&self, hex_os_image_hash: &str, dst_dir: &Path) -> Result<()> { + /// Verify GCP TDX image hash using PCR 2 Event Log + /// + /// For GCP TDX, we verify: + /// 1. PCR 0 matches expected GCP OVMF v2 firmware + /// 2. UKI hash by extracting Event 28 (3rd event in PCR 2) from TPM Event Log + /// + /// IMPORTANT: Extracting the 3rd event is GCP OVMF-specific behavior. + /// On GCP, PCR 2 events are ordered as: + /// [0]=EV_SEPARATOR, [1]=EV_EFI_GPT_EVENT, [2]=UKI (Event 28), [3]=Linux kernel + async fn verify_os_image_hash_for_gcp_tdx( + &self, + vm_config: &VmConfig, + tpm_quote: &TpmQuote, + ) -> Result<()> { + // Verify PCR 0 (GCP OVMF firmware) + const EXPECTED_PCR0: [u8; 32] = + hex!("0cca9ec161b09288802e5a112255d21340ed5b797f5fe29cecccfd8f67b9f802"); + + let pcr0 = tpm_quote + .pcr_values + .iter() + .find(|p| p.index == 0) + .context("PCR 0 not found in TPM quote")?; + + // Get expected UKI hash from os_image_hash (which should be set to UKI Authenticode hash) + let expected_uki_hash = &vm_config.os_image_hash; + + let pcr2_events: Vec<_> = tpm_quote + .event_log + .iter() + .filter(|e| e.pcr_index == 2) + .collect(); + debug!("PCR 2 Event Log contains {} events", pcr2_events.len()); + // Extract Event 28 (3rd event, 0-indexed as 2) + // NOTE: This is GCP OVMF-specific behavior + let event_28_digest = { + if pcr0.value != EXPECTED_PCR0 { + bail!( + "PCR 0 mismatch: expected GCP OVMF v2, got {}", + hex::encode(&pcr0.value) + ); + } + &pcr2_events.get(2).context("Event 28 not found")?.digest + }; + + if event_28_digest != expected_uki_hash { + bail!( + "UKI hash mismatch: expected={}, actual={}", + hex::encode(expected_uki_hash), + hex::encode(event_28_digest) + ); + } + debug!( + "✓ UKI hash verified from PCR 2 Event Log (Event 28), digest: {}", + hex::encode(event_28_digest) + ); + Ok(()) + } + + pub async fn download_image(&self, hex_os_image_hash: &str, dst_dir: &Path) -> Result<()> { let url = self .download_url .replace("{OS_IMAGE_HASH}", hex_os_image_hash); @@ -760,27 +849,3 @@ impl Mrs { Ok(()) } } - -mod upgrade_authority { - use serde::{Deserialize, Serialize}; - - #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] - pub struct BootInfo { - pub mrtd: Vec, - pub rtmr0: Vec, - pub rtmr1: Vec, - pub rtmr2: Vec, - pub rtmr3: Vec, - pub mr_aggregated: Vec, - pub os_image_hash: Vec, - pub mr_system: Vec, - pub app_id: Vec, - pub compose_hash: Vec, - pub instance_id: Vec, - pub device_id: Vec, - pub key_provider_info: Vec, - pub event_log: String, - pub tcb_status: String, - pub advisory_ids: Vec, - } -} diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index 84cb5ff6..c5da0b83 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -50,6 +50,8 @@ lspci.workspace = true base64.workspace = true serde-human-bytes.workspace = true size-parser = { workspace = true, features = ["serde"] } +fatfs.workspace = true +fscommon.workspace = true or-panic.workspace = true [dev-dependencies] diff --git a/vmm/src/app.rs b/vmm/src/app.rs index 289ddf7a..1533e6e4 100644 --- a/vmm/src/app.rs +++ b/vmm/src/app.rs @@ -123,7 +123,7 @@ pub struct App { } impl App { - fn lock(&self) -> MutexGuard { + fn lock(&self) -> MutexGuard<'_, AppState> { self.state.lock().or_panic("mutex poisoned") } @@ -831,6 +831,7 @@ fn make_vm_config(cfg: &Config, manifest: &Manifest, image: &Image) -> dstack_ty hugepages: manifest.hugepages, num_gpus: gpus.gpus.len() as u32, num_nvswitches: gpus.bridges.len() as u32, + host_share_mode: cfg.cvm.host_share_mode.clone(), hotplug_off: cfg.cvm.qemu_hotplug_off, image: Some(manifest.image.clone()), } diff --git a/vmm/src/app/qemu.rs b/vmm/src/app/qemu.rs index f2ab2f5f..735ca785 100644 --- a/vmm/src/app/qemu.rs +++ b/vmm/src/app/qemu.rs @@ -22,8 +22,10 @@ use base64::prelude::*; use bon::Builder; use dstack_types::{ mr_config::MrConfig, - shared_filenames::{APP_COMPOSE, ENCRYPTED_ENV, INSTANCE_INFO, USER_CONFIG}, - AppCompose, + shared_filenames::{ + APP_COMPOSE, ENCRYPTED_ENV, HOST_SHARED_DISK_LABEL, INSTANCE_INFO, USER_CONFIG, + }, + AppCompose, KeyProviderKind, }; use dstack_vmm_rpc as pb; use fs_err as fs; @@ -93,6 +95,70 @@ fn create_hd( Ok(()) } +/// Create a FAT32 disk image from a directory +fn create_shared_disk(disk_path: impl AsRef, shared_dir: impl AsRef) -> Result<()> { + use fatfs::{FileSystem, FormatVolumeOptions, FsOptions}; + use std::io::{Cursor, Seek, SeekFrom, Write}; + + let disk_path = disk_path.as_ref(); + let shared_dir = shared_dir.as_ref(); + + const DISK_SIZE: usize = 8 * 1024 * 1024; + let mut disk_data = vec![0u8; DISK_SIZE]; + + { + let cursor = Cursor::new(&mut disk_data); + let mut label_bytes = [b' '; 11]; + let label_str = HOST_SHARED_DISK_LABEL.as_bytes(); + let copy_len = label_str.len().min(11); + label_bytes[..copy_len].copy_from_slice(&label_str[..copy_len]); + let format_opts = FormatVolumeOptions::new() + .fat_type(fatfs::FatType::Fat32) + .volume_label(label_bytes); + fatfs::format_volume(cursor, format_opts).context("Failed to format disk as FAT32")?; + } + + // Open the formatted filesystem in memory and copy files + { + let mut cursor = Cursor::new(&mut disk_data); + cursor + .seek(SeekFrom::Start(0)) + .context("Failed to seek to start")?; + let fs = + FileSystem::new(cursor, FsOptions::new()).context("Failed to open FAT32 filesystem")?; + let root_dir = fs.root_dir(); + + // Copy all files from shared_dir to the FAT32 root + for entry in fs::read_dir(shared_dir).context("Failed to read shared directory")? { + let entry = entry.context("Failed to read directory entry")?; + let path = entry.path(); + + if path.is_file() { + let filename = entry.file_name(); + let filename_str = filename.to_string_lossy(); + + // Read source file + let content = fs::read(&path) + .with_context(|| format!("Failed to read file {}", path.display()))?; + + // Write to FAT32 filesystem + let mut fat_file = root_dir + .create_file(&filename_str) + .with_context(|| format!("Failed to create file {filename_str} in FAT32"))?; + fat_file + .write_all(&content) + .with_context(|| format!("Failed to write file {filename_str} to FAT32"))?; + fat_file.flush().context("Failed to flush FAT32 file")?; + } + } + } + + fs::write(disk_path, &disk_data) + .with_context(|| format!("Failed to write disk image to {}", disk_path.display()))?; + + Ok(()) +} + impl VmInfo { pub fn to_pb(&self, gw: &GatewayConfig, brief: bool) -> pb::VmInfo { let workdir = VmWorkDir::new(&self.workdir); @@ -362,6 +428,7 @@ impl VmConfig { if !shared_dir.exists() { fs::create_dir_all(&shared_dir)?; } + let app_compose = workdir.app_compose().context("Failed to get app compose")?; let qemu = &cfg.qemu_path; let mut smp = self.manifest.vcpu.max(1); let mut mem = self.manifest.memory; @@ -464,21 +531,74 @@ impl VmConfig { command.arg("-netdev").arg(netdev); command.arg("-device").arg("virtio-net-pci,netdev=net0"); - self.configure_machine(&mut command, &workdir, cfg)?; + self.configure_machine(&mut command, &workdir, cfg, &app_compose)?; + self.configure_smbios(&mut command, cfg); + + if matches!(app_compose.key_provider(), KeyProviderKind::Tpm) { + let tpm_path = if Path::new("/dev/tpmrm0").exists() { + "/dev/tpmrm0" + } else if Path::new("/dev/tpm0").exists() { + "/dev/tpm0" + } else { + bail!("TPM key provider requested but no TPM device found on host"); + }; + command + .arg("-tpmdev") + .arg(format!("passthrough,id=tpm0,path={tpm_path}")) + .arg("-device") + .arg("tpm-tis,tpmdev=tpm0"); + } command .arg("-device") .arg(format!("vhost-vsock-pci,guest-cid={}", self.cid)); - let ro = if self.image.info.shared_ro { - "on" - } else { - "off" - }; - command.arg("-virtfs").arg(format!( - "local,path={},mount_tag=host-shared,readonly={ro},security_model=mapped,id=virtfs0", - shared_dir.display(), - )); + // Configure shared files delivery: either via disk or 9p + match cfg.host_share_mode.as_str() { + "9p" => { + // Use 9p virtfs (default) + let ro = if self.image.info.shared_ro { + "on" + } else { + "off" + }; + command.arg("-virtfs").arg(format!( + "local,path={},mount_tag=host-shared,readonly={ro},security_model=mapped,id=virtfs0", + shared_dir.display(), + )); + } + "vvfat" => { + command + .arg("-blockdev") + .arg(format!( + "driver=vvfat,node-name=vvfat0,read-only=on,dir={},label={}", + shared_dir.display(), + HOST_SHARED_DISK_LABEL + )) + .arg("-device") + .arg("virtio-blk-pci,drive=vvfat0"); + } + "vhd" => { + // Use a second virtual disk (hd2) to share files + let shared_disk_path = workdir.shared_disk_path(); + if shared_disk_path.exists() { + fs::remove_file(&shared_disk_path).context("Failed to remove shared disk")?; + } + create_shared_disk(&shared_disk_path, &shared_dir) + .context("Failed to create shared disk")?; + command + .arg("-drive") + .arg(format!( + "file={},if=none,id=hd2,format=raw,readonly=on", + shared_disk_path.display() + )) + .arg("-device") + .arg("virtio-blk-pci,drive=hd2"); + } + _ => { + bail!("Invalid host sharing mode: {}", cfg.host_share_mode); + } + } let hugepages = self.manifest.hugepages; let pin_numa = self.manifest.pin_numa; @@ -658,6 +778,7 @@ impl VmConfig { command: &mut Command, workdir: &VmWorkDir, cfg: &CvmConfig, + app_compose: &AppCompose, ) -> Result<()> { if self.manifest.no_tee { command @@ -672,8 +793,9 @@ impl VmConfig { let img_ver = self.image.info.version_tuple().unwrap_or_default(); let support_mr_config_id = img_ver >= (0, 5, 2); - let tdx_object = if cfg.use_mrconfigid && support_mr_config_id { - let app_compose = workdir.app_compose().context("Failed to get app compose")?; + + // Compute mrconfigid if needed + let mrconfigid = if cfg.use_mrconfigid && support_mr_config_id { let compose_hash = workdir .app_compose_hash() .context("Failed to get compose hash")?; @@ -700,14 +822,94 @@ impl VmConfig { key_provider_id, } }; - let mrconfigid = BASE64_STANDARD.encode(mr_config.to_mr_config_id()); - format!("tdx-guest,id=tdx,mrconfigid={mrconfigid}") + Some(BASE64_STANDARD.encode(mr_config.to_mr_config_id())) } else { - "tdx-guest,id=tdx".to_string() + None }; - command.arg("-object").arg(tdx_object); + + // Build tdx-guest object with optional quote-generation-socket for kernel-level TSM support + #[derive(Serialize)] + struct QgsSocket { + r#type: &'static str, + cid: &'static str, + port: String, + } + + #[derive(Serialize)] + struct TdxGuestObject { + #[serde(rename = "qom-type")] + qom_type: &'static str, + id: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + mrconfigid: Option, + #[serde( + rename = "quote-generation-socket", + skip_serializing_if = "Option::is_none" + )] + quote_generation_socket: Option, + } + + let tdx_object = TdxGuestObject { + qom_type: "tdx-guest", + id: "tdx", + mrconfigid: mrconfigid.clone(), + quote_generation_socket: cfg.qgs_port.map(|port| QgsSocket { + r#type: "vsock", + cid: "2", + port: port.to_string(), + }), + }; + + // Use JSON format when quote-generation-socket is needed, otherwise use simple format + let tdx_object_arg = + serde_json::to_string(&tdx_object).context("failed to serialize tdx-guest object")?; + command.arg("-object").arg(tdx_object_arg); Ok(()) } + + fn configure_smbios(&self, command: &mut Command, cfg: &CvmConfig) { + let p = &cfg.product; + + fn cfg_if(ty: &mut Vec, name: &str, v: &Option) { + if let Some(v) = v { + ty.push(format!("{name}={v}")); + } + } + + let mut types = [const { Vec::new() }; 4]; + // SMBIOS type=0 (BIOS Information) + cfg_if(&mut types[0], "vendor", &p.bios_vendor); + cfg_if(&mut types[0], "version", &p.bios_version); + cfg_if(&mut types[0], "date", &p.bios_date); + cfg_if(&mut types[0], "release", &p.bios_release); + // SMBIOS type=1 (System Information) + cfg_if(&mut types[1], "manufacturer", &p.sys_vendor); + cfg_if(&mut types[1], "product", &p.product_name); + cfg_if(&mut types[1], "version", &p.product_version); + cfg_if(&mut types[1], "serial", &p.product_serial); + cfg_if(&mut types[1], "uuid", &p.product_uuid); + cfg_if(&mut types[1], "family", &p.product_family); + cfg_if(&mut types[1], "sku", &p.product_sku); + // SMBIOS type=2 (Baseboard Information) + cfg_if(&mut types[2], "manufacturer", &p.board_vendor); + cfg_if(&mut types[2], "product", &p.board_name); + cfg_if(&mut types[2], "version", &p.board_version); + cfg_if(&mut types[2], "serial", &p.board_serial); + cfg_if(&mut types[2], "asset", &p.board_asset_tag); + // SMBIOS type=3 (Chassis Information) + cfg_if(&mut types[3], "manufacturer", &p.chassis_vendor); + cfg_if(&mut types[3], "version", &p.chassis_version); + cfg_if(&mut types[3], "serial", &p.chassis_serial); + cfg_if(&mut types[3], "asset", &p.chassis_asset_tag); + + for (i, t) in types.iter().enumerate() { + if !t.is_empty() { + command + .arg("-smbios") + .arg(format!("type={i},{}", t.join(","))); + } + } + } } /// Round up a value to the nearest multiple of another value. @@ -878,6 +1080,10 @@ impl VmWorkDir { self.workdir.join("hda.img") } + pub fn shared_disk_path(&self) -> PathBuf { + self.workdir.join("shared.img") + } + pub fn qmp_socket(&self) -> PathBuf { self.workdir.join("qmp.sock") } diff --git a/vmm/src/config.rs b/vmm/src/config.rs index c4d15395..8a593fc0 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -190,6 +190,52 @@ pub struct CvmConfig { /// Networking configuration pub networking: Networking, + + /// Host sharing mode. (9p, vhd, vvfat) + pub host_share_mode: String, + + /// QGS (Quote Generation Service) vsock port for kernel-level TSM support. + /// When set, QEMU will pass this port to tdx-guest for configfs-tsm quote generation. + /// The guest kernel will use this vsock port to communicate with the host QGS. + /// Default is None (disabled), common value is 4050. + pub qgs_port: Option, + + /// SMBIOS product information for cloud environment detection + #[serde(default)] + pub product: ProductConfig, +} + +/// SMBIOS product information configuration. +/// Field names correspond to /sys/class/dmi/id/ entries in guest. +#[derive(Debug, Clone, Default, Deserialize)] +pub struct ProductConfig { + // SMBIOS type=0 (BIOS Information) + pub bios_vendor: Option, + pub bios_version: Option, + pub bios_date: Option, + pub bios_release: Option, + + // SMBIOS type=1 (System Information) + pub sys_vendor: Option, + pub product_name: Option, + pub product_version: Option, + pub product_serial: Option, + pub product_uuid: Option, + pub product_family: Option, + pub product_sku: Option, + + // SMBIOS type=2 (Baseboard Information) + pub board_vendor: Option, + pub board_name: Option, + pub board_version: Option, + pub board_serial: Option, + pub board_asset_tag: Option, + + // SMBIOS type=3 (Chassis Information) + pub chassis_vendor: Option, + pub chassis_version: Option, + pub chassis_serial: Option, + pub chassis_asset_tag: Option, } #[derive(Debug, Clone, Deserialize)] diff --git a/vmm/src/console_v1.html b/vmm/src/console_v1.html index ab0b54c8..63163772 100644 --- a/vmm/src/console_v1.html +++ b/vmm/src/console_v1.html @@ -2008,11 +2008,24 @@

Deploy a new instance

/>
+
+ + +
+ +
+ + +
+
- - @@ -2023,11 +2036,6 @@

Deploy a new instance

-
- - -
-
@@ -2284,8 +2292,7 @@

Derive VM

encryptedEnvs: [], storage_fs: '', app_id: null, - kms_enabled: true, - local_key_provider_enabled: false, + key_provider: 'kms', key_provider_id: '', gateway_enabled: true, public_logs: true, @@ -2473,10 +2480,19 @@

Derive VM

errorMessage.value = String(err); } } - function configGpu(form) { + function configGpu(form, isUpdate = false) { if (form.attachAllGpus) { return { attach_mode: 'all' }; } + // For updates, always return a config when GPUs are being explicitly updated + // Empty array means no GPUs should be attached + if (isUpdate) { + return { + attach_mode: 'listed', + gpus: (form.selectedGpus || []).map((slot) => ({ slot })), + }; + } + // For creation, return undefined if no GPUs are selected if (form.selectedGpus && form.selectedGpus.length > 0) { return { attach_mode: 'listed', @@ -2715,9 +2731,8 @@

Derive VM

} return 'running'; }; - const kmsEnabled = (vm) => { var _a, _b, _c; return ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.kms_enabled) || ((_c = (_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.features) === null || _c === void 0 ? void 0 : _c.includes('kms')); }; - const gatewayEnabled = (vm) => { var _a, _b, _c, _d; return ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.gateway_enabled) || ((_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.tproxy_enabled) || ((_d = (_c = vm.appCompose) === null || _c === void 0 ? void 0 : _c.features) === null || _d === void 0 ? void 0 : _d.includes('tproxy-net')); }; - const defaultTrue = (v) => (v === undefined ? true : v); + const kmsEnabled = (vm) => getKeyProvider(vm) === 'kms'; + const gatewayEnabled = (vm) => { var _a; return (_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.gateway_enabled; }; function formatMemory(memoryMB) { if (!memoryMB) { return '0 MB'; @@ -2742,17 +2757,24 @@

Derive VM

name: vmForm.value.name, runner: 'docker-compose', docker_compose_file: vmForm.value.dockerComposeFile, - kms_enabled: vmForm.value.kms_enabled, gateway_enabled: vmForm.value.gateway_enabled, public_logs: vmForm.value.public_logs, public_sysinfo: vmForm.value.public_sysinfo, public_tcbinfo: vmForm.value.public_tcbinfo, - local_key_provider_enabled: vmForm.value.local_key_provider_enabled, key_provider_id: vmForm.value.key_provider_id, allowed_envs: vmForm.value.encryptedEnvs.map((env) => env.key), no_instance_id: !vmForm.value.gateway_enabled, secure_time: false, }; + if (vmForm.value.key_provider !== undefined) { + appCompose.key_provider = vmForm.value.key_provider; + if (vmForm.value.key_provider === 'kms') { + appCompose.kms_enabled = true; + } + if (vmForm.value.key_provider === 'local') { + appCompose.local_key_provider_enabled = true; + } + } if (vmForm.value.storage_fs) { appCompose.storage_fs = vmForm.value.storage_fs; } @@ -2768,16 +2790,6 @@

Derive VM

appCompose.launch_token_hash = await calcComposeHash(launchToken.value); } const imgFeatures = imageVersionFeatures(imageVersion(vmForm.value.image)); - if (imgFeatures.compose_version < 2) { - const features = []; - if (vmForm.value.kms_enabled) - features.push('kms'); - if (vmForm.value.gateway_enabled) - features.push('tproxy-net'); - appCompose.features = features; - appCompose.manifest_version = 1; - appCompose.version = '1.0.0'; - } if (imgFeatures.compose_version < 3) { appCompose.tproxy_enabled = appCompose.gateway_enabled; delete appCompose.gateway_enabled; @@ -2813,12 +2825,11 @@

Derive VM

() => vmForm.value.name, () => vmForm.value.dockerComposeFile, () => vmForm.value.preLaunchScript, - () => vmForm.value.kms_enabled, () => vmForm.value.gateway_enabled, () => vmForm.value.public_logs, () => vmForm.value.public_sysinfo, () => vmForm.value.public_tcbinfo, - () => vmForm.value.local_key_provider_enabled, + () => vmForm.value.key_provider, () => vmForm.value.key_provider_id, () => vmForm.value.encryptedEnvs, () => vmForm.value.storage_fs, @@ -2948,7 +2959,7 @@

Derive VM

try { vmForm.value.memory = convertMemoryToMB(vmForm.value.memoryValue, vmForm.value.memoryUnit); const composeFile = await makeAppComposeFile(); - const encryptedEnv = await encryptEnv(vmForm.value.encryptedEnvs, vmForm.value.kms_enabled, vmForm.value.app_id); + const encryptedEnv = await encryptEnv(vmForm.value.encryptedEnvs, vmForm.value.key_provider === 'kms', vmForm.value.app_id); const payload = buildCreateVmPayload({ name: vmForm.value.name, image: vmForm.value.image, @@ -3040,7 +3051,7 @@

Derive VM

body.user_config = updated.user_config; body.update_ports = true; body.ports = normalizePorts(updated.ports); - body.gpus = updateDialog.value.updateGpuConfig ? configGpu(updated) : undefined; + body.gpus = updateDialog.value.updateGpuConfig ? configGpu(updated, true) : undefined; await vmmRpc.updateVm(body); updateDialog.value.encryptedEnvs = []; updateDialog.value.show = false; @@ -3054,8 +3065,21 @@

Derive VM

alert('failed to upgrade VM'); } } + function getKeyProvider(vm) { + var _a, _b, _c, _d; + if ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.key_provider) { + return (_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.key_provider; + } + if ((_c = vm.appCompose) === null || _c === void 0 ? void 0 : _c.kms_enabled) { + return 'kms'; + } + if ((_d = vm.appCompose) === null || _d === void 0 ? void 0 : _d.local_key_provider_enabled) { + return 'local'; + } + return 'none'; + } async function showCloneConfig(vm) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; const theVm = await ensureVmDetails(vm); if (!((_a = theVm === null || theVm === void 0 ? void 0 : theVm.configuration) === null || _a === void 0 ? void 0 : _a.compose_file)) { alert('Compose file not available for this VM. Please open its details first.'); @@ -3064,7 +3088,7 @@

Derive VM

const config = theVm.configuration; // Populate vmForm with current VM data, but clear envs and ports vmForm.value = { - name: `${config.name || vm.name}-cloned`, + name: `${config.name || vm.name}`, image: config.image || '', dockerComposeFile: ((_b = theVm.appCompose) === null || _b === void 0 ? void 0 : _b.docker_compose_file) || '', preLaunchScript: ((_c = theVm.appCompose) === null || _c === void 0 ? void 0 : _c.pre_launch_script) || '', @@ -3082,15 +3106,14 @@

Derive VM

ports: [], // Clear port mappings storage_fs: ((_g = theVm.appCompose) === null || _g === void 0 ? void 0 : _g.storage_fs) || 'ext4', app_id: config.app_id || '', - kms_enabled: !!((_h = theVm.appCompose) === null || _h === void 0 ? void 0 : _h.kms_enabled), kms_urls: config.kms_urls || [], - local_key_provider_enabled: !!((_j = theVm.appCompose) === null || _j === void 0 ? void 0 : _j.local_key_provider_enabled), - key_provider_id: ((_k = theVm.appCompose) === null || _k === void 0 ? void 0 : _k.key_provider_id) || '', - gateway_enabled: !!((_l = theVm.appCompose) === null || _l === void 0 ? void 0 : _l.gateway_enabled), + key_provider: getKeyProvider(theVm), + key_provider_id: ((_h = theVm.appCompose) === null || _h === void 0 ? void 0 : _h.key_provider_id) || '', + gateway_enabled: !!((_j = theVm.appCompose) === null || _j === void 0 ? void 0 : _j.gateway_enabled), gateway_urls: config.gateway_urls || [], - public_logs: !!((_m = theVm.appCompose) === null || _m === void 0 ? void 0 : _m.public_logs), - public_sysinfo: !!((_o = theVm.appCompose) === null || _o === void 0 ? void 0 : _o.public_sysinfo), - public_tcbinfo: !!((_p = theVm.appCompose) === null || _p === void 0 ? void 0 : _p.public_tcbinfo), + public_logs: !!((_k = theVm.appCompose) === null || _k === void 0 ? void 0 : _k.public_logs), + public_sysinfo: !!((_l = theVm.appCompose) === null || _l === void 0 ? void 0 : _l.public_sysinfo), + public_tcbinfo: !!((_m = theVm.appCompose) === null || _m === void 0 ? void 0 : _m.public_tcbinfo), pin_numa: !!config.pin_numa, hugepages: !!config.hugepages, no_tee: !!config.no_tee, @@ -3398,24 +3421,20 @@

Derive VM

} } function getVmFeatures(vm) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; + var _a, _b, _c, _d; const features = []; - // Check KMS - const kmsEnabled = ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.kms_enabled) || ((_c = (_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.features) === null || _c === void 0 ? void 0 : _c.includes('kms')) || - ((_e = (_d = vm.configuration) === null || _d === void 0 ? void 0 : _d.kms_urls) === null || _e === void 0 ? void 0 : _e.length) > 0; - if (kmsEnabled) - features.push("kms"); + const kp = getKeyProvider(vm); + if (kp && kp != 'none') + features.push(kp); // Check Gateway/TProxy - const gatewayEnabled = ((_f = vm.appCompose) === null || _f === void 0 ? void 0 : _f.gateway_enabled) || ((_g = vm.appCompose) === null || _g === void 0 ? void 0 : _g.tproxy_enabled) || - ((_j = (_h = vm.appCompose) === null || _h === void 0 ? void 0 : _h.features) === null || _j === void 0 ? void 0 : _j.includes('tproxy-net')) || ((_l = (_k = vm.configuration) === null || _k === void 0 ? void 0 : _k.gateway_urls) === null || _l === void 0 ? void 0 : _l.length) > 0; - if (gatewayEnabled) + if ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.gateway_enabled) features.push("gateway"); // Check other features from appCompose - if ((_m = vm.appCompose) === null || _m === void 0 ? void 0 : _m.public_logs) + if ((_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.public_logs) features.push("logs"); - if ((_o = vm.appCompose) === null || _o === void 0 ? void 0 : _o.public_sysinfo) + if ((_c = vm.appCompose) === null || _c === void 0 ? void 0 : _c.public_sysinfo) features.push("sysinfo"); - if ((_p = vm.appCompose) === null || _p === void 0 ? void 0 : _p.public_tcbinfo) + if ((_d = vm.appCompose) === null || _d === void 0 ? void 0 : _d.public_tcbinfo) features.push("tcbinfo"); return features.length > 0 ? features.join(', ') : 'None'; } diff --git a/vmm/src/vmm-cli.py b/vmm/src/vmm-cli.py index 7bf4b3fa..44cda20d 100755 --- a/vmm/src/vmm-cli.py +++ b/vmm/src/vmm-cli.py @@ -521,6 +521,8 @@ def create_app_compose(self, args) -> None: "no_instance_id": args.no_instance_id, "secure_time": args.secure_time, } + if args.key_provider: + app_compose["key_provider"] = args.key_provider if args.prelaunch_script: app_compose["pre_launch_script"] = open( args.prelaunch_script, 'rb').read().decode('utf-8') @@ -1160,6 +1162,9 @@ def main(): '--gateway', action='store_true', help='Enable dstack-gateway') compose_parser.add_argument( '--local-key-provider', action='store_true', help='Enable local key provider') + compose_parser.add_argument( + '--key-provider', choices=['none', 'kms', 'local', 'tpm'], default=None, + help='Override key provider type (none, kms, local, or tpm)') compose_parser.add_argument( '--key-provider-id', default=None, help='Key provider ID if you want to bind to a specific key provider') compose_parser.add_argument( diff --git a/vmm/ui/src/components/CreateVmDialog.ts b/vmm/ui/src/components/CreateVmDialog.ts index 21d2fa99..8f13d36d 100644 --- a/vmm/ui/src/components/CreateVmDialog.ts +++ b/vmm/ui/src/components/CreateVmDialog.ts @@ -123,11 +123,24 @@ const CreateVmDialogComponent = { /> +
+ + +
+ +
+ + +
+
- - @@ -138,11 +151,6 @@ const CreateVmDialogComponent = {
-
- - -
-
diff --git a/vmm/ui/src/composables/useVmManager.ts b/vmm/ui/src/composables/useVmManager.ts index ac598136..d7553420 100644 --- a/vmm/ui/src/composables/useVmManager.ts +++ b/vmm/ui/src/composables/useVmManager.ts @@ -32,7 +32,7 @@ type AppCompose = { pre_launch_script?: string; }; -type KeyProviderKind = 'none' | 'kms' | 'local'; +type KeyProviderKind = 'none' | 'kms' | 'local' | 'tpm'; const x25519 = require('../lib/x25519.js'); const { getVmmRpcClient } = require('../lib/vmmRpcClient'); @@ -95,8 +95,7 @@ type VmFormState = { encryptedEnvs: EncryptedEnvEntry[]; storage_fs: string; app_id: string | null; - kms_enabled: boolean; - local_key_provider_enabled: boolean; + key_provider?: KeyProviderKind; key_provider_id: string; gateway_enabled: boolean; public_logs: boolean; @@ -176,8 +175,7 @@ function createVmFormState(preLaunchScript: string): VmFormState { encryptedEnvs: [], storage_fs: '', app_id: null, - kms_enabled: true, - local_key_provider_enabled: false, + key_provider: 'kms', key_provider_id: '', gateway_enabled: true, public_logs: true, @@ -679,12 +677,9 @@ type CreateVmPayloadSource = { return 'running'; }; - const kmsEnabled = (vm: any) => vm.appCompose?.kms_enabled || vm.appCompose?.features?.includes('kms'); - - const gatewayEnabled = (vm: any) => - vm.appCompose?.gateway_enabled || vm.appCompose?.tproxy_enabled || vm.appCompose?.features?.includes('tproxy-net'); + const kmsEnabled = (vm: any) => getKeyProvider(vm) === 'kms'; - const defaultTrue = (v: boolean | undefined) => (v === undefined ? true : v); + const gatewayEnabled = (vm: any) => vm.appCompose?.gateway_enabled; function formatMemory(memoryMB?: number) { if (!memoryMB) { @@ -711,18 +706,28 @@ type CreateVmPayloadSource = { name: vmForm.value.name, runner: 'docker-compose', docker_compose_file: vmForm.value.dockerComposeFile, - kms_enabled: vmForm.value.kms_enabled, gateway_enabled: vmForm.value.gateway_enabled, public_logs: vmForm.value.public_logs, public_sysinfo: vmForm.value.public_sysinfo, public_tcbinfo: vmForm.value.public_tcbinfo, - local_key_provider_enabled: vmForm.value.local_key_provider_enabled, key_provider_id: vmForm.value.key_provider_id, allowed_envs: vmForm.value.encryptedEnvs.map((env) => env.key), no_instance_id: !vmForm.value.gateway_enabled, secure_time: false, }; + if (vmForm.value.key_provider !== undefined) { + appCompose.key_provider = vmForm.value.key_provider; + + // For backward compatibility + if (vmForm.value.key_provider === 'kms') { + appCompose.kms_enabled = true; + } + if (vmForm.value.key_provider === 'local') { + appCompose.local_key_provider_enabled = true; + } + } + if (vmForm.value.storage_fs) { appCompose.storage_fs = vmForm.value.storage_fs; } @@ -742,14 +747,6 @@ type CreateVmPayloadSource = { } const imgFeatures = imageVersionFeatures(imageVersion(vmForm.value.image)); - if (imgFeatures.compose_version < 2) { - const features: string[] = []; - if (vmForm.value.kms_enabled) features.push('kms'); - if (vmForm.value.gateway_enabled) features.push('tproxy-net'); - appCompose.features = features; - appCompose.manifest_version = 1; - appCompose.version = '1.0.0'; - } if (imgFeatures.compose_version < 3) { appCompose.tproxy_enabled = appCompose.gateway_enabled; delete appCompose.gateway_enabled; @@ -788,12 +785,11 @@ type CreateVmPayloadSource = { () => vmForm.value.name, () => vmForm.value.dockerComposeFile, () => vmForm.value.preLaunchScript, - () => vmForm.value.kms_enabled, () => vmForm.value.gateway_enabled, () => vmForm.value.public_logs, () => vmForm.value.public_sysinfo, () => vmForm.value.public_tcbinfo, - () => vmForm.value.local_key_provider_enabled, + () => vmForm.value.key_provider, () => vmForm.value.key_provider_id, () => vmForm.value.encryptedEnvs, () => vmForm.value.storage_fs, @@ -952,7 +948,7 @@ type CreateVmPayloadSource = { const composeFile = await makeAppComposeFile(); const encryptedEnv = await encryptEnv( vmForm.value.encryptedEnvs, - vmForm.value.kms_enabled, + vmForm.value.key_provider === 'kms', vmForm.value.app_id, ); const payload = buildCreateVmPayload({ @@ -1066,6 +1062,19 @@ type CreateVmPayloadSource = { } } + function getKeyProvider(vm: VmListItem): KeyProviderKind { + if (vm.appCompose?.key_provider) { + return vm.appCompose?.key_provider; + } + if (vm.appCompose?.kms_enabled) { + return 'kms'; + } + if (vm.appCompose?.local_key_provider_enabled) { + return 'local'; + } + return 'none'; + } + async function showCloneConfig(vm: VmListItem) { const theVm = await ensureVmDetails(vm); if (!theVm?.configuration?.compose_file) { @@ -1076,7 +1085,7 @@ type CreateVmPayloadSource = { // Populate vmForm with current VM data, but clear envs and ports vmForm.value = { - name: `${config.name || vm.name}-cloned`, + name: `${config.name || vm.name}`, image: config.image || '', dockerComposeFile: theVm.appCompose?.docker_compose_file || '', preLaunchScript: theVm.appCompose?.pre_launch_script || '', @@ -1092,11 +1101,10 @@ type CreateVmPayloadSource = { attachAllGpus: false, encryptedEnvs: [], // Clear environment variables ports: [], // Clear port mappings - storage_fs: theVm.appCompose?.storage_fs || 'ext4', + storage_fs: theVm.appCompose?.storage_fs || 'zfs', app_id: config.app_id || '', - kms_enabled: !!theVm.appCompose?.kms_enabled, kms_urls: config.kms_urls || [], - local_key_provider_enabled: !!theVm.appCompose?.local_key_provider_enabled, + key_provider: getKeyProvider(theVm), key_provider_id: theVm.appCompose?.key_provider_id || '', gateway_enabled: !!theVm.appCompose?.gateway_enabled, gateway_urls: config.gateway_urls || [], @@ -1435,15 +1443,11 @@ type CreateVmPayloadSource = { function getVmFeatures(vm: VmListItem) { const features = []; - // Check KMS - const kmsEnabled = vm.appCompose?.kms_enabled || vm.appCompose?.features?.includes('kms') || - vm.configuration?.kms_urls?.length > 0; - if (kmsEnabled) features.push("kms"); + const kp = getKeyProvider(vm); + if (kp && kp != 'none') features.push(kp); // Check Gateway/TProxy - const gatewayEnabled = vm.appCompose?.gateway_enabled || vm.appCompose?.tproxy_enabled || - vm.appCompose?.features?.includes('tproxy-net') || vm.configuration?.gateway_urls?.length > 0; - if (gatewayEnabled) features.push("gateway"); + if (vm.appCompose?.gateway_enabled) features.push("gateway"); // Check other features from appCompose if (vm.appCompose?.public_logs) features.push("logs"); diff --git a/vmm/vmm.toml b/vmm/vmm.toml index f4d504e0..1f14e13f 100644 --- a/vmm/vmm.toml +++ b/vmm/vmm.toml @@ -38,6 +38,45 @@ use_mrconfigid = true qemu_pci_hole64_size = 0 qemu_hotplug_off = false +host_share_mode = "vvfat" + +# QGS (Quote Generation Service) vsock port for kernel-level TSM support. +# When set, QEMU will pass this port to tdx-guest for configfs-tsm quote generation. +# The guest kernel will use this vsock port to communicate with the host QGS. +# Default is None (disabled), common value is 4050. +qgs_port = 4050 + +# SMBIOS product information for cloud environment detection. +# Field names correspond to /sys/class/dmi/id/ entries in guest. +[cvm.product] +## type=0 (BIOS Information) +# bios_vendor = "" +# bios_version = "" +# bios_date = "" +# bios_release = "" + +## type=1 (System Information) +sys_vendor = "dstack" +product_name = "dstack" +# product_version = "" +# product_serial = "" +# product_uuid = "" +# product_family = "" +# product_sku = "" + +## type=2 (Baseboard Information) +# board_vendor = "dstack" +# board_name = "dstack" +# board_version = "" +# board_serial = "" +# board_asset_tag = "" + +# type=3 (Chassis Information) +# chassis_vendor = "" +# chassis_version = "" +# chassis_serial = "" +# chassis_asset_tag = "" + [cvm.networking] mode = "user"