diff --git a/Cargo.lock b/Cargo.lock index 66e2c576..e9083251 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.33" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cf437c87b77a13a61b7d36b18a4e491a365244fd9853b602f99798b09abe9f" +checksum = "f07655fedc35188f3c50ff8fc6ee45703ae14ef1bc7ae7d80e23a747012184e3" dependencies = [ "alloy-core", "alloy-signer", @@ -104,9 +83,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b151e38e42f1586a01369ec52a6934702731d07e8509a7307331b09f6c46dc" +checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" dependencies = [ "alloy-eips", "alloy-primitives", @@ -115,6 +94,7 @@ dependencies = [ "alloy-trie", "alloy-tx-macros", "auto_impl", + "borsh", "c-kzg", "derive_more 2.1.1", "either", @@ -125,14 +105,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-consensus-any" -version = "1.0.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2d5e8668ef6215efdb7dcca6f22277b4e483a5650e05f5de22b2350971f4b8" +checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" dependencies = [ "alloy-consensus", "alloy-eips", @@ -144,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", ] @@ -161,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.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5434834adaf64fa20a6fb90877bc1d33214c41b055cc49f82189c98614368cc" +checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -200,20 +182,21 @@ dependencies = [ "alloy-rlp", "alloy-serde", "auto_impl", + "borsh", "c-kzg", "derive_more 2.1.1", "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", @@ -223,24 +206,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c69f6c9c68a1287c9d5ff903d0010726934de0dac10989be37b75a29190d55" +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.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf2ae05219e73e0979cb2cf55612aafbab191d130f203079805eaf881cca58" +checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -259,14 +242,14 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-network-primitives" -version = "1.0.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58f4f345cef483eab7374f2b6056973c7419ffe8ad35e994b7a7f5d8e0c7ba4" +checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" dependencies = [ "alloy-consensus", "alloy-eips", @@ -277,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.1.1", - "foldhash", - "hashbrown 0.15.5", - "indexmap 2.10.0", + "foldhash 0.2.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "itoa", "k256", "keccak-asm", @@ -296,7 +279,7 @@ dependencies = [ "proptest", "rand 0.9.2", "ruint", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "sha3", "tiny-keccak", @@ -321,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.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbde0801a32d21c5f111f037bee7e22874836fba7add34ed4a6919932dd7cf23" +checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -337,9 +320,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "361cd87ead4ba7659bda8127902eda92d17fa7ceb18aba1676f7be10f7222487" +checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -353,14 +336,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-serde" -version = "1.0.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64600fc6c312b7e0ba76f73a381059af044f4f21f43e07f51f1fa76c868fe302" +checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" dependencies = [ "alloy-primitives", "serde", @@ -369,9 +352,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5772858492b26f780468ae693405f895d6a27dea6e3eab2c36b6217de47c2647" +checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" dependencies = [ "alloy-primitives", "async-trait", @@ -379,14 +362,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-signer-local" -version = "1.0.41" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4195b803d0a992d8dbaab2ca1986fc86533d4bc80967c0cce7668b26ad99ef9" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" dependencies = [ "alloy-consensus", "alloy-network", @@ -395,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", @@ -442,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", @@ -458,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", @@ -486,14 +469,14 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.3.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99dac443033e83b14f68fac56e8c27e76421f1253729574197ceccd06598f3ef" +checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" dependencies = [ - "darling 0.21.2", + "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -507,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", @@ -522,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" @@ -537,22 +520,22 @@ 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]] @@ -599,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" @@ -619,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" @@ -644,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" @@ -665,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" @@ -685,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" @@ -736,7 +784,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -748,7 +796,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -776,7 +824,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -787,7 +835,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -813,7 +861,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -824,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", @@ -835,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", @@ -848,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", @@ -864,8 +911,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "sync_wrapper", "tower", "tower-layer", @@ -874,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", @@ -885,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" @@ -926,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" @@ -954,36 +984,13 @@ 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.10.5", - "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", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -992,9 +999,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1014,15 +1021,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", @@ -1036,9 +1043,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" @@ -1069,7 +1076,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]] @@ -1092,9 +1112,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", @@ -1127,7 +1147,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.15", + "thiserror 2.0.17", "tokio", "tokio-util", "tower-service", @@ -1148,9 +1168,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", @@ -1158,17 +1178,17 @@ 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]] @@ -1191,14 +1211,14 @@ dependencies = [ "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", @@ -1219,9 +1239,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" @@ -1231,18 +1251,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", @@ -1255,10 +1275,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", @@ -1269,6 +1290,8 @@ name = "cc-eventlog" version = "0.5.5" dependencies = [ "anyhow", + "digest 0.10.7", + "ez-hash", "fs-err", "hex", "insta", @@ -1289,7 +1312,6 @@ dependencies = [ "ra-rpc", "ra-tls", "serde_json", - "tdx-attest", ] [[package]] @@ -1328,7 +1350,7 @@ dependencies = [ "rustls", "serde", "tokio", - "toml_edit", + "toml_edit 0.22.27", "tracing-subscriber", ] @@ -1343,9 +1365,9 @@ dependencies = [ [[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" @@ -1400,9 +1422,9 @@ dependencies = [ [[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", @@ -1410,9 +1432,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", @@ -1422,21 +1444,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" @@ -1470,7 +1492,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1493,15 +1515,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]] @@ -1512,9 +1533,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", ] @@ -1536,6 +1557,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" @@ -1611,9 +1638,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", ] @@ -1708,9 +1735,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", @@ -1791,48 +1818,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", @@ -1840,29 +1843,18 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.20.11", + "darling_core", "quote", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce154b9bea7fb0c8e8326e62d00354000c36e79770ff21b8c84e3aa267d9d531" -dependencies = [ - "darling_core 0.21.2", - "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1876,7 +1868,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -1982,17 +1974,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]] @@ -2033,7 +2025,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "unicode-xid", ] @@ -2047,7 +2039,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.106", + "syn 2.0.111", "unicode-xid", ] @@ -2077,11 +2069,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]] @@ -2123,7 +2115,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2134,7 +2126,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2156,7 +2148,7 @@ checksum = "ed6b3e31251e87acd1b74911aed84071c8364fc9087972748ade2f1094ccce34" dependencies = [ "documented-macros", "phf", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -2171,7 +2163,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", + "futures", + "hex", + "hex_fmt", + "insta", + "or-panic", + "parity-scale-codec", + "serde", + "serde-human-bytes", + "serde_json", + "sha2 0.10.9", + "sha3", + "tdx-attest", + "tokio", ] [[package]] @@ -2199,7 +2216,7 @@ dependencies = [ "ipnet", "jemallocator", "load_config", - "nix", + "nix 0.29.0", "or-panic", "parcelona", "pin-project", @@ -2243,11 +2260,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", @@ -2307,6 +2326,7 @@ dependencies = [ "dstack-kms-rpc", "dstack-mr", "dstack-types", + "dstack-verifier", "fs-err", "git-version", "hex", @@ -2361,7 +2381,7 @@ dependencies = [ "flate2", "fs-err", "hex", - "hex-literal 1.0.0", + "hex-literal 1.1.0", "log", "object", "parity-scale-codec", @@ -2371,7 +2391,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tar", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -2429,6 +2449,7 @@ dependencies = [ name = "dstack-types" version = "0.5.5" dependencies = [ + "parity-scale-codec", "serde", "serde-human-bytes", "sha3", @@ -2442,16 +2463,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", @@ -2468,17 +2492,18 @@ dependencies = [ "serde", "serde-human-bytes", "serde_json", - "serde_yaml2", "sha2 0.10.9", "sha3", "sodiumbox", "tdx-attest", + "tempfile", "tokio", "toml", "tracing", "tracing-subscriber", "x25519-dalek", "x509-parser", + "yaml-rust2", ] [[package]] @@ -2491,13 +2516,16 @@ dependencies = [ "dcap-qvl", "dstack-mr", "dstack-types", + "ez-hash", "figment", "fs-err", "hex", + "hex-literal 1.1.0", "ra-tls", "reqwest", "rocket", "serde", + "serde-human-bytes", "serde_json", "sha2 0.10.9", "tempfile", @@ -2518,7 +2546,9 @@ dependencies = [ "dstack-kms-rpc", "dstack-types", "dstack-vmm-rpc", + "fatfs", "fs-err", + "fscommon", "git-version", "guest-api", "hex", @@ -2618,6 +2648,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" @@ -2672,7 +2714,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]] @@ -2684,7 +2746,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2714,12 +2776,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]] @@ -2761,6 +2838,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" @@ -2804,6 +2893,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" @@ -2836,9 +2931,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", @@ -2856,20 +2951,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", ] @@ -2880,6 +2981,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" @@ -2951,7 +3061,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3003,20 +3113,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" @@ -3054,15 +3150,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", ] @@ -3086,12 +3182,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" @@ -3109,7 +3199,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3154,7 +3244,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.10.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -3192,30 +3282,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]] @@ -3244,15 +3340,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", ] @@ -3265,9 +3358,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" @@ -3317,7 +3410,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.15", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -3336,7 +3429,7 @@ dependencies = [ "ipconfig", "lru-cache", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "resolv-conf", "smallvec", @@ -3357,11 +3450,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", ] @@ -3396,11 +3489,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]] @@ -3418,12 +3511,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", ] @@ -3508,19 +3600,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", @@ -3528,6 +3621,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3568,9 +3662,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", @@ -3584,7 +3678,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -3607,9 +3701,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", @@ -3617,7 +3711,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -3631,9 +3725,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", @@ -3644,9 +3738,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", @@ -3657,11 +3751,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", @@ -3672,42 +3765,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", @@ -3723,9 +3812,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", @@ -3759,7 +3848,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3775,13 +3864,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]] @@ -3796,7 +3886,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", ] @@ -3821,9 +3911,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", @@ -3870,17 +3960,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" @@ -3917,9 +3996,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", @@ -3927,20 +4006,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" @@ -3997,11 +4076,11 @@ 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", ] @@ -4085,26 +4164,20 @@ 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" - [[package]] name = "libc" -version = "0.2.175" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-link 0.2.1", ] [[package]] @@ -4115,13 +4188,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]] @@ -4138,15 +4211,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" @@ -4160,11 +4233,10 @@ 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", ] @@ -4181,7 +4253,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", @@ -4189,19 +4261,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" @@ -4255,7 +4314,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4273,6 +4332,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" @@ -4281,9 +4350,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" @@ -4335,6 +4404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -4352,14 +4422,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]] @@ -4373,20 +4443,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", ] @@ -4479,7 +4548,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", @@ -4509,13 +4590,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", @@ -4551,7 +4632,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4611,9 +4692,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -4621,21 +4702,21 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[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", @@ -4647,18 +4728,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", @@ -4696,9 +4777,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" @@ -4720,7 +4801,7 @@ checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4737,12 +4818,12 @@ 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]] @@ -4776,7 +4857,7 @@ checksum = "89ec0a2252bc3809594c903cc8c1b83cbccaba85b11d4728a43a681263f6c132" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4804,7 +4885,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4820,12 +4901,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]] @@ -4844,15 +4925,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]] @@ -4908,17 +4989,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]] @@ -4932,18 +5013,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", ] @@ -4954,7 +5034,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]] @@ -4964,7 +5044,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]] @@ -4997,7 +5077,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5026,7 +5106,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5082,9 +5162,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", ] @@ -5106,12 +5186,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]] @@ -5136,11 +5216,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]] @@ -5186,14 +5266,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", ] @@ -5206,21 +5286,20 @@ 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", @@ -5287,7 +5366,7 @@ dependencies = [ "prost 0.13.5", "prost-types 0.13.5", "regex", - "syn 2.0.106", + "syn 2.0.111", "tempfile", ] @@ -5314,7 +5393,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5373,7 +5452,7 @@ dependencies = [ "prost-build 0.9.0", "prost-types 0.13.5", "quote", - "syn 2.0.106", + "syn 2.0.111", "template-quote", ] @@ -5384,7 +5463,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]] @@ -5395,19 +5474,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", @@ -5415,20 +5494,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", @@ -5436,23 +5515,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", ] @@ -5490,13 +5569,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", @@ -5505,7 +5590,6 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sha3", - "tdx-attest", "tracing", "x509-parser", "yasna", @@ -5607,7 +5691,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", ] @@ -5654,11 +5738,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]] @@ -5669,27 +5753,27 @@ 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]] @@ -5700,9 +5784,9 @@ checksum = "09c30c54dffee5b40af088d5d50aa3455c91a0127164b51f0215efc4cb28fb3c" [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -5712,9 +5796,9 @@ dependencies = [ [[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", @@ -5723,15 +5807,15 @@ dependencies = [ [[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.28" +version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64 0.22.1", "bytes", @@ -5773,9 +5857,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" @@ -5832,9 +5916,9 @@ dependencies = [ "proc-macro2", "quote", "rinja_parser", - "rustc-hash 2.1.1", + "rustc-hash", "serde", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5861,7 +5945,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", @@ -5874,12 +5958,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", @@ -5925,7 +6009,7 @@ dependencies = [ "pin-project", "rocket", "serde", - "thiserror 2.0.15", + "thiserror 2.0.17", "tokio", "tokio-vsock", ] @@ -5933,15 +6017,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", ] @@ -5949,11 +6033,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", @@ -5968,13 +6052,14 @@ dependencies = [ [[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", @@ -5988,7 +6073,7 @@ dependencies = [ "rand 0.9.2", "rlp", "ruint-macro", - "serde", + "serde_core", "valuable", "zeroize", ] @@ -6007,22 +6092,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" @@ -6050,7 +6123,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]] @@ -6068,7 +6141,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", @@ -6077,38 +6150,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", @@ -6127,9 +6200,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", @@ -6148,9 +6221,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", @@ -6166,9 +6239,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", @@ -6193,9 +6266,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", @@ -6204,9 +6277,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", @@ -6228,9 +6301,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", @@ -6250,9 +6323,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", @@ -6276,28 +6349,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", @@ -6305,14 +6378,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", @@ -6370,16 +6443,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]] @@ -6396,9 +6469,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", @@ -6493,11 +6566,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", @@ -6506,9 +6579,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", @@ -6525,9 +6598,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" @@ -6566,21 +6639,23 @@ 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]] @@ -6600,7 +6675,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6620,7 +6695,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "itoa", "memchr", "serde", @@ -6647,7 +6722,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6673,19 +6748,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", @@ -6693,35 +6767,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]] @@ -6828,9 +6902,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", ] @@ -6845,6 +6919,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" @@ -6864,7 +6944,7 @@ dependencies = [ "anyhow", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -6894,12 +6974,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]] @@ -6941,9 +7021,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" @@ -6951,7 +7031,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" dependencies = [ - "loom 0.5.6", + "loom", ] [[package]] @@ -6993,7 +7073,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7014,7 +7094,7 @@ dependencies = [ "git-version", "libc", "load_config", - "nix", + "nix 0.29.0", "notify", "or-panic", "rocket", @@ -7060,9 +7140,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", @@ -7071,14 +7151,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]] @@ -7098,7 +7178,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7185,28 +7265,28 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tdx-attest-sys", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "tdx-attest-sys" version = "0.5.5" dependencies = [ - "bindgen 0.71.1", + "bindgen", "cc", ] [[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]] @@ -7251,11 +7331,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]] @@ -7266,18 +7346,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]] @@ -7300,9 +7380,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", @@ -7315,15 +7395,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", @@ -7340,9 +7420,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", @@ -7350,9 +7430,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", ] @@ -7365,40 +7445,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", @@ -7417,9 +7494,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", @@ -7449,8 +7526,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -7462,20 +7539,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" @@ -7503,7 +7610,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -7529,9 +7636,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.44" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -7546,14 +7653,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "tracing-core" -version = "0.1.36" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -7579,7 +7686,7 @@ dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "regex-automata", "sharded-slab", "smallvec", @@ -7607,9 +7714,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" @@ -7662,9 +7769,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" @@ -7702,13 +7809,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]] @@ -7731,11 +7839,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", ] @@ -7760,12 +7868,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]] @@ -7818,12 +7926,12 @@ 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]] @@ -7871,7 +7979,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -7906,9 +8014,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", ] @@ -7933,15 +8041,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" @@ -7961,11 +8069,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]] @@ -7990,7 +8098,7 @@ 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 0.1.3", "windows-numerics", @@ -8002,7 +8110,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]] @@ -8014,8 +8122,21 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result", - "windows-strings", + "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]] @@ -8024,31 +8145,31 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", + "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]] @@ -8069,7 +8190,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", ] @@ -8082,6 +8203,15 @@ dependencies = [ "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]] name = "windows-strings" version = "0.4.2" @@ -8091,6 +8221,15 @@ dependencies = [ "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]] name = "windows-sys" version = "0.48.0" @@ -8124,7 +8263,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]] @@ -8160,19 +8308,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 0.1.3", - "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]] @@ -8198,9 +8346,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" @@ -8216,9 +8364,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" @@ -8234,9 +8382,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" @@ -8246,9 +8394,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" @@ -8264,9 +8412,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" @@ -8282,9 +8430,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" @@ -8300,9 +8448,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" @@ -8318,15 +8466,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", ] @@ -8348,19 +8496,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" @@ -8419,7 +8564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.0.8", + "rustix 1.1.2", ] [[package]] @@ -8447,9 +8592,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", @@ -8476,11 +8621,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", @@ -8488,34 +8632,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]] @@ -8535,15 +8679,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", ] @@ -8556,14 +8700,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", @@ -8572,9 +8716,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", @@ -8583,13 +8727,13 @@ 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", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 81e14d25..d0c07582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "ra-tls", "tdx-attest-sys", "tdx-attest", + "dstack-attest", "dstack-util", "iohash", "guest-agent", @@ -69,6 +70,7 @@ supervisor = { path = "supervisor" } supervisor-client = { path = "supervisor/client" } tdx-attest = { path = "tdx-attest" } tdx-attest-sys = { path = "tdx-attest-sys" } +dstack-attest = { path = "dstack-attest" } certbot = { path = "certbot" } rocket-vsock-listener = { path = "rocket-vsock-listener" } host-api = { path = "host-api", default-features = false } @@ -82,6 +84,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,15 +121,19 @@ scale = { version = "3.7.4", package = "parity-scale-codec", features = [ "derive", ] } serde = { version = "1.0.228", 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" toml_edit = { version = "0.22.24", features = ["serde"] } yasna = "0.5.2" bytes = "1.10.1" +nom = "7.1" figment = "0.10.19" object = "0.36.4" +fatfs = "0.3.6" +fscommon = "0.1.1" +ciborium = "0.2" # Networking/HTTP bollard = "0.18.1" @@ -159,13 +166,17 @@ default-net = "0.22.0" aes-gcm = "0.10.3" curve25519-dalek = "4.1.3" dcap-qvl = "0.3.8" +dcap-qvl-webpki = "0.103" elliptic-curve = { version = "0.13.8", features = ["pkcs8"] } getrandom = "0.3.1" hkdf = "0.12.4" p256 = "0.13.2" +p384 = "0.13" +rsa = "0.9" ring = "0.17.14" rustls = "0.23.23" -rustls-pki-types = "1.11.0" +rustls-pki-types = "1.13.1" +rustls-webpki = "0.103.8" schnorrkel = "0.11.4" sha2 = { version = "0.10.8", default-features = false } sha3 = "0.10.8" @@ -179,10 +190,12 @@ 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" instant-acme = "0.7.2" +pem = "3.0" rcgen = { version = "0.13.2", features = ["pem"] } x509-parser = "0.16.0" pkcs8 = { version = "0.10", default-features = false } @@ -217,10 +230,12 @@ 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" +tar = "0.4" [profile.release] panic = "abort" diff --git a/REUSE.toml b/REUSE.toml index 1b6dff22..18aced85 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -68,6 +68,7 @@ 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", ] @@ -185,3 +186,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/src/lib.rs b/cc-eventlog/src/lib.rs index 50f491cc..e850f39c 100644 --- a/cc-eventlog/src/lib.rs +++ b/cc-eventlog/src/lib.rs @@ -2,288 +2,13 @@ // // 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; #[cfg(test)] mod tests { @@ -292,9 +17,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/cert-client/Cargo.toml b/cert-client/Cargo.toml index 087d6f09..996e867d 100644 --- a/cert-client/Cargo.toml +++ b/cert-client/Cargo.toml @@ -14,6 +14,5 @@ anyhow.workspace = true dstack-types.workspace = true dstack-kms-rpc.workspace = true ra-rpc = { workspace = true, features = ["client"] } -ra-tls.workspace = true +ra-tls = { workspace = true, features = ["quote"] } serde_json.workspace = true -tdx-attest.workspace = true diff --git a/cert-client/src/lib.rs b/cert-client/src/lib.rs index d53e821a..8328cd68 100644 --- a/cert-client/src/lib.rs +++ b/cert-client/src/lib.rs @@ -7,11 +7,10 @@ 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 { @@ -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,7 +62,9 @@ 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")?; Ok(CertRequestClient::Local { ca: Box::new(ca) }) @@ -96,26 +97,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/cvm-boundaries.md b/docs/security/cvm-boundaries.md index 06b8e0cb..deab70ed 100644 --- a/docs/security/cvm-boundaries.md +++ b/docs/security/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-best-practices.md](./security-best-practices.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..13f8e7b2 --- /dev/null +++ b/dstack-attest/Cargo.toml @@ -0,0 +1,36 @@ +# 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 +insta.workspace = true + +[features] +quote = [] + +[dev-dependencies] +futures = { workspace = true } +tokio = { workspace = true, features = ["full"] } diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs new file mode 100644 index 00000000..40271d9b --- /dev/null +++ b/dstack-attest/src/attestation.rs @@ -0,0 +1,793 @@ +// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Attestation functions + +use std::{borrow::Cow, time::SystemTime}; + +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, VmConfig}; +use ez_hash::{sha256, Hasher, Sha384}; +use or_panic::ResultOrPanic; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use serde_human_bytes as hex_bytes; +use sha2::Digest as _; + +const DSTACK_TDX: &str = "dstack-tdx"; +const DSTACK_GCP_TDX: &str = "dstack-gcp-tdx"; +const DSTACK_NITRO_ENCLAVE: &str = "dstack-nitro-enclave"; + +/// Attestation mode +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)] +pub enum AttestationMode { + /// Intel TDX with DCAP quote only + #[default] + #[serde(rename = "dstack-tdx")] + DstackTdx, + /// GCP TDX with DCAP quote only + #[serde(rename = "dstack-gcp-tdx")] + DstackGcpTdx, + /// Dstack attestation SDK in AWS Nitro Enclave + #[serde(rename = "dstack-nitro-enclave")] + DstackNitroEnclave, +} + +impl AttestationMode { + /// 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::DstackGcpTdx); + } + bail!("Unsupported platform: GCP(-tdx)"); + } + Platform::NitroEnclave => Ok(Self::DstackNitroEnclave), + } + } + + /// Check if TDX quote should be included + pub fn has_tdx(&self) -> bool { + match self { + Self::DstackTdx => true, + Self::DstackGcpTdx => true, + Self::DstackNitroEnclave => false, + } + } + + /// Get TPM runtime event PCR index + pub fn tpm_runtime_pcr(&self) -> Option { + match self { + Self::DstackGcpTdx => Some(14), + Self::DstackTdx => None, + Self::DstackNitroEnclave => None, + } + } + + /// As string for debug + pub fn as_str(&self) -> &'static str { + match self { + Self::DstackTdx => DSTACK_TDX, + Self::DstackGcpTdx => DSTACK_GCP_TDX, + Self::DstackNitroEnclave => DSTACK_NITRO_ENCLAVE, + } + } + + /// Returns true if the attestation mode supports composability (OS image + runtime loadable application) + pub fn is_composable(&self) -> bool { + match self { + Self::DstackTdx => true, + Self::DstackGcpTdx => true, + Self::DstackNitroEnclave => false, + } + } +} + +/// 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) + } +} + +#[allow(clippy::large_enum_variant)] +/// Represents a verified attestation +#[derive(Clone)] +pub enum DstackVerifiedReport { + DstackTdx(TdxVerifiedReport), + DstackGcpTdx, + DstackNitroEnclave, +} + +impl DstackVerifiedReport { + pub fn tdx_report(&self) -> Option<&TdxVerifiedReport> { + match self { + DstackVerifiedReport::DstackTdx(report) => Some(report), + DstackVerifiedReport::DstackGcpTdx => None, + DstackVerifiedReport::DstackNitroEnclave => 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 an NSM (Nitro Security Module) attestation document +#[derive(Clone, Encode, Decode)] +pub struct NsmQuote { + /// The COSE Sign1 attestation document from NSM + pub document: 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) = attestation.tdx_quote_mut() { + tdx_quote.event_log = tdx_quote + .event_log + .iter() + .filter(|e| e.imr == 3) + .map(|e| e.stripped()) + .collect(); + } + self + } +} + +#[derive(Clone, Encode, Decode)] +pub enum AttestationQuote { + DstackTdx(TdxQuote), + DstackGcpTdx, + DstackNitroEnclave, +} + +impl AttestationQuote { + pub fn mode(&self) -> AttestationMode { + match self { + AttestationQuote::DstackTdx { .. } => AttestationMode::DstackTdx, + AttestationQuote::DstackGcpTdx => AttestationMode::DstackGcpTdx, + AttestationQuote::DstackNitroEnclave => AttestationMode::DstackNitroEnclave, + } + } +} + +/// Attestation data +#[derive(Clone, Encode, Decode)] +pub struct Attestation { + /// The quote + pub quote: AttestationQuote, + + /// 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 { + pub fn tdx_quote_mut(&mut self) -> Option<&mut TdxQuote> { + match &mut self.quote { + AttestationQuote::DstackTdx(quote) => Some(quote), + AttestationQuote::DstackGcpTdx => None, + AttestationQuote::DstackNitroEnclave => None, + } + } + + pub fn tdx_quote(&self) -> Option<&TdxQuote> { + match &self.quote { + AttestationQuote::DstackTdx(quote) => Some(quote), + AttestationQuote::DstackGcpTdx => None, + AttestationQuote::DstackNitroEnclave => None, + } + } + + /// Get TDX quote bytes + pub fn get_tdx_quote_bytes(&self) -> Option> { + self.tdx_quote().map(|q| q.quote.clone()) + } + + /// Get TDX event log bytes + pub fn get_tdx_event_log_bytes(&self) -> Option> { + self.tdx_quote() + .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() + .map(|q| serde_json::to_string(&q.event_log).unwrap_or_default()) + } + + pub fn get_td10_report(&self) -> Option { + self.tdx_quote() + .and_then(|q| Quote::parse(&q.quote).ok()) + .and_then(|quote| quote.report.as_td10().cloned()) + } +} + +pub trait GetDeviceId { + fn get_devide_id(&self) -> Vec; +} + +impl GetDeviceId for () { + fn get_devide_id(&self) -> Vec { + Vec::new() + } +} + +impl GetDeviceId for DstackVerifiedReport { + fn get_devide_id(&self) -> Vec { + match self { + DstackVerifiedReport::DstackTdx(tdx_report) => tdx_report.ppid.to_vec(), + DstackVerifiedReport::DstackGcpTdx => Vec::new(), + DstackVerifiedReport::DstackNitroEnclave => Vec::new(), + } + } +} + +struct Mrs { + mr_system: [u8; 32], + mr_aggregated: [u8; 32], +} + +impl Attestation { + fn decode_mr_tdx( + &self, + boottime_mr: bool, + mr_key_provider: &[u8], + tdx_quote: &TdxQuote, + ) -> Result { + let quote = Quote::parse(&tdx_quote.quote).context("Failed to parse 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 VM config from the external or embedded config + pub fn decode_vm_config<'a>(&'a self, mut config: &'a str) -> Result { + if config.is_empty() { + config = &self.config; + } + if config.is_empty() { + // No vm config for nitro enclave + config = "{}"; + } + let vm_config: VmConfig = + serde_json::from_str(config).context("Failed to parse vm config")?; + Ok(vm_config) + } + + /// Decode the app info from the event log + pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + self.decode_app_info_ex(boottime_mr, "") + } + + pub fn decode_app_info_ex(&self, boottime_mr: bool, vm_config: &str) -> 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 os_image_hash = self + .decode_vm_config(vm_config) + .context("Failed to decode os image hash")? + .os_image_hash; + let mrs = match &self.quote { + AttestationQuote::DstackTdx(q) => { + self.decode_mr_tdx(boottime_mr, &mr_key_provider, q)? + } + AttestationQuote::DstackGcpTdx | AttestationQuote::DstackNitroEnclave => { + bail!("Unsupported attestation quote"); + } + }; + let compose_hash = if self.quote.mode().is_composable() { + self.find_event_payload("compose-hash").unwrap_or_default() + } else { + os_image_hash.clone() + }; + Ok(AppInfo { + app_id: self.find_event_payload("app-id").unwrap_or_default(), + instance_id: self.find_event_payload("instance-id").unwrap_or_default(), + device_id: sha256(self.report.get_devide_id()).to_vec(), + mr_system: mrs.mr_system, + mr_aggregated: mrs.mr_aggregated, + key_provider_info, + os_image_hash, + compose_hash, + }) + } +} + +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") + } +} + +impl Attestation { + /// 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 { + quote: AttestationQuote::DstackTdx(TdxQuote { + quote, + event_log: tdx_eventlog, + }), + runtime_events, + report_data, + config: "".into(), + report: (), + }) + } +} + +#[cfg(feature = "quote")] +impl Attestation { + /// Create an attestation for local machine (auto-detect mode) + pub fn local() -> Result { + Self::quote(&[0u8; 64]) + } + + /// Create an attestation from a report data + pub fn quote(report_data: &[u8; 64]) -> Result { + Self::quote_with_app_id(report_data, None) + } + + pub fn quote_with_app_id(report_data: &[u8; 64], app_id: Option<[u8; 20]>) -> Result { + let mode = AttestationMode::detect()?; + let runtime_events = if mode.is_composable() { + RuntimeEvent::read_all().context("Failed to read runtime events")? + } else if let Some(app_id) = app_id { + vec![RuntimeEvent::new("app-id".to_string(), app_id.to_vec())] + } else { + vec![] + }; + + let quote = match mode { + AttestationMode::DstackTdx => { + 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")?; + AttestationQuote::DstackTdx(TdxQuote { quote, event_log }) + } + AttestationMode::DstackGcpTdx | AttestationMode::DstackNitroEnclave => { + bail!("Unsupported attestation mode: {mode:?}"); + } + }; + let config = match "e { + AttestationQuote::DstackTdx(_) => { + // TODO: Find a better way handling this hardcode path + fs_err::read_to_string("/dstack/.host-shared/.sys-config.json").unwrap_or_default() + } + AttestationQuote::DstackGcpTdx | AttestationQuote::DstackNitroEnclave => { + bail!("Unsupported attestation mode: {mode:?}"); + } + }; + + Ok(Self { + quote, + runtime_events, + report_data: *report_data, + config, + report: (), + }) + } +} + +impl Attestation { + /// Verify the quote with optional custom time (testing hook) + pub async fn verify_with_time( + self, + pccs_url: Option<&str>, + _now: Option, + ) -> Result { + let report = match &self.quote { + AttestationQuote::DstackTdx(q) => { + let report = self.verify_tdx(pccs_url, &q.quote).await?; + DstackVerifiedReport::DstackTdx(report) + } + AttestationQuote::DstackGcpTdx | AttestationQuote::DstackNitroEnclave => { + bail!("Unsupported attestation mode: {:?}", self.quote.mode()); + } + }; + + Ok(VerifiedAttestation { + quote: self.quote, + runtime_events: self.runtime_events, + report_data: self.report_data, + config: self.config, + report, + }) + } + + /// 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 { + self.verify_with_time(pccs_url, None).await + } + + async fn verify_tdx(&self, pccs_url: Option<&str>, quote: &[u8]) -> Result { + 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)?; + + let td_report = tdx_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) + ); + } + + if td_report.report_data != self.report_data[..] { + bail!("tdx report_data mismatch"); + } + 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..179dd861 --- /dev/null +++ b/dstack-attest/src/lib.rs @@ -0,0 +1,29 @@ +// 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; + +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")?; + } + Ok(()) +} diff --git a/dstack-mr/Cargo.toml b/dstack-mr/Cargo.toml index 32a96f96..12ca5009 100644 --- a/dstack-mr/Cargo.toml +++ b/dstack-mr/Cargo.toml @@ -28,6 +28,6 @@ scale.workspace = true [dev-dependencies] dstack-types.workspace = true -reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } -flate2 = "1.0" -tar = "0.4" +reqwest = { workspace = true, features = ["blocking"] } +flate2.workspace = true +tar.workspace = true 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..4af37cfc 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -2,6 +2,9 @@ // // SPDX-License-Identifier: Apache-2.0 +use std::path::Path; + +use scale::{Decode, Encode}; use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; use size_parser::human_size; @@ -69,6 +72,7 @@ pub enum KeyProviderKind { None, Kms, Local, + Tpm, } impl KeyProviderKind { @@ -79,6 +83,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 +109,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,17 +136,18 @@ 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, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct VmConfig { - pub spec_version: u32, - #[serde(with = "hex_bytes")] + #[serde(with = "hex_bytes", default)] 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 +168,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 +199,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 +218,7 @@ impl KeyProvider { match self { KeyProvider::None { .. } => KeyProviderKind::None, KeyProvider::Local { .. } => KeyProviderKind::Local, + KeyProvider::Tpm { .. } => KeyProviderKind::Tpm, KeyProvider::Kms { .. } => KeyProviderKind::Kms, } } @@ -208,6 +227,7 @@ impl KeyProvider { match self { KeyProvider::None { .. } => &[], KeyProvider::Local { mr, .. } => mr, + KeyProvider::Tpm { pubkey, .. } => pubkey, KeyProvider::Kms { pubkey, .. } => pubkey, } } @@ -244,3 +264,48 @@ 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, + /// AWS Nitro Enclave + NitroEnclave, +} + +impl Platform { + /// Detect platform from system DMI information + pub fn detect() -> Option { + // Nitro Enclave: NSM device exists only inside enclave + if Path::new("/dev/nsm").exists() { + return Some(Self::NitroEnclave); + } + + 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", + Self::NitroEnclave => "aws-nitro-enclave", + } + } +} 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..e8b47a90 100644 --- a/dstack-util/Cargo.toml +++ b/dstack-util/Cargo.toml @@ -32,7 +32,7 @@ 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 host-api = { workspace = true, features = ["client"] } @@ -43,14 +43,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..6bcc270a 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 | Platform::NitroEnclave => { + // Skip notify on unsupported platforms + 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 0145180d..a5cd6d8d 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -3,22 +3,20 @@ // 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, cert::generate_ra_cert, kdf::{derive_ecdsa_key, derive_ecdsa_key_pair_from_bytes}, rcgen::KeyPair, }; -use serde::Deserialize; -use std::{collections::HashMap, path::Path}; use std::{ io::{self, Read, Write}, path::PathBuf, @@ -28,6 +26,7 @@ use tdx_attest as att; use utils::AppKeys; mod crypto; +mod docker_compose; mod host_api; mod parse_env_file; mod system_setup; @@ -43,14 +42,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 @@ -184,44 +185,46 @@ struct RemoveOrphansArgs { /// path to the docker-compose.yaml file #[arg(short = 'f', long)] compose: String, -} -#[derive(Debug, Deserialize)] -struct ComposeConfig { - name: Option, - services: HashMap, -} + /// 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, -#[derive(Debug, Deserialize)] -struct ComposeService {} + /// Docker data root directory for offline mode (default: /var/lib/docker) + #[arg(short = 'd', long, default_value = "/var/lib/docker")] + docker_root: String, +} 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 hex_decode(hex_str: &str) -> Result> { + hex::decode(hex_str.trim_start_matches("0x")).context("Invalid hex string") +} + +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<()> { @@ -247,6 +250,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 { @@ -285,14 +339,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(); @@ -311,38 +364,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 @@ -356,15 +431,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, }) } @@ -381,89 +448,6 @@ 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)?, - }; - - // 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 docker_compose.services.contains_key(service_name) { - continue; - } - // 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))?; - } - - Ok(()) -} - #[tokio::main] async fn main() -> Result<()> { { @@ -475,9 +459,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)?; } @@ -506,7 +491,15 @@ 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?; + } } } diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index 6e568702..79133e14 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::{ @@ -150,12 +150,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 +201,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 +289,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 +406,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 +506,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 +638,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 +655,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,8 +724,12 @@ 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) } @@ -683,7 +741,11 @@ 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 => { + bail!("Tpm key provider is not supported"); } } } @@ -767,12 +829,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 +901,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 +1055,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 +1099,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 +1135,9 @@ impl<'a> Stage0<'a> { KeyProvider::Local { mr, .. } => { KeyProviderInfo::new("local-sgx".into(), hex::encode(mr)) } + KeyProvider::Tpm { .. } => { + bail!("Tpm key provider is not supported"); + } KeyProvider::Kms { pubkey, .. } => { KeyProviderInfo::new("kms".into(), hex::encode(pubkey)) } @@ -1010,7 +1147,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 +1170,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 +1186,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/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_service.rs b/gateway/src/main_service.rs index 6390366f..e6bc6775 100644 --- a/gateway/src/main_service.rs +++ b/gateway/src/main_service.rs @@ -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/guest-agent/Cargo.toml b/guest-agent/Cargo.toml index b8e0c881..9bf291b9 100644 --- a/guest-agent/Cargo.toml +++ b/guest-agent/Cargo.toml @@ -29,7 +29,7 @@ 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 guest-api = { workspace = true, features = ["client"] } host-api = { workspace = true, features = ["client"] } @@ -46,8 +46,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..2c4aef25 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..03967547 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,41 @@ 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() + .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 +220,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 +241,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 +302,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 +310,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 +325,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 +417,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 +446,21 @@ 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 mut attestation = attestation; + let Some(quote) = attestation.tdx_quote_mut() 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.to_vec(), + 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/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..57fead25 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::{ @@ -15,20 +11,18 @@ use dstack_kms_rpc::{ GetMetaResponse, GetTempCaCertResponse, KmsKeyResponse, KmsKeys, PublicKeyResponse, 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 tracing::info; use upgrade_authority::BootInfo; use crate::{ @@ -57,6 +51,7 @@ pub struct KmsStateInner { k256_key: SigningKey, temp_ca_cert: String, temp_ca_key: String, + verifier: CvmVerifier, } impl KmsState { @@ -70,6 +65,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 +77,7 @@ impl KmsState { k256_key, temp_ca_cert, temp_ca_key, + verifier, }), }) } @@ -90,50 +91,6 @@ pub struct RpcHandler { struct BootConfig { boot_info: BootInfo, gateway_app_id: String, - 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 { @@ -161,10 +118,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 +143,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,36 +166,33 @@ 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 vm_config: VmConfig = - serde_json::from_str(vm_config).context("Failed to decode VM config")?; - let os_image_hash = vm_config.os_image_hash.clone(); + let tcb_status; + let advisory_ids; + match att.report.tdx_report() { + Some(report) => { + tcb_status = report.status.clone(); + advisory_ids = report.advisory_ids.clone(); + } + None => { + tcb_status = "".to_string(); + advisory_ids = Vec::new(); + } + }; + let app_info = att.decode_app_info_ex(use_boottime_mr, vm_config_str)?; 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(), + attestation_mode: att.quote.mode(), mr_aggregated: app_info.mr_aggregated.to_vec(), - os_image_hash: os_image_hash.clone(), + os_image_hash: app_info.os_image_hash, mr_system: app_info.mr_system.to_vec(), app_id: app_info.app_id, compose_hash: app_info.compose_hash, 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, + advisory_ids, }; let response = self .state @@ -468,13 +203,12 @@ 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 { boot_info, gateway_app_id: response.gateway_app_id, - os_image_hash, }) } @@ -507,7 +241,6 @@ impl KmsRpc for RpcHandler { let BootConfig { boot_info, gateway_app_id, - os_image_hash, } = self .ensure_app_boot_allowed(&request.vm_config) .await @@ -540,7 +273,7 @@ impl KmsRpc for RpcHandler { k256_signature, tproxy_app_id: gateway_app_id.clone(), gateway_app_id, - os_image_hash, + os_image_hash: boot_info.os_image_hash, }) } @@ -614,15 +347,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 +391,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..169fd495 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -4,22 +4,14 @@ use crate::config::AuthApi; use anyhow::{bail, Result}; +use ra_tls::attestation::AttestationMode; use serde::{Deserialize, Serialize}; 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, + pub attestation_mode: AttestationMode, #[serde(with = "hex_bytes")] pub mr_aggregated: Vec, #[serde(with = "hex_bytes")] @@ -36,7 +28,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/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..2a83ca64 100644 --- a/ra-tls/Cargo.toml +++ b/ra-tls/Cargo.toml @@ -28,9 +28,17 @@ x509-parser.workspace = true yasna.workspace = true tracing.workspace = true sha3.workspace = true -tdx-attest.workspace = true scale.workspace = true cc-eventlog.workspace = true serde-human-bytes.workspace = true +flate2.workspace = true or-panic.workspace = true +rand.workspace = true +dstack-types.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..680a0291 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 ring::signature::{ - EcdsaKeyPair, UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P256_SHA256_ASN1_SIGNING, +use crate::oids::{ + PHALA_RATLS_APP_ID, PHALA_RATLS_ATTESTATION, PHALA_RATLS_CERT_USAGE, PHALA_RATLS_EVENT_LOG, + PHALA_RATLS_TDX_QUOTE, }; -use scale::{Decode, Encode}; +use crate::traits::CertExt; +#[cfg(feature = "quote")] +use dstack_attest::attestation::QuoteContentType; +use dstack_attest::attestation::{Attestation, AttestationQuote, VersionedAttestation}; /// 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,28 @@ 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(usage) = self.special_usage { + add_ext(&mut params, PHALA_RATLS_CERT_USAGE, usage); } - 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(ver_att) = self.attestation { + let VersionedAttestation::V0 { attestation } = &ver_att; + match &attestation.quote { + AttestationQuote::DstackTdx(tdx_quote) => { + // For backward compatibility, we serialize the quote to the classic oids. + 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 +372,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,23 +432,82 @@ 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. +#[cfg(feature = "quote")] pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result { + generate_ra_cert_with_app_id(ca_cert_pem, ca_key_pem, None) +} + +/// Generate a certificate with RA-TLS quote and event log. +/// If app_id is provided, it will be included in the quote. +#[cfg(feature = "quote")] +pub fn generate_ra_cert_with_app_id( + ca_cert_pem: String, + ca_key_pem: String, + app_id: Option<[u8; 20]>, +) -> Result { use 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) = get_quote(&report_data, None).context("Failed to get quote")?; - let event_logs = 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_with_app_id(&report_data, app_id) + .context("Failed to get quote for cert pubkey")? + .into_versioned(); + + // Build certificate request with all extensions let req = CertRequest::builder() .subject("RA-TLS TEMP Cert") - .quote("e) - .event_log(&event_log) .key(&key) + .attestation(&attestation) .build(); let cert = ca.sign(req).context("Failed to sign certificate")?; Ok(CertPair { @@ -355,14 +519,16 @@ pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result" +} +``` + ## Error Responses All endpoints may return the following HTTP status codes: diff --git a/sdk/go/README.md b/sdk/go/README.md index 03024f1b..b915fe12 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -28,8 +28,8 @@ func main() { fmt.Println(key.Key) // Same path always returns the same key // Generate an attestation quote - quote, _ := client.GetQuote(context.Background(), []byte("my-app-state")) - fmt.Println(quote.Quote) + attest, _ := client.Attest(context.Background(), []byte("my-app-state")) + fmt.Println(attest.Attestation) } ``` @@ -87,6 +87,19 @@ fmt.Println(rtmrs) - `EventLog`: JSON string of measured events - `ReplayRTMRs()`: Method to compute RTMR values from event log +`Attest()` creates a versioned attestation with the given report data. + +```go +attest, _ := client.Attest(ctx, []byte("my-app-state")) +fmt.Println(attest.Attestation) +``` + +**Parameters:** +- `reportData`: Exactly 64 bytes recommended. If shorter, pad with zeros. If longer, hash it first (e.g., SHA-256). + +**Returns:** `*AttestResponse` +- `Attestation`: Versioned attestation as bytes + ### Get Instance Info ```go 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..ed9dee23 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,8 +449,8 @@ func TestInfo(t *testing.T) { } func TestGetKeySignatureVerification(t *testing.T) { - expectedAppPubkey, _ := hex.DecodeString("02b85cceca0c02d878f0ebcda72a97469a472416eb6faf3c4807642132f9786810") - expectedKmsPubkey, _ := hex.DecodeString("02cad3a8bb11c5c0858fb3e402048b5137457039d577986daade678ed4b4ab1b9b") + expectedAppPubkey, _ := hex.DecodeString("02818494263695e8839122dbd88e281d7380622999df4e60a14befa0f2d096fc7c") + expectedKmsPubkey, _ := hex.DecodeString("0321529e458424ab1f710a3a57ec4dad2fb195ddca572f7469242ba6c7563085b6") client := dstack.NewDstackClient() path := "/test/path" diff --git a/sdk/js/README.md b/sdk/js/README.md index 609db912..70e7e82a 100644 --- a/sdk/js/README.md +++ b/sdk/js/README.md @@ -347,6 +347,20 @@ Generates a TDX attestation quote containing the provided report data. - Cryptographic proof of execution environment - Audit trail generation +##### `attest(reportData: string | Buffer | Uint8Array): Promise` + +Generates a versioned attestation containing the provided report data. + +**Parameters:** +- `reportData`: Data to include in attestation (max 64 bytes) + +**Returns:** `AttestResponse` +- `attestation`: Hex-encoded attestation payload + +**Use Cases:** +- Remote attestation across multiple platform types +- Verifier APIs that accept versioned attestations + ##### `getTlsKey(options?: TlsKeyOptions): Promise` Generates a fresh, random TLS key pair with X.509 certificate for TLS/SSL connections. **Important**: This method generates different keys on each call - use `getKey()` for deterministic keys. 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 e66e2660..ddb28245 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/README.md b/sdk/rust/README.md index a6b0056a..cc5761b3 100644 --- a/sdk/rust/README.md +++ b/sdk/rust/README.md @@ -23,8 +23,8 @@ async fn main() -> Result<(), Box> { println!("{}", key.key); // Same path always returns the same key // Generate an attestation quote - let quote = client.get_quote(b"my-app-state".to_vec()).await?; - println!("{}", quote.quote); + let resp = client.attest(b"my-app-state".to_vec()).await?; + println!("{}", resp.attestation); Ok(()) } @@ -97,6 +97,13 @@ println!("{}", info.tcb_info); - `compose_hash`: Hash of the app configuration - `app_cert`: Application certificate (PEM) +#### `attest(report_data: Vec) -> AttestResponse` +Generates a versioned attestation with a custom 64-byte payload. +- `attestation`: Hex-encoded attestation + +#### `emit_event(event: String, payload: Vec)` +Sends an event log with associated binary payload to the runtime. + ### Generate TLS Certificates `get_tls_key()` creates fresh TLS certificates. Unlike `get_key()`, each call generates a new random key. diff --git a/sdk/rust/src/dstack_client.rs b/sdk/rust/src/dstack_client.rs index f1f206a1..26341a60 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..2c4aef25 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/tdx-attest/src/dummy.rs b/tdx-attest/src/dummy.rs index 12ee199b..65314a4f 100644 --- a/tdx-attest/src/dummy.rs +++ b/tdx-attest/src/dummy.rs @@ -45,9 +45,6 @@ pub enum TdxAttestError { 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) } diff --git a/tdx-attest/src/lib.rs b/tdx-attest/src/lib.rs index 3cfa4fdc..453843ce 100644 --- a/tdx-attest/src/lib.rs +++ b/tdx-attest/src/lib.rs @@ -24,14 +24,3 @@ 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..9959777d 100644 --- a/tdx-attest/src/linux.rs +++ b/tdx-attest/src/linux.rs @@ -2,18 +2,13 @@ // // 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 fs_err as fs; use num_enum::FromPrimitive; use thiserror::Error; @@ -53,22 +48,17 @@ pub enum TdxAttestError { UnknownError(u32), } -pub fn get_quote( - report_data: &TdxReportData, - att_key_id_list: Option<&[TdxUuid]>, -) -> Result<(TdxUuid, Vec)> { +pub fn get_quote(report_data: &TdxReportData) -> Result> { 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()); + let key_id_list_ptr = 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), + 0, &mut att_key_id as *mut TdxUuid as *mut tdx_uuid_t, &mut quote_ptr, &mut quote_size, @@ -86,7 +76,7 @@ pub fn get_quote( tdx_att_free_quote(quote_ptr); } - Ok((att_key_id, quote)) + Ok(quote) } pub fn get_report(report_data: &TdxReportData) -> Result { @@ -106,30 +96,6 @@ pub fn get_report(report_data: &TdxReportData) -> Result { Ok(report) } -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(()) -} - pub fn extend_rtmr(index: u32, event_type: u32, digest: [u8; 48]) -> Result<()> { let event = tdx_rtmr_event_t { version: 1, diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml index 78706da9..cbec4d68 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,10 @@ dstack-mr.workspace = true dcap-qvl.workspace = true cc-eventlog.workspace = true sha2.workspace = true +ez-hash.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/README.md b/verifier/README.md index 4d3d8618..d3ae2767 100644 --- a/verifier/README.md +++ b/verifier/README.md @@ -6,9 +6,11 @@ A HTTP server that provides dstack quote verification services using the same ve ### POST /verify -Verifies a dstack quote with the provided quote and VM configuration. The body can be grabbed via [getQuote](https://github.com/Dstack-TEE/dstack/blob/master/sdk/curl/api.md#3-get-quote). +Verifies a dstack attestation or quote with the provided data and VM configuration. The body can be grabbed via [getQuote](https://github.com/Dstack-TEE/dstack/blob/master/sdk/curl/api.md#3-get-quote) or [attest](https://github.com/Dstack-TEE/dstack/blob/master/sdk/curl/api.md#8-attest). **Request Body:** +Provide either `attestation` or (`quote` + `event_log` + `vm_config`). + ```json { "quote": "hex-encoded-quote", @@ -16,6 +18,12 @@ Verifies a dstack quote with the provided quote and VM configuration. The body c "vm_config": "json-vm-config-string", } ``` +or +```json +{ + "attestation": "hex-encoded-attestation", +} +``` **Response:** ```json @@ -26,7 +34,7 @@ Verifies a dstack quote with the provided quote and VM configuration. The body c "event_log_verified": true, "os_image_hash_verified": true, "report_data": "hex-encoded-64-byte-report-data", - "tcb_status": "OK", + "tcb_status": "UpToDate", "advisory_ids": [], "app_info": { "app_id": "hex-string", @@ -113,7 +121,18 @@ Save the docker compose file as `docker-compose.yml` and run `docker compose up ### Request verification -Grab a quote from your app. It's depends on your app how to grab a quote. +Grab an attestation or quote from your app. It's depends on your app how to grab it. + +```bash +# Grab an attestation from the demo app +curl https://712eab2f507b963e11144ae67218177e93ac2a24-3000.test0.dstack.org:12004/Attest?report_data=0x1234 -o attest.json +``` + +Send the attestation to the verifier. + +```bash +$ curl -s -d @attest.json localhost:8080/verify | jq +``` ```bash # Grab a quote from the demo app diff --git a/verifier/src/lib.rs b/verifier/src/lib.rs new file mode 100644 index 00000000..8ead45fd --- /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, 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 0eeaf7d9..c921ed8b 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 98521da3..42eef846 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -9,12 +9,14 @@ 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 ra_tls::attestation::{ + Attestation, AttestationQuote, 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 +25,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 +46,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 +55,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 +84,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 +126,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 +263,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 +321,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 +406,48 @@ 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().map(|r| r.status.clone()); + details.advisory_ids = att + .report + .tdx_report() + .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 +471,47 @@ 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") + vm_config: String, + attestation: &VerifiedAttestation, + debug: bool, + details: &mut VerificationDetails, + ) -> Result { + let vm_config = attestation + .decode_vm_config(&vm_config) + .context("Failed to decode VM config")?; + match &attestation.quote { + AttestationQuote::DstackTdx(_) => { + self.verify_os_image_hash_for_dstack_tdx(&vm_config, attestation, debug, details) + .await?; + } + AttestationQuote::DstackGcpTdx | AttestationQuote::DstackNitroEnclave => { + bail!( + "Unsupported attestation quote: {:?}", + attestation.quote.mode() + ); + } + } + 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 +524,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 +536,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 +551,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 +567,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 +585,8 @@ impl CvmVerifier { &expected_mrs.rtmr0, &verified_mrs.rtmr0, &expected_logs[0], - &computation_result.event_indices[0], - &event_log, + &[], + event_log, )); } @@ -572,8 +596,8 @@ impl CvmVerifier { &expected_mrs.rtmr1, &verified_mrs.rtmr1, &expected_logs[1], - &computation_result.event_indices[1], - &event_log, + &[], + event_log, )); } @@ -583,8 +607,8 @@ impl CvmVerifier { &expected_mrs.rtmr2, &verified_mrs.rtmr2, &expected_logs[2], - &computation_result.event_indices[2], - &event_log, + &[], + event_log, )); } @@ -597,7 +621,7 @@ impl CvmVerifier { } } - async fn download_image(&self, hex_os_image_hash: &str, dst_dir: &Path) -> Result<()> { + 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); 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 0972d5c6..c176ce94 100644 --- a/vmm/src/app.rs +++ b/vmm/src/app.rs @@ -805,22 +805,21 @@ pub(crate) fn make_sys_config(cfg: &Config, manifest: &Manifest) -> Result dstack_types::VmConfig { +fn make_vm_config(cfg: &Config, manifest: &Manifest, image: &Image) -> Result { let os_image_hash = image .digest .as_ref() .and_then(|d| hex::decode(d).ok()) .unwrap_or_default(); let gpus = manifest.gpus.clone().unwrap_or_default(); - dstack_types::VmConfig { - spec_version: 1, + let mut config = serde_json::to_value(dstack_types::VmConfig { os_image_hash, cpu_count: manifest.vcpu, memory_size: manifest.memory as u64 * 1024 * 1024, @@ -831,9 +830,13 @@ 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()), - } + })?; + // For backward compatibility + config["spec_version"] = serde_json::Value::from(1); + Ok(config) } fn paginate(items: Vec, page: u32, page_size: u32) -> impl Iterator { 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 2f979e2f..965ccaf6 100644 --- a/vmm/src/console_v1.html +++ b/vmm/src/console_v1.html @@ -2074,11 +2074,23 @@

Deploy a new instance

/>
+
+ + +
+ +
+ + +
+
- - @@ -2089,11 +2101,6 @@

Deploy a new instance

-
- - -
-
@@ -2350,8 +2357,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, @@ -2790,9 +2796,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'; @@ -2817,17 +2822,25 @@

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; + // 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; } @@ -2843,16 +2856,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; @@ -2888,12 +2891,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, @@ -3023,7 +3025,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, @@ -3129,8 +3131,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.'); @@ -3139,7 +3154,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) || '', @@ -3155,17 +3170,16 @@

Derive VM

attachAllGpus: false, encryptedEnvs: [], // Clear environment variables ports: [], // Clear port mappings - storage_fs: ((_g = theVm.appCompose) === null || _g === void 0 ? void 0 : _g.storage_fs) || 'ext4', + storage_fs: ((_g = theVm.appCompose) === null || _g === void 0 ? void 0 : _g.storage_fs) || 'zfs', 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, @@ -3473,24 +3487,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..b0db62a1 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'], default=None, + help='Override key provider type (none, kms, local)') 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..0cc73ca2 100644 --- a/vmm/ui/src/components/CreateVmDialog.ts +++ b/vmm/ui/src/components/CreateVmDialog.ts @@ -123,11 +123,23 @@ const CreateVmDialogComponent = { /> +
+ + +
+ +
+ + +
+
- - @@ -138,11 +150,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"