From 068364582fd0962133233bcf1e6be7f7bc80ae80 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Sat, 17 Oct 2026 06:27:05 +0000 Subject: [PATCH 1/6] feat(TC-887): at-rest key escrow crate and ceremony CLI Adds two workspace crates that the node does not use yet: - tinycloud-at-rest-escrow: 2-of-3 Shamir split of the at-rest root key (vsss-rs 5.4.0, GF(256)), shares sealed with age 0.12.1 to pinned age1tag1 (hardware) or age1 recipients, the escrow bundle and its fingerprint, the canary, the ARK-MACed verification record and the recovery wire format. - tinycloud-at-rest-cli (tinycloud-at-rest): recipients config, nonce, inspect, extract-share, check-share, verify-node, share-wrap and verify-record. share-wrap verifies the node's TDX quote with DCAP collateral (dcap-qvl 0.7.0) and a pinned trust policy before it releases a share; shares are read from stdin only. Fixtures: real TDX quotes and collateral from dcap-qvl 0.7.0 (MIT). Lockfile: dcap-qvl raises anyhow, borsh, chrono and typenum within their semver ranges. docs/at-rest-escrow.md describes the formats. --- Cargo.lock | 1749 +++++++++++++++-- Cargo.toml | 2 + Dockerfile | 2 + docs/at-rest-escrow.md | 71 + release-plz.toml | 10 + tinycloud-at-rest-cli/Cargo.toml | 26 + tinycloud-at-rest-cli/src/attest.rs | 605 ++++++ tinycloud-at-rest-cli/src/main.rs | 289 +++ .../tests/fixtures/NOTICE.md | 5 + .../tests/fixtures/tdx_quote | Bin 0 -> 5006 bytes .../tests/fixtures/tdx_quote_collateral.json | 11 + .../tests/fixtures/tdx_quote_outdated | Bin 0 -> 5006 bytes .../tdx_quote_outdated_collateral.json | 1 + tinycloud-at-rest-escrow/Cargo.toml | 30 + tinycloud-at-rest-escrow/src/bundle.rs | 454 +++++ tinycloud-at-rest-escrow/src/lib.rs | 252 +++ tinycloud-at-rest-escrow/src/recipients.rs | 180 ++ tinycloud-at-rest-escrow/src/record.rs | 193 ++ tinycloud-at-rest-escrow/src/recovery.rs | 196 ++ tinycloud-at-rest-escrow/src/share.rs | 152 ++ tinycloud-at-rest-escrow/src/test_support.rs | 105 + 21 files changed, 4221 insertions(+), 112 deletions(-) create mode 100644 docs/at-rest-escrow.md create mode 100644 tinycloud-at-rest-cli/Cargo.toml create mode 100644 tinycloud-at-rest-cli/src/attest.rs create mode 100644 tinycloud-at-rest-cli/src/main.rs create mode 100644 tinycloud-at-rest-cli/tests/fixtures/NOTICE.md create mode 100644 tinycloud-at-rest-cli/tests/fixtures/tdx_quote create mode 100644 tinycloud-at-rest-cli/tests/fixtures/tdx_quote_collateral.json create mode 100644 tinycloud-at-rest-cli/tests/fixtures/tdx_quote_outdated create mode 100644 tinycloud-at-rest-cli/tests/fixtures/tdx_quote_outdated_collateral.json create mode 100644 tinycloud-at-rest-escrow/Cargo.toml create mode 100644 tinycloud-at-rest-escrow/src/bundle.rs create mode 100644 tinycloud-at-rest-escrow/src/lib.rs create mode 100644 tinycloud-at-rest-escrow/src/recipients.rs create mode 100644 tinycloud-at-rest-escrow/src/record.rs create mode 100644 tinycloud-at-rest-escrow/src/recovery.rs create mode 100644 tinycloud-at-rest-escrow/src/share.rs create mode 100644 tinycloud-at-rest-escrow/src/test_support.rs diff --git a/Cargo.lock b/Cargo.lock index f90966a2..8918f2fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" dependencies = [ "abnf-core", - "nom", + "nom 7.1.3", ] [[package]] @@ -18,7 +18,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -33,8 +33,8 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", - "generic-array", + "crypto-common 0.1.6", + "generic-array 0.14.9", ] [[package]] @@ -45,7 +45,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", "zeroize", ] @@ -64,6 +64,57 @@ dependencies = [ "zeroize", ] +[[package]] +name = "age" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd290633c2482479f70f6d1d96ae0e9f52c6a26cd5859edd47ee1fe33fc89f26" +dependencies = [ + "age-core", + "base64 0.22.1", + "bech32 0.11.1", + "chacha20poly1305", + "cipher", + "cookie-factory", + "hkdf", + "hmac 0.12.1", + "hpke", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "ml-kem", + "nom 8.0.0", + "p256 0.13.2", + "pin-project", + "rand 0.8.5", + "rust-embed", + "scrypt", + "sha2 0.10.9", + "sha3", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d4375964d1501e5f1b32aef2ead573913893ff238448d4e9fdf1522d828656" +dependencies = [ + "base64 0.22.1", + "bech32 0.11.1", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "hpke", + "io_tee", + "nom 8.0.0", + "rand 0.8.5", + "secrecy", + "sha2 0.10.9", +] + [[package]] name = "ahash" version = "0.7.8" @@ -439,7 +490,7 @@ dependencies = [ "alloy-primitives 0.7.7", "async-trait", "auto_impl", - "elliptic-curve", + "elliptic-curve 0.13.8", "k256", "thiserror 1.0.69", ] @@ -612,9 +663,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" @@ -625,6 +676,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -1053,7 +1113,7 @@ dependencies = [ "asn1-rs-derive 0.1.0", "asn1-rs-impl 0.1.0", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror 1.0.69", @@ -1069,7 +1129,7 @@ dependencies = [ "asn1-rs-derive 0.5.1", "asn1-rs-impl 0.2.0", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror 1.0.69", @@ -1122,6 +1182,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "asn1_der" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" + [[package]] name = "async-attributes" version = "1.1.2" @@ -1416,6 +1482,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-lc-rs" +version = "1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "aws-sdk-dynamodb" version = "0.19.0" @@ -1775,6 +1864,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base256emoji" version = "1.0.2" @@ -1809,18 +1904,39 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bech32" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + [[package]] name = "bigdecimal" version = "0.4.10" @@ -1957,7 +2073,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq 0.4.2", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1966,7 +2082,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.9", ] [[package]] @@ -1975,7 +2091,16 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.9", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array 0.4.15", ] [[package]] @@ -2005,25 +2130,26 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" dependencies = [ "once_cell", "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 3.0.9", ] [[package]] @@ -2225,7 +2351,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", ] [[package]] @@ -2235,7 +2372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -2243,9 +2380,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -2316,7 +2453,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", "zeroize", ] @@ -2370,6 +2507,21 @@ dependencies = [ "cc", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" version = "1.0.5" @@ -2382,6 +2534,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337cdbf3f1a0e643b4a7d1a2ffa39d22342fb6ee25739b5cfb997c28b3586422" +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "comfy-table" version = "7.1.2" @@ -2419,7 +2581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -2436,6 +2598,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -2526,6 +2694,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2536,6 +2713,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2561,6 +2748,12 @@ dependencies = [ "ciborium-io", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2570,6 +2763,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -2603,6 +2805,30 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -2630,7 +2856,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ - "generic-array", + "generic-array 0.14.9", "subtle", ] @@ -2640,23 +2866,61 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.9", "rand_core 0.6.4", "subtle", "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96272c2ff28b807e09250b180ad1fb7889a3258f7455759b5c3c58b719467130" +dependencies = [ + "num-traits", + "rand_core 0.6.4", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array 0.4.15", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.9", "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array 0.4.15", + "rand_core 0.10.1", +] + [[package]] name = "csv" version = "1.4.0" @@ -2696,6 +2960,16 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctutils" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +dependencies = [ + "cmov", + "subtle", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2703,10 +2977,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto", + "fiat-crypto 0.2.9", "rustc_version 0.4.1", "subtle", "zeroize", @@ -2864,7 +3138,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.114", +] + +[[package]] +name = "dcap-qvl" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b992c9511adf7135ae7c63c023c9303a3615c0f38d7b39514dcc262f91c6adda" +dependencies = [ + "anyhow", + "asn1_der", + "base64 0.23.1", + "borsh", + "byteorder", + "chrono", + "const-oid 0.10.2", + "dcap-qvl-webpki", + "der 0.8.2", + "derive_more 2.1.1", + "futures", + "hex", + "log", + "p256 0.14.0", + "parity-scale-codec", + "pem 4.0.0", + "reqwest 0.13.5", + "ring 0.17.14", + "rustls-pki-types", + "scale-info", + "serde", + "serde-human-bytes", + "serde_json", + "sha2 0.11.1", + "signature 3.0.0", + "tracing", + "urlencoding", + "x509-cert", +] + +[[package]] +name = "dcap-qvl-webpki" +version = "0.103.15+dcap.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8f2341b6a0f80654c55d0753d420aa306c97b5234c3a47f7e4ccfb6a695b2e" +dependencies = [ + "ecdsa 0.17.0", + "ed25519-dalek", + "p256 0.14.0", + "p384 0.14.0", + "ring 0.17.14", + "rustls-pki-types", + "sha2 0.10.9", + "signature 2.2.0", + "untrusted 0.9.0", ] [[package]] @@ -2895,6 +3222,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", + "der_derive", + "flagset", + "zeroize", +] + [[package]] name = "der-parser" version = "7.0.0" @@ -2903,7 +3242,7 @@ checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82" dependencies = [ "asn1-rs 0.3.1", "displaydoc", - "nom", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", @@ -2917,12 +3256,23 @@ checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ "asn1-rs 0.6.2", "displaydoc", - "nom", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", ] +[[package]] +name = "der_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "deranged" version = "0.5.5" @@ -2968,22 +3318,42 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + [[package]] name = "derive_more" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ - "derive_more-impl", + "derive_more-impl 2.1.1", ] [[package]] name = "derive_more-impl" -version = "2.1.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ - "convert_case 0.10.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version 0.4.1", @@ -3087,7 +3457,7 @@ dependencies = [ "iref", "k256", "multibase 0.9.3", - "p256", + "p256 0.13.2", "serde_json", "simple_asn1", "ssi-dids-core", @@ -3104,7 +3474,7 @@ version = "0.3.2" source = "git+https://github.com/tinycloudlabs/ssi#9d519692ab1b07b53d71f27ebad8473d64c338bf" dependencies = [ "async-trait", - "bech32", + "bech32 0.8.1", "bs58 0.4.0", "chrono", "iref", @@ -3158,7 +3528,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.9", ] [[package]] @@ -3169,10 +3539,22 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid 0.9.6", - "crypto-common", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -3237,12 +3619,26 @@ checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der 0.7.10", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", "spki 0.7.3", ] +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der 0.8.2", + "digest 0.11.3", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "zeroize", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -3250,7 +3646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8 0.10.2", - "signature", + "signature 2.2.0", ] [[package]] @@ -3319,21 +3715,55 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", + "base16ct 0.2.0", "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", - "generic-array", - "group", + "ff 0.13.1", + "generic-array 0.14.9", + "group 0.13.0", "hkdf", "pem-rfc7468 0.7.0", "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array 0.4.15", + "rand_core 0.10.1", + "sec1 0.8.1", "subtle", "zeroize", ] +[[package]] +name = "elliptic-curve-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de2b6fae800f08032a6ea32995b52925b1d451bff9d445c8ab2932323277faf" +dependencies = [ + "elliptic-curve 0.13.8", + "heapless", + "hex", + "multiexp", + "serde", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -3485,16 +3915,33 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ + "bitvec 1.0.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "figment" version = "0.10.19" @@ -3520,6 +3967,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "find-msvc-tools" version = "0.1.8" @@ -3547,6 +4003,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flatbuffers" version = "25.12.19" @@ -3568,6 +4030,51 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror 2.0.18", +] + [[package]] name = "flume" version = "0.11.1" @@ -3591,6 +4098,12 @@ 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 = "foreign-types" version = "0.3.2" @@ -3615,6 +4128,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "1.1.0" @@ -3792,6 +4311,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" +dependencies = [ + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -3814,11 +4343,23 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + [[package]] name = "ghash" version = "0.5.1" @@ -3853,11 +4394,22 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "h2" version = "0.3.27" @@ -3914,6 +4466,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3950,7 +4511,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -3958,6 +4519,9 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -3977,6 +4541,16 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -4019,13 +4593,83 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" +[[package]] +name = "hickory-net" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto", + "idna", + "ipnet", + "jni", + "rand 0.10.3", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.3", + "ring 0.17.14", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.3", + "resolv-conf", + "smallvec", + "system-configuration 0.7.0", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -4037,6 +4681,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "home" version = "0.5.12" @@ -4046,6 +4699,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "hpke" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4917627a14198c3603282c5158b815ad5534795451d3c074b53cf3cee0960b11" +dependencies = [ + "aead", + "aes-gcm", + "chacha20poly1305", + "digest 0.10.7", + "generic-array 0.14.9", + "hkdf", + "hmac 0.12.1", + "p256 0.13.2", + "rand_core 0.6.4", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "http" version = "0.2.12" @@ -4113,6 +4786,26 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "0.14.32" @@ -4171,7 +4864,7 @@ dependencies = [ "hyper 0.14.32", "log", "rustls 0.19.1", - "rustls-native-certs", + "rustls-native-certs 0.5.0", "tokio", "tokio-rustls 0.22.0", "webpki", @@ -4276,6 +4969,72 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a217bbb075dcaefb292efa78897fc0678245ca67f265d12c351e42268fcb0305" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602e6bd30c3db2749e13e38b363a3d98d9d41de1d8de7a79c31bb69e45b47cda" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error3", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.114", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -4507,7 +5266,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array", + "generic-array 0.14.9", ] [[package]] @@ -4519,15 +5278,53 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", + "socket2 0.6.1", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", ] [[package]] @@ -4546,6 +5343,9 @@ name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +dependencies = [ + "serde", +] [[package]] name = "iref" @@ -4630,6 +5430,55 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "simd_cesu8", + "syn 2.0.114", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.114", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4847,11 +5696,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "once_cell", "sha2 0.10.9", - "signature", + "signature 2.2.0", ] [[package]] @@ -4860,7 +5709,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -4883,6 +5732,16 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "kem" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f" +dependencies = [ + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -5322,6 +6181,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -5349,6 +6218,35 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" +dependencies = [ + "hybrid-array 0.2.3", + "kem", + "rand_core 0.6.4", + "sha3", +] + +[[package]] +name = "moka" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "mown" version = "0.2.2" @@ -5417,6 +6315,20 @@ dependencies = [ "data-encoding-macro", ] +[[package]] +name = "multiexp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" +dependencies = [ + "ff 0.13.1", + "group 0.13.0", + "rand_core 0.6.4", + "rustversion", + "std-shims", + "zeroize", +] + [[package]] name = "multihash" version = "0.16.3" @@ -5521,14 +6433,20 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nom" version = "7.1.3" @@ -5539,6 +6457,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -5570,6 +6497,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "rand 0.8.5", ] [[package]] @@ -5595,6 +6523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", + "rand 0.8.5", ] [[package]] @@ -5729,6 +6658,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -5774,6 +6707,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-src" version = "300.5.4+3.5.4" @@ -5935,24 +6874,51 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", "sha2 0.10.9", ] +[[package]] +name = "p256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", + "primeorder 0.14.0", + "sha2 0.11.1", +] + [[package]] name = "p384" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", "sha2 0.10.9", ] +[[package]] +name = "p384" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "fiat-crypto 0.3.0", + "primefield", + "primeorder 0.14.0", + "sha2 0.11.1", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -6034,7 +7000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac", + "hmac 0.12.1", ] [[package]] @@ -6080,6 +7046,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.3.1" @@ -6257,7 +7233,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -6269,11 +7245,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + [[package]] name = "potential_utf" version = "0.1.4" @@ -6298,6 +7280,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_dtoa" version = "0.3.0" @@ -6307,13 +7300,38 @@ dependencies = [ "ryu_floating_decimal", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.8", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve 0.14.1", + "primefield", + "wnaf", ] [[package]] @@ -6390,6 +7408,16 @@ dependencies = [ "quote", ] +[[package]] +name = "proc-macro-error-attr3" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "proc-macro-error2" version = "2.0.1" @@ -6402,6 +7430,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "proc-macro-error3" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" +dependencies = [ + "proc-macro-error-attr3", + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "proc-macro2" version = "1.0.105" @@ -6573,6 +7613,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -6617,6 +7658,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.6.2" @@ -6651,6 +7698,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20 0.10.2", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -6690,6 +7748,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -6726,7 +7790,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" dependencies = [ - "pem", + "pem 3.0.6", "ring 0.17.14", "rustls-pki-types", "time", @@ -6923,16 +7987,69 @@ dependencies = [ "webpki-roots 1.0.5", ] +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "hickory-resolver", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.36", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls 0.26.4", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint 0.7.5", + "hmac 0.13.0", +] + [[package]] name = "ring" version = "0.16.20" @@ -7152,7 +8269,7 @@ dependencies = [ "pkcs1 0.7.5", "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", + "signature 2.2.0", "spki 0.7.3", "subtle", "zeroize", @@ -7216,6 +8333,41 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19afa5b4b6a611de00bd1bdae6ae6f39084c9399f0679c3f52d8469cf335cc23" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d8afda6374eac59e066abee06d265247ebbaf3006cf878e2879e8356e34053" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.114", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" +dependencies = [ + "sha2 0.11.1", + "walkdir", +] + [[package]] name = "rust_decimal" version = "1.40.0" @@ -7268,7 +8420,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -7342,6 +8494,7 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring 0.17.14", @@ -7355,12 +8508,24 @@ dependencies = [ name = "rustls-native-certs" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +dependencies = [ + "openssl-probe 0.1.6", + "rustls 0.19.1", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" dependencies = [ - "openssl-probe", - "rustls 0.19.1", + "openssl-probe 0.2.1", + "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.5.1", ] [[package]] @@ -7391,6 +8556,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.36", + "rustls-native-certs 0.8.5", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.9", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -7407,6 +8599,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring 0.17.14", "rustls-pki-types", "untrusted 0.9.0", @@ -7468,6 +8661,40 @@ dependencies = [ "cipher", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "bitvec 1.0.1", + "cfg-if", + "derive_more 1.0.0", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "schannel" version = "0.1.28" @@ -7718,14 +8945,37 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", + "base16ct 0.2.0", "der 0.7.10", - "generic-array", + "generic-array 0.14.9", "pkcs8 0.10.2", "subtle", "zeroize", ] +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils", + "der 0.8.2", + "hybrid-array 0.4.15", + "subtle", + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -7733,13 +8983,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "num-bigint", "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.15.0" @@ -7799,6 +9062,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-human-bytes" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aff481ca1fe108deba0f217b45d9f1d494e7e7f906bcc7366d8a5648c5a1e65" +dependencies = [ + "base64 0.13.1", + "hex", + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -7902,6 +9176,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap 2.13.0", "itoa", "memchr", "serde", @@ -8034,6 +9309,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct 0.2.0", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -8041,7 +9326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -8053,7 +9338,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -8065,10 +9350,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + [[package]] name = "sha3" version = "0.10.8" @@ -8124,12 +9420,32 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd-adler32" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version 0.4.1", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -8263,6 +9579,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + [[package]] name = "spki" version = "0.5.4" @@ -8283,6 +9605,16 @@ dependencies = [ "der 0.7.10", ] +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", +] + [[package]] name = "sqlparser" version = "0.44.0" @@ -8420,10 +9752,10 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "generic-array", + "generic-array 0.14.9", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", "md-5", @@ -8466,7 +9798,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", @@ -8657,7 +9989,7 @@ dependencies = [ "iref", "k256", "keccak-hash", - "p256", + "p256 0.13.2", "pin-project", "rand 0.8.5", "ripemd160", @@ -8752,7 +10084,7 @@ dependencies = [ "linked-data", "locspan", "multibase 0.9.3", - "p256", + "p256 0.13.2", "pin-project", "rand 0.8.5", "rdf-types", @@ -8789,7 +10121,7 @@ dependencies = [ "digest 0.10.7", "getrandom 0.2.17", "hex", - "hmac", + "hmac 0.12.1", "iref", "linked-data", "rdf-types", @@ -8897,7 +10229,7 @@ dependencies = [ "num-bigint", "num-derive", "num-traits", - "p256", + "p256 0.13.2", "rand 0.8.5", "rsa 0.6.1", "serde", @@ -8924,7 +10256,7 @@ dependencies = [ "iref", "k256", "linked-data", - "p256", + "p256 0.13.2", "rand 0.8.5", "rsa 0.6.1", "serde", @@ -8969,8 +10301,8 @@ dependencies = [ "csv", "ed25519-dalek", "k256", - "p256", - "p384", + "p256 0.13.2", + "p384 0.13.1", "thiserror 1.0.69", "unsigned-varint 0.7.2", ] @@ -9152,7 +10484,7 @@ dependencies = [ "k256", "linked-data", "multibase 0.9.3", - "p256", + "p256 0.13.2", "pin-project", "rand_core 0.6.4", "rdf-types", @@ -9282,6 +10614,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "std-shims" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227c4f8561598188d0df96dbe749824576174bba278b5b6bb2eacff1066067d0" +dependencies = [ + "hashbrown 0.16.1", + "rustversion", + "spin 0.10.1", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -9386,6 +10729,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn-solidity" version = "0.7.7" @@ -9443,7 +10797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] @@ -9454,7 +10808,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -9478,6 +10843,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -9626,6 +10997,47 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinycloud-at-rest-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "clap", + "dcap-qvl", + "hex", + "rand 0.8.5", + "serde", + "serde_json", + "sha2 0.10.9", + "tinycloud-at-rest-escrow", + "tokio", + "zeroize", +] + +[[package]] +name = "tinycloud-at-rest-escrow" +version = "0.1.0" +dependencies = [ + "age", + "age-core", + "base64 0.22.1", + "bech32 0.11.1", + "hex", + "hkdf", + "hmac 0.12.1", + "hpke", + "p256 0.13.2", + "rand 0.8.5", + "serde", + "serde_json", + "sha2 0.10.9", + "subtle", + "thiserror 2.0.18", + "vsss-rs", + "zeroize", +] + [[package]] name = "tinycloud-auth" version = "1.3.0" @@ -9666,7 +11078,7 @@ dependencies = [ "futures", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "iri-string", "k256", "libp2p", @@ -9715,13 +11127,13 @@ dependencies = [ "chacha20poly1305", "chrono", "clap", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "ed25519-dalek", "flate2", "futures", "hex", - "hmac", + "hmac 0.12.1", "hyper 0.14.32", "if-addrs", "k256", @@ -9733,7 +11145,7 @@ dependencies = [ "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", - "pem", + "pem 3.0.6", "percent-encoding", "pin-project", "proc-macro2", @@ -9749,7 +11161,7 @@ dependencies = [ "rustls 0.23.36", "rustls-pemfile 2.2.0", "scrypt", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "serde", "serde_ipld_dagcbor 0.6.4", @@ -9879,6 +11291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -10307,6 +11720,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typed-builder" version = "0.14.0" @@ -10320,9 +11742,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ubyte" @@ -10378,6 +11800,31 @@ dependencies = [ "version_check", ] +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -10429,7 +11876,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -10541,6 +11988,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vsss-rs" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ec751bdcc8bda099e269b24cc6b4ad14f9ce8b0490c1599174070e792ecd70c" +dependencies = [ + "crypto-bigint 0.6.1", + "elliptic-curve 0.13.8", + "elliptic-curve-tools", + "generic-array 1.4.5", + "hex", + "hybrid-array 0.4.15", + "num", + "rand_core 0.6.4", + "serde", + "sha3", + "subtle", + "zeroize", +] + [[package]] name = "wait-timeout" version = "0.2.1" @@ -10556,6 +12023,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -10675,6 +12152,15 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -10709,6 +12195,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -10725,6 +12217,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -11075,6 +12576,18 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff 0.14.0", + "group 0.14.0", + "hybrid-array 0.4.15", + "primefield", +] + [[package]] name = "writeable" version = "0.6.2" @@ -11108,6 +12621,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-cert" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b" +dependencies = [ + "const-oid 0.10.2", + "der 0.8.2", + "spki 0.8.1", +] + [[package]] name = "x509-parser" version = "0.13.2" @@ -11119,7 +12643,7 @@ dependencies = [ "data-encoding", "der-parser 7.0.0", "lazy_static", - "nom", + "nom 7.1.3", "oid-registry 0.4.0", "rusticata-macros", "thiserror 1.0.69", @@ -11136,7 +12660,7 @@ dependencies = [ "data-encoding", "der-parser 9.0.0", "lazy_static", - "nom", + "nom 7.1.3", "oid-registry 0.7.1", "rusticata-macros", "thiserror 1.0.69", @@ -11299,6 +12823,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/Cargo.toml b/Cargo.toml index cd75bfb3..b9ceda0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ members = [ "tinycloud-verifier-wasm", "tinycloud-sdk-rs", "tinycloud-core", + "tinycloud-at-rest-escrow", + "tinycloud-at-rest-cli", "dependencies/siwe", "dependencies/siwe-recap", "dependencies/cacao", diff --git a/Dockerfile b/Dockerfile index 827f3ba5..60e35331 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,8 @@ COPY ./rust-toolchain.toml ./ COPY ./tinycloud-node-server/ ./tinycloud-node-server/ COPY ./tinycloud-auth/ ./tinycloud-auth/ COPY ./tinycloud-core/ ./tinycloud-core/ +COPY ./tinycloud-at-rest-escrow/ ./tinycloud-at-rest-escrow/ +COPY ./tinycloud-at-rest-cli/ ./tinycloud-at-rest-cli/ COPY ./tinycloud-sdk-rs/ ./tinycloud-sdk-rs/ COPY ./tinycloud-sdk-wasm/ ./tinycloud-sdk-wasm/ COPY ./tinycloud-verifier-wasm/ ./tinycloud-verifier-wasm/ diff --git a/docs/at-rest-escrow.md b/docs/at-rest-escrow.md new file mode 100644 index 00000000..b969b499 --- /dev/null +++ b/docs/at-rest-escrow.md @@ -0,0 +1,71 @@ +# At-rest key escrow (TC-887) + +Two workspace crates, both unused by the node binary so far: + +- `tinycloud-at-rest-escrow`: the escrow format. It splits the at-rest root key (ARK) 2-of-3 and seals the shares to age recipients. It also builds the bundle and its fingerprint, the canary, the verification record, and the recovery wire format. +- `tinycloud-at-rest-cli`: the `tinycloud-at-rest` tool that holders and operators run. + +Build the tool from the release tag you are verifying: + +```sh +cargo build --release -p tinycloud-at-rest-cli # target/release/tinycloud-at-rest +``` + +## Formats + +`lp(x)` is `u32 BE length ‖ x`. All digests are SHA-256 and written as lowercase hex. + +- **Share.** The ARK is split with Shamir over GF(256) (`vsss-rs` 5.4.0), threshold 2 of 3. Share `i` has x-coordinate `i`. The sealed plaintext is `"TCS1" ‖ ark_id(16) ‖ generation u32 BE ‖ i ‖ share(33)`, 58 bytes in all. +- **Recipients.** Each share is age-encrypted (`age` 0.12.1, ASCII armor) to one holder. + - `age1tag1…`: a P-256 hardware key. Use the `age1tag1` form that `age-plugin-yubikey` prints. age encrypts to it natively, and decryption needs the plugin and the key. + - `age1…` X25519: software keys only. The CLI warns when one is used. + - `age1yubikey1…` is refused, because encrypting to it needs the plugin. +- **Commitment.** `SHA-256(lp("tinycloud/at-rest/escrow-share-commit/v1") ‖ ark_id ‖ generation ‖ i ‖ lp(share))`. +- **Canary.** + - `canary = HKDF-SHA256(ikm = ARK, salt = ark_id, info = lp("tinycloud/at-rest/escrow-canary/v1") ‖ generation)`. + - `canary_fp = SHA-256(lp("tinycloud/at-rest/escrow-canary-fp/v1") ‖ ark_id ‖ generation ‖ lp(canary))`. +- **Bundle.** JSON with `version`, `ark_id`, `generation`, `threshold`, `canary_fp`, and `shares[{index, holder, recipient, commitment, ciphertext}]`. Unknown fields are rejected. +- **EFP (escrow fingerprint).** `SHA-256(lp("tinycloud/at-rest/escrow-bundle/v1") ‖ version ‖ ark_id ‖ generation ‖ threshold ‖ canary_fp ‖ n ‖ for each share: i ‖ lp(holder) ‖ lp(recipient) ‖ commitment ‖ SHA-256(ciphertext))`. +- **Verification record.** JSON that names the EFP, all three indices and all three pairs. It carries an HMAC-SHA256 under `HKDF(ARK, salt = ark_id, info = lp("tinycloud/at-rest/escrow-record/v1"))`, so only the ARK holder can make or check one. +- **Recovery binding.** `xyz.tinycloud.at-rest/recovery-key/v1\0:::`. The `\0` here is two characters. A TDX quote's REPORTDATA must be `SHA-256(binding) ‖ 32 zero bytes`. + +## Commands + +| Command | Reads | Prints | +|---|---|---| +| `recipients --holder NAME=RECIPIENT` ×3 | — | `[at_rest.escrow]` TOML and the compose env line | +| `nonce` | — | 32 random bytes, base64url | +| `inspect --bundle B [--expect-efp E]` | bundle | EFP, ark_id, generation, canary_fp, holders | +| `extract-share --bundle B --holder H` | bundle | holder H's armored age file | +| `check-share --bundle B` | decrypted share on **stdin** | which share it is; refuses on a commitment mismatch | +| `verify-node --bundle B --recovery R --nonce N --trust T [--collateral C \| --pccs-url U]` | node's recovery response | OK or REFUSED | +| `share-wrap` (same flags) | decrypted share on **stdin** | after `verify-node` passes: the share sealed to the node's recovery key | +| `verify-record --bundle B --record V` | record | `TINYCLOUD_AT_REST__ESCROW__VERIFIED_FP=` | + +A decrypted share only ever arrives on stdin (`age -d -i share.age | tinycloud-at-rest …`). It never goes on argv or to a file. + +### What `verify-node` and `share-wrap` check before releasing a share + +All of these must hold, or the command prints `REFUSED: …` and exits 1: + +1. The node's `efp`, `ark_id` and `generation` equal the holder's own bundle. +2. The node's binding is exactly the one recomputed from the recovery recipient, that EFP, and the holder's nonce. The nonce must be at least 22 characters. A response made for any other nonce is stale. +3. The quote passes Intel DCAP verification with `dcap-qvl` 0.7.0. That covers the signature, the PCK chain, the CRLs, the TCB info and the QE identity. Collateral comes from the PCCS (Phala's by default) or from `--collateral`, checked at the current time. +4. The TCB status is in the trust policy's `allowed_tcb_status` (default `["UpToDate"]`), and the TD debug bit is clear. +5. MRTD and RTMR0-2 equal the trust policy. +6. The dstack event log replays to the quoted RTMR3. Its single `app-id` and `compose-hash` events come before `boot-mr-done` and equal the trust policy. +7. The quoted REPORTDATA is `SHA-256(binding) ‖ 0³²`. + +Trust policy (`--trust`), from a source independent of the node being checked: + +```json +{ + "app_id": "<40 hex>", + "compose_hash": "<64 hex: SHA-256 of app-compose.json>", + "mrtd": "<96 hex>", + "rtmr0": "<96 hex>", + "rtmr1": "<96 hex>", + "rtmr2": "<96 hex>", + "allowed_tcb_status": ["UpToDate"] +} +``` diff --git a/release-plz.toml b/release-plz.toml index d0a52710..5a55e262 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -71,6 +71,16 @@ name = "tinycloud-sdk-rs" release = false publish = false +[[package]] +name = "tinycloud-at-rest-escrow" +release = false +publish = false + +[[package]] +name = "tinycloud-at-rest-cli" +release = false +publish = false + [[package]] name = "tinycloud-sdk-wasm" release = false diff --git a/tinycloud-at-rest-cli/Cargo.toml b/tinycloud-at-rest-cli/Cargo.toml new file mode 100644 index 00000000..c650adff --- /dev/null +++ b/tinycloud-at-rest-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tinycloud-at-rest-cli" +version = "0.1.0" +edition = "2021" +publish = false +description = "Escrow ceremony and recovery tool for TinyCloud encryption at rest (TC-887)" + +[[bin]] +name = "tinycloud-at-rest" +path = "src/main.rs" + +[dependencies] +tinycloud-at-rest-escrow = { path = "../tinycloud-at-rest-escrow" } +# Intel DCAP quote verification (Phala), pinned: it decides whether a holder +# releases a share. +dcap-qvl = { version = "=0.7.0" } +anyhow = "1" +clap = { version = "4", features = ["derive"] } +base64 = { workspace = true } +hex = { workspace = true } +rand = "0.8" +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = "0.10" +tokio = { workspace = true } +zeroize = "1" diff --git a/tinycloud-at-rest-cli/src/attest.rs b/tinycloud-at-rest-cli/src/attest.rs new file mode 100644 index 00000000..02882cc5 --- /dev/null +++ b/tinycloud-at-rest-cli/src/attest.rs @@ -0,0 +1,605 @@ +//! Deciding whether a TEE's recovery key is safe to release a share to. +//! +//! A holder releases a share only to a recovery recipient that a genuine, +//! up-to-date, non-debug TDX quote binds to, from the exact measured +//! deployment the holder pinned in a trust policy. The steps follow +//! `docs/attestation.md` and the registry's dstack verifier: +//! +//! 1. DCAP: quote signature, PCK chain, CRLs, TCB info and QE identity, with +//! collateral (`dcap-qvl`). TCB status must be in the policy's allow-list. +//! 2. TD attributes: the debug bit must be clear. +//! 3. MRTD and RTMR0-2 equal the pinned OS-image measurements. +//! 4. The dstack event log replays to the quoted RTMR3, and its `app-id` and +//! `compose-hash` events equal the pinned values. +//! 5. REPORTDATA equals SHA-256 of the binding the holder recomputes from +//! their own fresh nonce, the bundle's EFP and the recovery recipient. + +use anyhow::{anyhow, bail, ensure, Context, Result}; +use serde::Deserialize; +use sha2::{Digest, Sha384}; +use tinycloud_at_rest_escrow::{ + binding_digest, recovery_binding, validate_nonce, EscrowBundle, RecoveryInfo, +}; + +/// Measurements a holder trusts, from a source independent of the node +/// being verified (the release's published measurements, Phala's dashboard +/// cross-checked by Sam). Hex, lowercase. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TrustPolicy { + /// dstack app id (20 bytes). + pub app_id: String, + /// SHA-256 of the deployed app-compose.json. + pub compose_hash: String, + /// dstack OS image measurements (48 bytes each). + pub mrtd: String, + pub rtmr0: String, + pub rtmr1: String, + pub rtmr2: String, + /// Accepted DCAP TCB statuses. Default: only `UpToDate`. + #[serde(default = "default_tcb_status")] + pub allowed_tcb_status: Vec, +} + +fn default_tcb_status() -> Vec { + vec!["UpToDate".into()] +} + +fn hex_exact(value: &str, bytes: usize, label: &str) -> Result> { + ensure!( + value.len() == bytes * 2 + && value + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)), + "{label} must be {bytes} bytes of lowercase hex" + ); + Ok(hex::decode(value)?) +} + +impl TrustPolicy { + pub fn validate(&self) -> Result<()> { + hex_exact(&self.app_id, 20, "trust policy app_id")?; + hex_exact(&self.compose_hash, 32, "trust policy compose_hash")?; + for (v, l) in [ + (&self.mrtd, "mrtd"), + (&self.rtmr0, "rtmr0"), + (&self.rtmr1, "rtmr1"), + (&self.rtmr2, "rtmr2"), + ] { + hex_exact(v, 48, &format!("trust policy {l}"))?; + } + ensure!( + !self.allowed_tcb_status.is_empty(), + "trust policy allowed_tcb_status is empty" + ); + Ok(()) + } +} + +/// The fields of a DCAP-verified TD report this tool decides on. +#[derive(Clone, Debug)] +pub struct AttestedTd { + pub tcb_status: String, + pub td_attributes: [u8; 8], + pub mr_td: [u8; 48], + pub rtmr: [[u8; 48]; 4], + pub report_data: [u8; 64], +} + +/// Step 1: full DCAP verification of `quote` against `collateral` at `now`. +pub fn verify_quote( + quote: &[u8], + collateral: &dcap_qvl::QuoteCollateralV3, + now_secs: u64, +) -> Result { + let verified = dcap_qvl::verify::verify(quote, collateral, now_secs) + .map_err(|e| anyhow!("TDX quote failed DCAP verification: {e}"))?; + let td = verified + .report + .as_td10() + .ok_or_else(|| anyhow!("quote is not a TDX quote"))?; + Ok(AttestedTd { + tcb_status: verified.status.clone(), + td_attributes: td.td_attributes, + mr_td: td.mr_td, + rtmr: [td.rt_mr0, td.rt_mr1, td.rt_mr2, td.rt_mr3], + report_data: td.report_data, + }) +} + +/// Fetch collateral for `quote` from a PCCS (default: Phala's). +pub async fn fetch_collateral(quote: &[u8], pccs_url: &str) -> Result { + dcap_qvl::collateral::CollateralClient::with_default_http(pccs_url) + .map_err(|e| anyhow!("PCCS client: {e}"))? + .fetch(quote) + .await + .map_err(|e| anyhow!("fetching DCAP collateral from {pccs_url}: {e}")) +} + +const RUNTIME_EVENT_TYPE: u32 = 0x0800_0001; + +#[derive(Debug, Deserialize)] +struct Event { + imr: u32, + event_type: u32, + #[serde(default)] + digest: String, + #[serde(default)] + event: String, + #[serde(default)] + event_payload: String, + #[serde(default)] + version: Option, + #[serde(default)] + preimage: Option, +} + +fn runtime_event_digest(index: usize, e: &Event) -> Result<[u8; 48]> { + let payload = hex::decode(&e.event_payload) + .with_context(|| format!("runtime event {index} payload is not hex"))?; + let preimage = match e.version.unwrap_or(1) { + 1 => { + let mut p = RUNTIME_EVENT_TYPE.to_le_bytes().to_vec(); + p.push(b':'); + p.extend_from_slice(e.event.as_bytes()); + p.push(b':'); + p.extend_from_slice(&payload); + p + } + 2 => { + // dstack v2: canonical JSON {"name","payload","type"}; the log + // carries it and it must match exactly. + let canonical = serde_json::json!({ + "name": e.event, + "payload": hex::encode(&payload), + "type": RUNTIME_EVENT_TYPE, + }) + .to_string() + .into_bytes(); + let given = e + .preimage + .as_deref() + .ok_or_else(|| anyhow!("runtime event {index} has no preimage"))?; + ensure!( + hex::decode(given).ok().as_deref() == Some(canonical.as_slice()), + "runtime event {index} preimage is not canonical" + ); + canonical + } + _ => bail!("runtime event {index}: unsupported dstack event-log version"), + }; + Ok(Sha384::digest(&preimage).into()) +} + +/// Replay the RTMR3 events of a dstack event log. +fn replay_rtmr3(events: &[Event]) -> Result<[u8; 48]> { + let mut register = [0u8; 48]; + let mut runtime = 0usize; + for (index, e) in events.iter().enumerate() { + ensure!(e.imr <= 3, "event log entry {index} has an invalid IMR"); + if e.imr != 3 { + continue; + } + runtime += 1; + ensure!( + e.event_type == RUNTIME_EVENT_TYPE, + "runtime event {index} has an unsupported type" + ); + let rebuilt = runtime_event_digest(index, e)?; + // dstack's stripped log leaves runtime digests empty; the authenticated + // value is rebuilt from the payload either way. + if !e.digest.is_empty() { + ensure!( + hex::decode(&e.digest).ok().as_deref() == Some(rebuilt.as_slice()), + "runtime event {index} digest does not bind its contents" + ); + } + let mut h = Sha384::new(); + h.update(register); + h.update(rebuilt); + register = h.finalize().into(); + } + ensure!(runtime > 0, "dstack event log has no RTMR3 events"); + Ok(register) +} + +/// The single `name` event, measured before `boot-mr-done`, must carry `expected`. +fn require_boot_event(events: &[Event], name: &str, expected: &str) -> Result<()> { + let runtime: Vec<&Event> = events.iter().filter(|e| e.imr == 3).collect(); + let position = |n: &str| runtime.iter().position(|e| e.event == n); + let count = |n: &str| runtime.iter().filter(|e| e.event == n).count(); + ensure!( + count("boot-mr-done") == 1 && count("system-ready") == 1, + "dstack boot markers must each appear once" + ); + let boot_done = position("boot-mr-done").expect("counted"); + let system_ready = position("system-ready").expect("counted"); + ensure!( + boot_done > 0 && system_ready > boot_done, + "dstack boot event ordering is invalid" + ); + ensure!( + count(name) == 1, + "dstack event log must contain exactly one {name} event" + ); + let at = position(name).expect("counted"); + ensure!( + at < boot_done, + "{name} must be measured before boot completion" + ); + ensure!( + runtime[at].event_payload == expected, + "{name} in the attested event log is {}, but the trust policy pins {expected}", + runtime[at].event_payload + ); + Ok(()) +} + +/// Smallest nonce accepted for freshness: 128 bits as base64url. +pub const MIN_NONCE_LEN: usize = 22; + +/// Steps 2-5, plus agreement between the node's answer and the bundle. +/// `nonce` must be the one this holder generated for this request. +pub fn check_recovery( + info: &RecoveryInfo, + bundle: &EscrowBundle, + nonce: &str, + trust: &TrustPolicy, + td: &AttestedTd, +) -> Result<()> { + trust.validate()?; + bundle.validate()?; + validate_nonce(nonce)?; + ensure!( + nonce.len() >= MIN_NONCE_LEN, + "nonce must be at least {MIN_NONCE_LEN} chars; use `tinycloud-at-rest nonce`" + ); + let efp = bundle.fingerprint(); + ensure!( + info.efp == efp && info.ark_id == bundle.ark_id && info.generation == bundle.generation, + "the node's escrow ({}) is not the bundle you hold ({efp})", + info.efp + ); + + // Step 5 first on the cheap side: the node's own binding must be the one + // we expect, for our nonce. A reply made for another nonce is stale. + let expected = recovery_binding(&info.recovery_recipient, &efp, nonce); + ensure!( + info.binding == expected, + "the node's binding is not for your nonce: stale or replayed response; request a new one" + ); + let digest = binding_digest(&expected); + + ensure!( + trust.allowed_tcb_status.contains(&td.tcb_status), + "TDX TCB status is {}, not one of {:?}", + td.tcb_status, + trust.allowed_tcb_status + ); + ensure!( + td.td_attributes[0] & 0x01 == 0, + "the TD is a debug TD (TUD.DEBUG set); its memory is readable by the host" + ); + for (got, want, label) in [ + (&td.mr_td, &trust.mrtd, "MRTD"), + (&td.rtmr[0], &trust.rtmr0, "RTMR0"), + (&td.rtmr[1], &trust.rtmr1, "RTMR1"), + (&td.rtmr[2], &trust.rtmr2, "RTMR2"), + ] { + ensure!( + hex::encode(got) == *want, + "{label} is {}, but the trust policy pins {want}", + hex::encode(got) + ); + } + + let log = info + .event_log + .as_deref() + .ok_or_else(|| anyhow!("the node sent no event log"))?; + let events: Vec = serde_json::from_str(log).context("event log is not a JSON array")?; + ensure!( + replay_rtmr3(&events)? == td.rtmr[3], + "event log does not replay to the quoted RTMR3" + ); + require_boot_event(&events, "app-id", &trust.app_id)?; + require_boot_event(&events, "compose-hash", &trust.compose_hash)?; + + let mut want_report = [0u8; 64]; + want_report[..32].copy_from_slice(&digest.0); + ensure!( + td.report_data == want_report, + "the quote's REPORTDATA does not bind this recovery key and your nonce: replayed or foreign quote" + ); + ensure!( + info.binding_sha256 == digest, + "binding_sha256 does not match binding" + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tinycloud_at_rest_escrow::{Ark, ArkId, Digest32, HolderRecipient, RecoveryIdentity}; + + const TDX_QUOTE: &[u8] = include_bytes!("../tests/fixtures/tdx_quote"); + const TDX_COLLATERAL: &[u8] = include_bytes!("../tests/fixtures/tdx_quote_collateral.json"); + const TDX_QUOTE_OUTDATED: &[u8] = include_bytes!("../tests/fixtures/tdx_quote_outdated"); + const TDX_COLLATERAL_OUTDATED: &[u8] = + include_bytes!("../tests/fixtures/tdx_quote_outdated_collateral.json"); + // Inside every validity window of the fixture collateral (TCB info and QE + // identity issued 2025-06-19 / 2026-02-18, valid 30 days). + const FIXTURE_NOW: u64 = 1_750_377_600; // 2025-06-20T00:00:00Z + const FIXTURE_NOW_OUTDATED: u64 = 1_771_459_200; // 2026-02-19T00:00:00Z + + const NONCE: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + const APP_ID: &str = "1111111111111111111111111111111111111111"; + const COMPOSE: &str = "2222222222222222222222222222222222222222222222222222222222222222"; + + fn bundle() -> EscrowBundle { + let holders: Vec = ["a", "b", "c"] + .into_iter() + .map(|h| HolderRecipient { + holder: h.into(), + recipient: RecoveryIdentity::generate().recipient(), + }) + .collect(); + EscrowBundle::create(&Ark::generate(), ArkId::generate(), 1, &holders).unwrap() + } + + fn event(name: &str, payload: &str) -> serde_json::Value { + serde_json::json!({ + "imr": 3, "event_type": RUNTIME_EVENT_TYPE, "digest": "", + "event": name, "event_payload": payload, + }) + } + + fn event_log(app_id: &str, compose: &str) -> String { + serde_json::json!([ + {"imr": 0, "event_type": 1, "digest": "00", "event": "", "event_payload": ""}, + event("system-preparing", ""), + event("app-id", app_id), + event("compose-hash", compose), + event("instance-id", "33"), + event("boot-mr-done", ""), + event("system-ready", ""), + ]) + .to_string() + } + + /// A consistent, passing scenario built on synthetic measurements. + struct Scenario { + bundle: EscrowBundle, + info: RecoveryInfo, + trust: TrustPolicy, + td: AttestedTd, + } + + fn scenario() -> Scenario { + let bundle = bundle(); + let tee = RecoveryIdentity::generate(); + let binding = recovery_binding(&tee.recipient(), &bundle.fingerprint(), NONCE); + let digest = binding_digest(&binding); + let log = event_log(APP_ID, COMPOSE); + let events: Vec = serde_json::from_str(&log).unwrap(); + let mut report_data = [0u8; 64]; + report_data[..32].copy_from_slice(&digest.0); + let td = AttestedTd { + tcb_status: "UpToDate".into(), + td_attributes: [0; 8], + mr_td: [0xa0; 48], + rtmr: [ + [0xa1; 48], + [0xa2; 48], + [0xa3; 48], + replay_rtmr3(&events).unwrap(), + ], + report_data, + }; + let trust = TrustPolicy { + app_id: APP_ID.into(), + compose_hash: COMPOSE.into(), + mrtd: hex::encode([0xa0; 48]), + rtmr0: hex::encode([0xa1; 48]), + rtmr1: hex::encode([0xa2; 48]), + rtmr2: hex::encode([0xa3; 48]), + allowed_tcb_status: default_tcb_status(), + }; + let info = RecoveryInfo { + mode: "dstack".into(), + state: "unlocked".into(), + ark_id: bundle.ark_id, + generation: bundle.generation, + efp: bundle.fingerprint(), + canary_fp: bundle.canary_fp, + recovery_recipient: tee.recipient(), + binding: binding.clone(), + binding_sha256: digest, + quote: Some(String::new()), + event_log: Some(log), + shares_held: vec![], + }; + Scenario { + bundle, + info, + trust, + td, + } + } + + fn check(s: &Scenario, nonce: &str) -> Result<()> { + check_recovery(&s.info, &s.bundle, nonce, &s.trust, &s.td) + } + + fn assert_refused(s: &Scenario, nonce: &str, needle: &str) { + let err = check(s, nonce).expect_err("must refuse").to_string(); + assert!(err.contains(needle), "{err:?} lacks {needle:?}"); + } + + #[test] + fn consistent_attestation_passes() { + check(&scenario(), NONCE).unwrap(); + } + + #[test] + fn stale_nonce_is_refused() { + // The node answered for an older nonce; the holder checks with theirs. + let s = scenario(); + assert_refused( + &s, + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "not for your nonce", + ); + assert_refused(&s, "short", "at least 22"); + } + + #[test] + fn replayed_quote_with_other_report_data_is_refused() { + // A genuine quote for a different binding, with the node's JSON + // rewritten to name the attacker's recipient. + let mut s = scenario(); + let attacker = RecoveryIdentity::generate().recipient(); + s.info.recovery_recipient = attacker.clone(); + s.info.binding = recovery_binding(&attacker, &s.bundle.fingerprint(), NONCE); + s.info.binding_sha256 = binding_digest(&s.info.binding); + assert_refused(&s, NONCE, "REPORTDATA does not bind"); + } + + #[test] + fn wrong_measurements_are_refused() { + let base = scenario(); + let mut s = scenario(); + s.td = base.td.clone(); + s.td.mr_td[0] ^= 1; + s.info = base.info.clone(); + s.bundle = base.bundle.clone(); + assert_refused(&s, NONCE, "MRTD"); + + for (i, label) in [(0usize, "RTMR0"), (1, "RTMR1"), (2, "RTMR2")] { + let mut s = scenario(); + s.td.rtmr[i][47] ^= 1; + assert_refused(&s, NONCE, label); + } + + let mut s = scenario(); + s.td.rtmr[3][0] ^= 1; + assert_refused(&s, NONCE, "replay to the quoted RTMR3"); + + // Another app (its own consistent log and RTMR3) and another compose. + for (app, compose, needle) in [ + ( + "9999999999999999999999999999999999999999", + COMPOSE, + "app-id", + ), + ( + APP_ID, + "9999999999999999999999999999999999999999999999999999999999999999", + "compose-hash", + ), + ] { + let mut s = scenario(); + let log = event_log(app, compose); + s.td.rtmr[3] = + replay_rtmr3(&serde_json::from_str::>(&log).unwrap()).unwrap(); + s.info.event_log = Some(log); + assert_refused(&s, NONCE, needle); + } + } + + #[test] + fn debug_td_and_bad_tcb_status_are_refused() { + let mut s = scenario(); + s.td.td_attributes[0] |= 1; + assert_refused(&s, NONCE, "debug TD"); + let mut s = scenario(); + s.td.tcb_status = "OutOfDate".into(); + assert_refused(&s, NONCE, "TCB status"); + } + + #[test] + fn node_escrow_must_match_the_holders_bundle() { + let mut s = scenario(); + s.info.efp = Digest32([7; 32]); + assert_refused(&s, NONCE, "not the bundle you hold"); + } + + #[test] + fn tampered_event_log_is_refused() { + let mut s = scenario(); + let mut events: Vec = + serde_json::from_str(s.info.event_log.as_deref().unwrap()).unwrap(); + events[2]["digest"] = serde_json::json!("00".repeat(48)); + s.info.event_log = Some(serde_json::Value::Array(events).to_string()); + assert_refused(&s, NONCE, "digest does not bind"); + } + + fn collateral(bytes: &[u8]) -> dcap_qvl::QuoteCollateralV3 { + serde_json::from_slice(bytes).unwrap() + } + + #[test] + fn genuine_quote_verifies_but_is_refused_as_foreign() { + // A real Intel-signed TDX quote passes DCAP, but its REPORTDATA binds + // someone else's data, so it can't vouch for our recovery key. + let td = verify_quote(TDX_QUOTE, &collateral(TDX_COLLATERAL), FIXTURE_NOW).unwrap(); + let mut s = scenario(); + s.trust.mrtd = hex::encode(td.mr_td); + s.trust.rtmr0 = hex::encode(td.rtmr[0]); + s.trust.rtmr1 = hex::encode(td.rtmr[1]); + s.trust.rtmr2 = hex::encode(td.rtmr[2]); + s.trust.allowed_tcb_status = vec![td.tcb_status.clone()]; + let log_rtmr3 = s.td.rtmr[3]; + s.td = td; + s.td.rtmr[3] = log_rtmr3; + assert_refused(&s, NONCE, "REPORTDATA does not bind"); + } + + #[test] + fn forged_quote_fails_dcap() { + // REPORTDATA, MRTD, and the ECDSA signature (after the 48-byte header, + // 584-byte TD body and 4-byte signature length). + for offset in [48 + 520, 48 + 136, 48 + 584 + 4 + 10] { + let mut forged = TDX_QUOTE.to_vec(); + forged[offset] ^= 0x01; + let err = verify_quote(&forged, &collateral(TDX_COLLATERAL), FIXTURE_NOW) + .expect_err("forged quote must fail"); + assert!(err.to_string().contains("DCAP"), "{err}"); + } + } + + #[test] + fn quote_outside_collateral_validity_fails_dcap() { + assert!(verify_quote( + TDX_QUOTE, + &collateral(TDX_COLLATERAL), + FIXTURE_NOW + 10 * 365 * 86_400 + ) + .is_err()); + } + + #[test] + fn outdated_platform_quote_is_refused() { + // A real quote from a platform whose TCB level the collateral no + // longer lists. DCAP refuses it outright. + let err = verify_quote( + TDX_QUOTE_OUTDATED, + &collateral(TDX_COLLATERAL_OUTDATED), + FIXTURE_NOW_OUTDATED, + ) + .expect_err("outdated platform must be refused"); + assert!(err.to_string().contains("DCAP"), "{err}"); + } + + #[test] + fn trust_policy_rejects_malformed_pins() { + let mut t = scenario().trust; + t.mrtd = "AB".repeat(48); + assert!(t.validate().is_err()); + let mut t = scenario().trust; + t.allowed_tcb_status.clear(); + assert!(t.validate().is_err()); + assert!(serde_json::from_str::(r#"{"app_id":"x","extra":1}"#).is_err()); + } +} diff --git a/tinycloud-at-rest-cli/src/main.rs b/tinycloud-at-rest-cli/src/main.rs new file mode 100644 index 00000000..d18720a8 --- /dev/null +++ b/tinycloud-at-rest-cli/src/main.rs @@ -0,0 +1,289 @@ +//! `tinycloud-at-rest`: escrow ceremony and recovery tool (TC-887). +//! +//! Secrets are never accepted on argv. A decrypted share is read from stdin +//! only (pipe it from `age -d`), and nothing here writes one to disk. + +mod attest; + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use base64::Engine; +use clap::{Args, Parser, Subcommand}; +use tinycloud_at_rest_escrow::{ + validate_holders, wrap_share, EscrowBundle, HolderRecipient, RecipientKind, RecoveryInfo, + VerificationRecord, +}; +use zeroize::Zeroizing; + +use attest::TrustPolicy; + +#[derive(Parser)] +#[command( + name = "tinycloud-at-rest", + about = "TinyCloud at-rest key escrow ceremony and recovery" +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Validate the three holder recipients and print the node config for them. + Recipients { + /// `NAME=RECIPIENT`, three times (e.g. `sam=age1tag1…`). + #[arg(long = "holder", required = true)] + holders: Vec, + }, + /// Print a fresh 256-bit nonce for a recovery request. + Nonce, + /// Validate a bundle and print its fingerprint, canary and holders. + Inspect { + #[arg(long)] + bundle: PathBuf, + /// Fail unless the bundle's EFP equals this value. + #[arg(long)] + expect_efp: Option, + }, + /// Print one holder's sealed share (an armored age file) to stdout. + ExtractShare { + #[arg(long)] + bundle: PathBuf, + #[arg(long)] + holder: String, + }, + /// Offline check: read a decrypted share on stdin (from `age -d`) and + /// check it against the bundle's commitment. Prints no share bytes. + CheckShare { + #[arg(long)] + bundle: PathBuf, + }, + /// Verify a node's recovery key: DCAP quote, measurements, event log, + /// and the binding to your nonce. Releases nothing. + VerifyNode(NodeArgs), + /// Verify the node exactly as `verify-node` does, then read a decrypted + /// share on stdin, check it, and print it sealed to the node's recovery key. + ShareWrap(NodeArgs), + /// Check that a verification record covers the bundle and print the value + /// to pin as `at_rest.escrow.verified_fp`. + VerifyRecord { + #[arg(long)] + bundle: PathBuf, + #[arg(long)] + record: PathBuf, + }, +} + +#[derive(Args)] +struct NodeArgs { + /// The escrow bundle you were given (not the node's copy). + #[arg(long)] + bundle: PathBuf, + /// The node's `GET /at-rest/recovery?nonce=…` response. + #[arg(long)] + recovery: PathBuf, + /// The nonce you generated for that request. + #[arg(long)] + nonce: String, + /// Trusted measurements (JSON; see ESCROW-CEREMONY.md). + #[arg(long)] + trust: PathBuf, + /// DCAP collateral JSON for offline verification. Without it, collateral + /// is fetched from `--pccs-url`. + #[arg(long)] + collateral: Option, + #[arg(long, default_value = dcap_qvl::collateral::PHALA_PCCS_URL)] + pccs_url: String, +} + +fn read_json(path: &Path, what: &str) -> Result { + let bytes = + std::fs::read(path).with_context(|| format!("reading {what} {}", path.display()))?; + serde_json::from_slice(&bytes).with_context(|| format!("parsing {what} {}", path.display())) +} + +fn read_bundle(path: &Path) -> Result { + let bundle: EscrowBundle = read_json(path, "bundle")?; + bundle.validate()?; + Ok(bundle) +} + +/// A decrypted share from stdin, bounded and zeroized. +fn read_share_stdin(bundle: &EscrowBundle) -> Result { + let mut buf = Zeroizing::new(Vec::with_capacity(64)); + std::io::stdin() + .lock() + .take(4096) + .read_to_end(&mut buf) + .context("reading the decrypted share from stdin")?; + if buf.is_empty() { + bail!("no share on stdin; pipe it from `age -d -i `"); + } + Ok(bundle.open_share(&buf)?) +} + +async fn verify_node(args: &NodeArgs) -> Result<(EscrowBundle, RecoveryInfo)> { + let bundle = read_bundle(&args.bundle)?; + let info: RecoveryInfo = read_json(&args.recovery, "recovery response")?; + let trust: TrustPolicy = read_json(&args.trust, "trust policy")?; + trust.validate()?; + if info.mode != "dstack" { + bail!( + "the node is not in a TEE (mode {:?}); a share is only released to an attested TEE", + info.mode + ); + } + let quote = + hex::decode(info.quote.as_deref().unwrap_or_default()).context("quote is not hex")?; + if quote.is_empty() { + bail!("the node sent no TDX quote"); + } + let collateral = match &args.collateral { + Some(path) => read_json(path, "collateral")?, + None => attest::fetch_collateral("e, &args.pccs_url).await?, + }; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + let td = attest::verify_quote("e, &collateral, now)?; + attest::check_recovery(&info, &bundle, &args.nonce, &trust, &td)?; + eprintln!( + "OK: genuine TDX quote ({}), measurements match the trust policy, recovery key {} bound to your nonce and escrow {}", + td.tcb_status, info.recovery_recipient, info.efp + ); + Ok((bundle, info)) +} + +fn print_holders(bundle: &EscrowBundle) { + for s in &bundle.shares { + println!( + "share {} holder {:<10} recipient {}", + s.index, s.holder, s.recipient + ); + } +} + +async fn run(cli: Cli) -> Result<()> { + match cli.command { + Command::Recipients { holders } => { + let parsed = holders + .iter() + .map(|h| { + let (holder, recipient) = h + .split_once('=') + .context("each --holder must be NAME=RECIPIENT")?; + Ok(HolderRecipient { + holder: holder.into(), + recipient: recipient.into(), + }) + }) + .collect::>>()?; + let kinds = validate_holders(&parsed)?; + for (h, kind) in parsed.iter().zip(&kinds) { + if *kind == RecipientKind::SoftwareX25519 { + eprintln!( + "WARNING: holder {:?} uses an X25519 (software) recipient, not a hardware key", + h.holder + ); + } + } + let inline = parsed + .iter() + .map(|h| format!("{{holder=\"{}\",recipient=\"{}\"}}", h.holder, h.recipient)) + .collect::>() + .join(","); + println!("# tinycloud.toml"); + println!("[at_rest.escrow]"); + println!("recipients = ["); + for h in &parsed { + println!( + " {{ holder = \"{}\", recipient = \"{}\" }},", + h.holder, h.recipient + ); + } + println!("]"); + println!(); + println!("# or compose environment"); + println!("TINYCLOUD_AT_REST__ESCROW__RECIPIENTS=[{inline}]"); + } + Command::Nonce => { + let mut bytes = [0u8; 32]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut bytes); + println!( + "{}", + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) + ); + } + Command::Inspect { bundle, expect_efp } => { + let bundle = read_bundle(&bundle)?; + let efp = bundle.fingerprint(); + println!("efp {efp}"); + println!("ark_id {}", bundle.ark_id); + println!("generation {}", bundle.generation); + println!("canary_fp {}", bundle.canary_fp); + print_holders(&bundle); + if let Some(expected) = expect_efp { + if expected != efp.to_string() { + bail!("bundle EFP {efp} is not the expected {expected}"); + } + eprintln!("OK: EFP matches"); + } + } + Command::ExtractShare { bundle, holder } => { + let bundle = read_bundle(&bundle)?; + let share = bundle + .share_for(&holder) + .with_context(|| format!("no holder {holder:?} in this bundle"))?; + std::io::stdout().write_all(share.ciphertext.as_bytes())?; + } + Command::CheckShare { bundle } => { + let bundle = read_bundle(&bundle)?; + let share = read_share_stdin(&bundle)?; + let holder = &bundle.shares[usize::from(share.index) - 1].holder; + eprintln!( + "OK: share {} ({holder}) matches escrow {} (ARK {})", + share.index, + bundle.fingerprint(), + bundle.ark_id + ); + } + Command::VerifyNode(args) => { + verify_node(&args).await?; + } + Command::ShareWrap(args) => { + let (bundle, info) = verify_node(&args).await?; + let share = read_share_stdin(&bundle)?; + let wrapped = wrap_share(&info.recovery_recipient, &share)?; + std::io::stdout().write_all(wrapped.as_bytes())?; + eprintln!( + "OK: share {} sealed to the node's recovery key; POST it to /at-rest/recovery/shares", + share.index + ); + } + Command::VerifyRecord { bundle, record } => { + let bundle = read_bundle(&bundle)?; + let record: VerificationRecord = read_json(&record, "verification record")?; + record.covers(&bundle)?; + eprintln!( + "OK: record covers shares {:?} and pairs {:?} of escrow {} (verified {})", + record.verified_indices, record.pairs, record.efp, record.verified_at + ); + println!("TINYCLOUD_AT_REST__ESCROW__VERIFIED_FP={}", record.efp); + } + } + Ok(()) +} + +fn main() { + let cli = Cli::parse(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("tokio runtime"); + if let Err(e) = runtime.block_on(run(cli)) { + eprintln!("REFUSED: {e:#}"); + std::process::exit(1); + } +} diff --git a/tinycloud-at-rest-cli/tests/fixtures/NOTICE.md b/tinycloud-at-rest-cli/tests/fixtures/NOTICE.md new file mode 100644 index 00000000..cd896cbe --- /dev/null +++ b/tinycloud-at-rest-cli/tests/fixtures/NOTICE.md @@ -0,0 +1,5 @@ +`tdx_quote*` and `tdx_quote*_collateral.json` are copied unchanged from +`dcap-qvl` 0.7.0 `sample/` (MIT License, Copyright (c) 2026 Phala Network, +https://github.com/Phala-Network/dcap-qvl). They are real Intel-signed TDX +quotes with their DCAP collateral, used to test that genuine quotes verify and +that forged, foreign or outdated ones are refused. diff --git a/tinycloud-at-rest-cli/tests/fixtures/tdx_quote b/tinycloud-at-rest-cli/tests/fixtures/tdx_quote new file mode 100644 index 0000000000000000000000000000000000000000..667f510d98591f0b76d0dfb3b9462ae2edf71e13 GIT binary patch literal 5006 zcmcIn3$PQ_8NP54?Bcs3P{m#=ts_CRdEI+cW#F9M&1N^*P4<~=bg0QDck|B8%}q90 zpy*V``WP5{p;oWpR1mBqI3rc3YFm+_XdS2(1xJz26wCOaj-6W6QQDK_UI=(;L8d#C z+2lLtKj;6?`Tqa=Go@>C*DYP`XXO4N*Wm|(zgmTzyJhwAsndQsx~%m1{<&v8vFH)} z;KIijG-pkn)IB!4{koz5uRl!QzWT1AZCmG!taxV6Jt2`p}*) zj@;?#c^Ph)vVPjT-J1*BKU;nkyzBe@w>xG%eR^B{|7VlW&tBr4v3#G!y-L=WugOi{ za_>@Wg?q`ZKl|*eBWu>)=zr{?(eF*a`KgB&?Yb{8`ttK#w^H*b4gBQl-Oo*ZY=3lP z&y7DAc1;Q2S6Q`Io&Eb;Hf-Gd$hIrDRwj+&BX97LwIlVtt|K4Me~Y^K&UGs`&6nPP z`jh_I*rOkQsmytG@6})SSnHm-`RTI<_qe9HlGje!eYeATx)I;M#((f%ji1BUU)#c8 zzIZ0w{p{p@g?-Ab#MUe3c(zUX{G)Xrzy0f5Zo2G}>pz`4^5y~dkn+am+eUvZtladQ z`8IK3&#UM*KT$u}v1!tt2bb;~Sb0-dboLYWD})VkWj($rWUcPs>~NjG<|}#b!9QFS zAUCA{c<+&G)~|>UiGPoMm=Jfb7@7OtOLuO+ZS+BPsQ-(-^WJn$eoiPJ+;s6rbMD{& zqU*q}AC(U;i1+WT{pqc(!*?{MP3`g_FS@&DUpnRA zU2R&O!ZFMkn{#mN{ocBbPwvHESbg0uq+niKw=VRK`+e8pPw!ZX-L~uOTXV1cuO;7E zJ8x-e)ykPP@80+&aj5(Jr8D$Tp84&5ebJ6<&yye7{=e+cbbRuuhd$v}FZVL@Ovd;4 zXmn-ny1l>Kw|(`pneli3{QR@=Jx_7(ZJ9fvQ3v?SrLQkqzk^t^U_oiuU5mQUecO8J zAJ?qD_n%LWT+|)janCDX9e(k?za4ojae$)({uiYS*?Ie&>XlDDH0zfG4=p=#TezpI zYtrQIDQ8WccJ{Zh>F1n#-uV|?c+s~no^i>{S+l=$>1A^+|L$2`i_h!ox~BO+ie~s8 zf)ZuM#}GiKS}4Xb4An0a1Z3j|Fc}C8MhU~juo@Z)4s(5iKICa=X2X|aEIg6bOv2EZ zKqaTvPH#h}y$vlY4kn!Lx{m}BGQpD4XVlib)6>sR{yv zk$`Qk8stE(wJJ<=gsO6;KdtjzRx!COaB^9$!OKcL(!?z0I1Gzqp~*>%$$-|1Bnk5@ z+AW_Tpe#BJL!tN}+BvTm#lcu1pG6+#Ik9BY?fC(WguZbDGT~xP^hqb_d@Yfs6b>?N zuS_$jIDOS5O&Nl-p4JOgov;Ag3=u=ej=bk#M_e>zvZ`Xi7za5THp;ZbxxK)kXo48~ zZc=`LGXO%}z-)#*+~_`v7RH=U;5TimFcgBhpTz2nppb<0r8<{Xe z@)4WB%7gZVE0#`VFd-czf?0}7hgy@xekw-73`Y}2v<&rhez27Ei>7XsYC)&Zj7uI| zFRE6OR&%;jZD2`svau*vjkgI@)tuhppo=f22rt4}JRXS+YEB<|d#gd0h6i0R8+Y=J z1nJFcvctrVu`HHP6!=<<&R1iWBNvM@roTxR5f4GAQNBRcSuo60Ca*dosa(V)hJ$q6 z1~4&Pi*dl+ysji^QqXP+O^(wh2@qpZ{2~XzKtBbt0HAXrLW0INb0J+z^FY8M1`!y@ z5(LO-rr*#4or6FS1VeNM0-3l+siqUdIVng)t2sMLJ`h2tlqu7g#3bCZK%vzb(=dE8 z+K(ohcYh2=RH4WTaH5H06AX}OfS3TMCc-havkwhW6By74hZEp_grfs$d)gTSg9d!# z105l-odz9vpovcuLhvL8+(dXY1b(6cgojRohdc4k5QNTw4p&8l2RYb(;_XNf0un={ zAt0at6u{A^bq?r`k1fKX8$d*YLhJ62;R1!hArd4^WJ`iDG)m=kE<@WW992)sN3?Fi zeS(ojQ6NR6{h|>rf*9svIe&7%Ub2#mDT3pf2rX0(zeX$_0>N(|$2-nICprg?xv7&a z9F&e$BnXfY;5ooi8i6qa$%edXUP(%B-Y4@8Ay>3Hbxx;;Q`Lk6@HL_4%GavWpx+TQ znKWR7MB1Cn08k1=thgOx<0U#%H2M@*Ejr}vl`EX5w@}t7GwD;YmUvM-`E@~SjhtNU zhB2DPX5MbczG^P1K=N9I!Ff8&e06@|k!&L;?qwrRp4s#7>5Bgjt+* zJjVG!oyh_N8+V;P-gQC1D~3YXk#5CmdI2{CoazX*bTm{9#aGjd5KU^=VonrH%4jL4 zp9I6QhU&M5gdUaShSnj=1VECF7RAO@4+2dFc~$0oIg>Iq|5#gr=ytTlKorwS!2lF5 zL3`h@(~*x#A$Pl3MvM`~ve-9R z3Du0UFD2FU1v$^=v6NiMI3t?nDu-azB4G7v0w-i*f)$Aj;&^5tL=Lm6RC9ntNYd#c z5=yN@^do_jEXZ_3nw^tU@Ya%uNWp@b*Rvqmk{`iM z`3ynHN4!bQO5h21Di0*RsCdb62&j6{l^0C1c@rWYZssP+=w|V!$6Ami)z&nILj=Z< zkfD=CcXE6~M~Wsu6@xLN6*ZT^ObU_~9jukyN~w{NwE7Sk2^JL5O)y?Q&=?5wwlm1xtkuk2X*_oorl4?+ zd^yQ8Ma$@uRAtC3&{nPp%jSu3nOK}W)_9^uRUGd-+l?w)OSpuSlD^4#>`X}CBAhMOh^ifzI24nCW=sH2h?Itb5~tVHb-h& z5a;S4ccENqXxTiB=Veu^`@+r&3j-c29|%G0o8)V|d26RD_D%A&vp2=izngY*dW!ja zB1^GOnx1ODw$qhr(Ha}8l+EUplu!SQppTf!(y-!p$F_* zrjM4QUNWJW2q>3_hP(kVl+F>dT4vvM2qi>VDHoYsB zmVe!uOg8Vm@B99EpU?Yzv&^%^bAxC2+_`nef9T%$!*>y@cHMpb@)dtPm$>?hZCm8q zcOJ~_{pzzX zKfm+Lnw_VA?W_mJue|W#hh|@Kic{kU{(6k@$^AInU4Gz#le=N#b`+FedEb5 z-mvTJ7YXm`@+FO3V>jRM-l5i(7cbs?=ljpxc;0JIJon4i-7kIc^8PpXo%ZWzAHVd9 zM-T5`mfLpyzhC<2t|i*dw?43O-_n)$U2}*h?k#`N_! zy?5`p=^y&zu{ZPoVlUcu&(kk6+Y@zl%Mah7lJC5G`h(xO@hi(cERJGqZ0)+G|H1V5 zn9u6DbqD6Yf3#!kliRw7`cdt%SJ$rIu%6x&xoykK%is2Izw_Rm-yq*V(YCK`yX?B= zU3Z+%iGl4O(r=HQb=_*??@#UCZ+vOb<>Tr@KR=QEC!Js7S@DUsFt;68v;LcVo~YjT zhG+A-RfnH?{)z+JUq6HW>88hDSmU{WzDytK|9j!*!TsLPHO8-e z_Wb1J*Y|kA%IDwObnWWQig#D;AAjibUAZrKjHaVDk`|yap0xtp2Vg&hwGaH z!OR;lV%KOuyoe=p>2ODoC7UsIq)Ac)fmu~g2T5IlD9^EdU1j0|sB*xUVh9@8w7VU zLFZYBls-h+OcY0!oz^Lq%&JUIfaY8TrY+AKNL09QaN)k5k*D)ipvNMh45Kmx+9#It z4Y|xSVq4Ed%YxL#q3ejiIr0cZku;JQEDN-z33gfqDm0B&xoRbn5)Ywi&bmnhkej4L?_zC_^9VFwFpu5TbE?O*>-alaNUZySLdUNY&Eef|Donfnp6P zMk7UpQ8EEEBkr$Bc5b-11UbppEoe5`Ni)M|bGe928WCYK-Vc`zuG%R?qCUef*Rm0@ zMi>pZvYHdG4*?Xfi`~2ucCvo6i^ux~u2#>bMGT}`H(v=;A|1AMYSuF9_JmF`VZu0y zvU)ZhwDJ@_fw~dSa#~-2q{v$LJ1myDJC@wiBor{YnBK*Bw) zh%^O3X^L?%4%@&+V35BGq{svWybavCSXGWzFcOX<;oGQ<4Z^1QLnS+jLK%xWI^m zknUQ=tbewYOcSJ#XmI^Xe4@~b3XHS>iH0pM#zO=n6i&>Y)@ThPz{E6IT(k(V2L`dD zH2C%1w5ds+4TfJSb3Q zwIo?qjE*n{ATp6z!J+%5sZgBtlbv=u9SJJROx-KRbYdW0jEEKs+F<6R#g497E^(=& zVi(oA8G{OQ-pI7XG@`zwPDcFa27chKR`fqc;8wFENWr36LL12(;~Ziej;HVgOOSvQZz^GZSka-W00^;mQTx zm1FFvhHr-=Ym>bM0zScFdICz>f(mnZ%n_Vp$L3|47;4eU>-2o81kUf2bb4NaBj#VceZ>l0^=|)QqO}7(Wv&9ybUahXyqBWwZ)+^L;+AeYGa z(pLnALm+KJsgjkPbb6X3E2RXQjcQ8Q2l5HU;ARk121Sg>Q;gz3Os}FT=O8Lq*=8Xk z+lJHZ#woF8H00@Qy=LJ?pl0-gq|wkE+!$32tYt(2Ph>MfCpT;}Iyx1ejgv#+b6bv7 zJ=`r|AjRw0*aC&2Bn24Olm^mEjB4+w^dh%olF8&yr(8ZV+#JXXjLTPB;sxA&5m?-C zrE%+I9;r5i8$cx6z?K1TX4+b{LfYQSq^5Iee~O);s7@|ZB=sn=e5oSQ0AAHXzFZ|} z&^)c@eF~_fV6R$Iwdrm%kZSfTir$++>3BUQ2WUPl#`>kC==}uTwH%kb$~<>dd_5)j zYOR9EHykshXsMa7#5vUlY}pI*vWXlz+Ogpd??}To?C_%9ykA<>oX&&Z#XN3MPQE#v z2fd5=IqHozr*2c4>I*7kizw9-ShqQqf`ZCB?nhLsz69<~=OZWFcK1|5pO2h4nZ9~7 z^x>tUtHhwM{uq3vCmW0{AYRWVtcgl8-KL!!6RU&>U!syw;xb!Kb5_0qwP;CeRP{i| z&s$Zb>v2-(B?9$UyRTa{n5(Ip-eZ$gI|^e#rxr^<;#BxLV9y*~iBsY0C|wEsx8S7s zx)7zrC`>;tUx##M2CydPZpQ&%70hATNW*|g)Gq#SRjxzdDQ z32{m$9KhYXYMTRb4n)b?Or$Bay_sgPInmBcCc;v>7m-0Xo8l4$Sqd55mYm78_^bX0 HXFdM~?FVd; literal 0 HcmV?d00001 diff --git a/tinycloud-at-rest-cli/tests/fixtures/tdx_quote_outdated_collateral.json b/tinycloud-at-rest-cli/tests/fixtures/tdx_quote_outdated_collateral.json new file mode 100644 index 00000000..bd64f8ad --- /dev/null +++ b/tinycloud-at-rest-cli/tests/fixtures/tdx_quote_outdated_collateral.json @@ -0,0 +1 @@ +{"pck_crl_issuer_chain":"-----BEGIN CERTIFICATE-----\nMIICljCCAj2gAwIBAgIVAJVvXc29G+HpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC\nMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD\nb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw\nCQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg\nBgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs\nIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex\nCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB/7t21lXSO\n2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z+UiRZCnqR7psOvgqFeSxlmTlJl\neTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS\nBgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy\ndmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d\nzb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w+i6VYGW3UF/22uaXe0YJDj1Ue\nnA+TjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n","root_ca_crl":"308201203081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3235303332303131323135375a170d3236303430333131323135375aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020347003044022030c9fce1438da0a94e4fffdd46c9650e393be6e5a7862d4e4e73527932d04af302206539efe3f734c3d7df20d9dfc4630e1c7ff0439a0f8ece101f15b5eaff9b4f33","pck_crl":"30820d1730820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303231383130343131355a170d3236303332303130343131355a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303231383130343131355a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303231383130343131355a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303231383130343131355a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303231383130343131355a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303231383130343131355a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303231383130343131355a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303231383130343131355a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303231383130343131355a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303231383130343131355a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303231383130343131355a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303231383130343131355a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303231383130343131355a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303231383130343131355a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303231383130343131355a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303231383130343131355a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303231383130343131355a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303231383130343131355a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303231383130343131355a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303231383130343131355a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303231383130343131355a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303231383130343131355a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303231383130343131355a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303231383130343131355a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303231383130343131355a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303231383130343131355a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303231383130343131355a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303231383130343131355a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303231383130343131355a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303231383130343131355a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303231383130343131355a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303231383130343131355a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303231383130343131355a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303231383130343131355a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303231383130343131355a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303231383130343131355a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303231383130343131355a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303231383130343131355a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303231383130343131355a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303231383130343131355a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303231383130343131355a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303231383130343131355a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303231383130343131355a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303231383130343131355a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303231383130343131355a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303231383130343131355a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303231383130343131355a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303231383130343131355a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303231383130343131355a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303231383130343131355a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303231383130343131355a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303231383130343131355a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303231383130343131355a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303231383130343131355a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303231383130343131355a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303231383130343131355a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303231383130343131355a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303231383130343131355a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d0403020348003045022100f00668672d1a3079ecea223cc81ce18abb14c7945b485d4b2cd744bff993c95c02203055c33fdacd3b52c61c4a043945a6e607d8b361db1082cc2aa93dd7e83e377b","tcb_info_issuer_chain":"-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n","tcb_info":"{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-02-18T10:58:51Z\",\"nextUpdate\":\"2026-03-20T10:58:51Z\",\"fmspc\":\"90C06F000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":13,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":13,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}","tcb_info_signature":"bdd7c459dbae4634650fd5f7a6bab2f89f4c081e043ecd76dda8dd00362732b889ad8dcdb93daa18cdb5cda6d5aa9092908f5465863d390ebe23da7dd46a5f2f","qe_identity_issuer_chain":"-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n","qe_identity":"{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-02-18T10:42:15Z\",\"nextUpdate\":\"2026-03-20T10:42:15Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}","qe_identity_signature":"85037e9d4dbf39bc6f7f404e29fdf920e96d9e6e6f4afd288fcbe085c59bd52f09da1c284f13bbd342f2c787b6dba3003db958a75134b136bca068272f2392bf"} diff --git a/tinycloud-at-rest-escrow/Cargo.toml b/tinycloud-at-rest-escrow/Cargo.toml new file mode 100644 index 00000000..abf5d6a6 --- /dev/null +++ b/tinycloud-at-rest-escrow/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tinycloud-at-rest-escrow" +version = "0.1.0" +edition = "2021" +publish = false +description = "Escrow of the TinyCloud at-rest root key: 2-of-3 Shamir shares sealed to pinned age recipients (TC-887)" + +[dependencies] +# Pinned exactly: share and bundle formats depend on these crates' behaviour. +age = { version = "=0.12.1", default-features = false, features = ["armor"] } +vsss-rs = { version = "=5.4.0", default-features = false, features = ["std", "zeroize"] } +hkdf = "0.12" +hmac = "0.12" +sha2 = "0.10" +subtle = "2.6" +zeroize = { version = "1", features = ["derive"] } +rand = "0.8" +serde = { workspace = true } +serde_json = { workspace = true } +hex = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +# A software P-256 identity that opens `p256tag` stanzas, so tests can +# round-trip shares sealed to hardware-key (`age1tag1…`) recipients. +age-core = "0.12" +hpke = { version = "0.12", default-features = false, features = ["alloc", "p256"] } +p256 = "0.13" +base64 = { workspace = true } +bech32 = "0.11" diff --git a/tinycloud-at-rest-escrow/src/bundle.rs b/tinycloud-at-rest-escrow/src/bundle.rs new file mode 100644 index 00000000..a495f08f --- /dev/null +++ b/tinycloud-at-rest-escrow/src/bundle.rs @@ -0,0 +1,454 @@ +//! The escrow bundle: three sealed shares, their commitments and the canary. + +use std::io::Write; + +use serde::{Deserialize, Serialize}; +use vsss_rs::Gf256; +use zeroize::Zeroizing; + +use crate::recipients::{parse, validate_holders, HolderRecipient, ParsedRecipient}; +use crate::share::{SharePayload, PAYLOAD_LEN}; +use crate::{ + canary_fingerprint, lp, sha256, Ark, ArkId, Digest32, Efp, EscrowError, HOLDERS, THRESHOLD, +}; + +const BUNDLE_VERSION: u32 = 1; +const EFP_DOMAIN: &[u8] = b"tinycloud/at-rest/escrow-bundle/v1"; +const ARMOR_BEGIN: &str = "-----BEGIN AGE ENCRYPTED FILE-----"; +/// An armored age file for a 58-byte payload and one stanza is under 1 KiB. +const MAX_CIPHERTEXT: usize = 4096; + +/// One holder's entry in a bundle. Public data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EscrowShare { + /// 1..=3, the Shamir x-coordinate. + pub index: u8, + pub holder: String, + pub recipient: String, + /// See [`SharePayload::commitment`]. + pub commitment: Digest32, + /// ASCII-armored age file sealed to `recipient`. Save it as a file and + /// open it with `age -d -i `. + pub ciphertext: String, +} + +/// Everything needed to recover one ARK generation from two holders. Public +/// data: it is stored next to the node's data and handed to the holders. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EscrowBundle { + pub version: u32, + pub ark_id: ArkId, + pub generation: u32, + pub threshold: u8, + /// See [`canary_fingerprint`]. + pub canary_fp: Digest32, + pub shares: Vec, +} + +/// Proof that every pair of shares rebuilt the in-TEE ARK. Only +/// [`EscrowBundle::test_unlock`] makes one, so a +/// [`VerificationRecord`](crate::VerificationRecord) can't be sealed without +/// a real test-unlock. +#[derive(Debug)] +pub struct TestUnlock { + pub(crate) efp: Efp, + pub(crate) pairs: Vec<[u8; 2]>, +} + +impl TestUnlock { + pub fn efp(&self) -> Efp { + self.efp + } +} + +pub(crate) fn seal_armored( + recipient: &ParsedRecipient, + plaintext: &[u8], +) -> Result { + let age_err = |e: &dyn std::fmt::Display| EscrowError::Age(e.to_string()); + let encryptor = age::Encryptor::with_recipients(std::iter::once(recipient.as_age())) + .map_err(|e| age_err(&e))?; + let mut out = Vec::new(); + let armor = age::armor::ArmoredWriter::wrap_output(&mut out, age::armor::Format::AsciiArmor) + .map_err(|e| age_err(&e))?; + let mut writer = encryptor.wrap_output(armor).map_err(|e| age_err(&e))?; + writer.write_all(plaintext).map_err(|e| age_err(&e))?; + writer + .finish() + .and_then(|armor| armor.finish()) + .map_err(|e| age_err(&e))?; + String::from_utf8(out).map_err(|_| EscrowError::Age("armor is not UTF-8".into())) +} + +/// Combine two shares. The caller has already checked them against the bundle. +fn combine(a: &SharePayload, b: &SharePayload) -> Result { + let shares = Zeroizing::new(vec![a.share.to_vec(), b.share.to_vec()]); + let secret = Zeroizing::new(Gf256::combine_array(&*shares).map_err(|_| EscrowError::Shamir)?); + let bytes: [u8; 32] = secret + .as_slice() + .try_into() + .map_err(|_| EscrowError::Shamir)?; + Ok(Ark::from_bytes(bytes)) +} + +impl EscrowBundle { + /// TEE only. Split `ark` 2-of-3 and seal share i to `holders[i]`. + /// + /// Before returning, every pair of fresh shares is combined and compared + /// with `ark`, so a split that can't be recovered never leaves this call. + pub fn create( + ark: &Ark, + ark_id: ArkId, + generation: u32, + holders: &[HolderRecipient], + ) -> Result { + validate_holders(holders)?; + let split = Zeroizing::new( + Gf256::split_array( + usize::from(THRESHOLD), + usize::from(HOLDERS), + ark.expose(), + rand::rngs::OsRng, + ) + .map_err(|_| EscrowError::Shamir)?, + ); + let payloads = split + .iter() + .map(|s| SharePayload::new(ark_id, generation, s)) + .collect::, _>>()?; + let mut shares = Vec::with_capacity(holders.len()); + for (i, (holder, payload)) in holders.iter().zip(&payloads).enumerate() { + if usize::from(payload.index) != i + 1 { + return Err(EscrowError::Shamir); + } + let recipient = parse(&holder.holder, &holder.recipient)?; + shares.push(EscrowShare { + index: payload.index, + holder: holder.holder.clone(), + recipient: holder.recipient.clone(), + commitment: payload.commitment(), + ciphertext: seal_armored(&recipient, &payload.encode())?, + }); + } + let bundle = Self { + version: BUNDLE_VERSION, + ark_id, + generation, + threshold: THRESHOLD, + canary_fp: canary_fingerprint(ark, &ark_id, generation), + shares, + }; + bundle.validate()?; + bundle.test_unlock(ark, &payloads)?; + Ok(bundle) + } + + /// Structural checks. Every other method calls this first. + pub fn validate(&self) -> Result<(), EscrowError> { + if self.version != BUNDLE_VERSION { + return Err(EscrowError::Bundle("unsupported version")); + } + if self.threshold != THRESHOLD { + return Err(EscrowError::Bundle("threshold must be 2")); + } + if self.shares.len() != usize::from(HOLDERS) { + return Err(EscrowError::Bundle("bundle must hold exactly 3 shares")); + } + for (i, share) in self.shares.iter().enumerate() { + if usize::from(share.index) != i + 1 { + return Err(EscrowError::Bundle( + "shares must be indexed 1, 2, 3 in order", + )); + } + if share.ciphertext.len() > MAX_CIPHERTEXT || !share.ciphertext.starts_with(ARMOR_BEGIN) + { + return Err(EscrowError::Bundle( + "share ciphertext is not an armored age file", + )); + } + } + validate_holders(&self.holders())?; + Ok(()) + } + + /// The holders and recipients, in index order. + pub fn holders(&self) -> Vec { + self.shares + .iter() + .map(|s| HolderRecipient { + holder: s.holder.clone(), + recipient: s.recipient.clone(), + }) + .collect() + } + + pub fn share_for(&self, holder: &str) -> Option<&EscrowShare> { + self.shares.iter().find(|s| s.holder == holder) + } + + /// The escrow fingerprint: + /// `SHA-256(lp(domain) ‖ version ‖ ark_id ‖ generation ‖ threshold ‖ canary_fp ‖ n ‖ + /// for each share: index ‖ lp(holder) ‖ lp(recipient) ‖ commitment ‖ SHA-256(ciphertext))`. + pub fn fingerprint(&self) -> Efp { + let mut pre = Vec::with_capacity(512); + lp(&mut pre, EFP_DOMAIN); + pre.extend_from_slice(&self.version.to_be_bytes()); + pre.extend_from_slice(&self.ark_id.0); + pre.extend_from_slice(&self.generation.to_be_bytes()); + pre.push(self.threshold); + pre.extend_from_slice(&self.canary_fp.0); + pre.push(u8::try_from(self.shares.len()).unwrap_or(u8::MAX)); + for s in &self.shares { + pre.push(s.index); + lp(&mut pre, s.holder.as_bytes()); + lp(&mut pre, s.recipient.as_bytes()); + pre.extend_from_slice(&s.commitment.0); + pre.extend_from_slice(&sha256(s.ciphertext.as_bytes())); + } + Digest32(sha256(&pre)) + } + + /// Check one decrypted share against this bundle without a second share. + pub fn verify_share(&self, payload: &SharePayload) -> Result<(), EscrowError> { + self.validate()?; + if payload.ark_id != self.ark_id || payload.generation != self.generation { + return Err(EscrowError::ShareMismatch); + } + let entry = &self.shares[usize::from(payload.index) - 1]; + if entry.commitment != payload.commitment() { + return Err(EscrowError::Commitment { + index: payload.index, + }); + } + Ok(()) + } + + /// Parse and check a share as decrypted by a holder (`age -d` output). + pub fn open_share(&self, decrypted: &[u8]) -> Result { + if decrypted.len() != PAYLOAD_LEN { + return Err(EscrowError::ShareFormat); + } + let payload = SharePayload::decode(decrypted)?; + self.verify_share(&payload)?; + Ok(payload) + } + + /// TEE only (the result is the ARK). Rebuild the ARK from two distinct, + /// committed shares and check it against the canary. + pub fn recover(&self, a: &SharePayload, b: &SharePayload) -> Result { + self.verify_share(a)?; + self.verify_share(b)?; + if a.index == b.index { + return Err(EscrowError::DuplicateIndex { index: a.index }); + } + let ark = combine(a, b)?; + if canary_fingerprint(&ark, &self.ark_id, self.generation) != self.canary_fp { + return Err(EscrowError::Canary); + } + Ok(ark) + } + + /// TEE only. The ceremony check: given all three shares, every pair must + /// rebuild exactly `ark` (constant-time compare) and its canary. + pub fn test_unlock( + &self, + ark: &Ark, + shares: &[SharePayload], + ) -> Result { + self.validate()?; + if canary_fingerprint(ark, &self.ark_id, self.generation) != self.canary_fp { + return Err(EscrowError::Canary); + } + let mut by_index: [Option<&SharePayload>; HOLDERS as usize] = [None; HOLDERS as usize]; + for share in shares { + self.verify_share(share)?; + let slot = &mut by_index[usize::from(share.index) - 1]; + if slot.is_some() { + return Err(EscrowError::DuplicateIndex { index: share.index }); + } + *slot = Some(share); + } + let [Some(s1), Some(s2), Some(s3)] = by_index else { + return Err(EscrowError::Holders( + "test-unlock needs the shares of all three holders".into(), + )); + }; + let mut pairs = Vec::with_capacity(3); + for (a, b) in [(s1, s2), (s1, s3), (s2, s3)] { + if !self.recover(a, b)?.ct_eq(ark) { + return Err(EscrowError::Canary); + } + pairs.push([a.index, b.index]); + } + Ok(TestUnlock { + efp: self.fingerprint(), + pairs, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_support::*; + + type Mutation = Box; + + #[test] + fn create_then_two_holders_recover_and_one_cannot() { + let (ark, ark_id, holders, ids) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let shares = open_all(&bundle, &ids); + + for (a, b) in [(0, 1), (0, 2), (1, 2), (2, 0)] { + assert!(bundle.recover(&shares[a], &shares[b]).unwrap().ct_eq(&ark)); + } + // One share, twice, is still one share. + assert!(matches!( + bundle.recover(&shares[0], &shares[0]), + Err(EscrowError::DuplicateIndex { index: 1 }) + )); + } + + #[test] + fn a_single_share_is_independent_of_the_ark() { + // Same share-1 polynomial evaluation could come from any ARK: with + // threshold 2 the y-bytes of one share are uniform. Check that two + // splits of the same ARK give unrelated share bytes. + let (ark, ark_id, holders, ids) = escrow_fixture(); + let a = open_all( + &EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(), + &ids, + ); + let b = open_all( + &EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(), + &ids, + ); + assert_ne!(a[0].share.as_slice(), b[0].share.as_slice()); + assert_ne!(&a[0].share[1..], ark.expose().as_slice()); + } + + #[test] + fn tampered_share_or_wrong_ark_fails_before_combining() { + let (ark, ark_id, holders, ids) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let shares = open_all(&bundle, &ids); + + let mut flipped = shares[1].clone(); + flipped.share[5] ^= 0x40; + assert!(matches!( + bundle.recover(&shares[0], &flipped), + Err(EscrowError::Commitment { index: 2 }) + )); + + let other = EscrowBundle::create(&Ark::generate(), ArkId::generate(), 1, &holders).unwrap(); + let foreign = open_all(&other, &ids); + assert!(matches!( + bundle.recover(&shares[0], &foreign[1]), + Err(EscrowError::ShareMismatch) + )); + } + + #[test] + fn forged_commitment_still_fails_the_canary() { + // An attacker who controls the stored bundle can rewrite a commitment + // to match a junk share. The canary still rejects the combination. + let (ark, ark_id, holders, ids) = escrow_fixture(); + let mut bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let shares = open_all(&bundle, &ids); + let mut junk = shares[1].clone(); + junk.share[7] ^= 1; + bundle.shares[1].commitment = junk.commitment(); + assert!(matches!( + bundle.recover(&shares[0], &junk), + Err(EscrowError::Canary) + )); + } + + #[test] + fn test_unlock_needs_all_three_and_the_same_ark() { + let (ark, ark_id, holders, ids) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let shares = open_all(&bundle, &ids); + + let ok = bundle.test_unlock(&ark, &shares).unwrap(); + assert_eq!(ok.efp(), bundle.fingerprint()); + assert_eq!(ok.pairs, vec![[1, 2], [1, 3], [2, 3]]); + + assert!(bundle.test_unlock(&ark, &shares[..2]).is_err()); + let dup = [shares[0].clone(), shares[0].clone(), shares[2].clone()]; + assert!(matches!( + bundle.test_unlock(&ark, &dup), + Err(EscrowError::DuplicateIndex { index: 1 }) + )); + assert!(matches!( + bundle.test_unlock(&Ark::generate(), &shares), + Err(EscrowError::Canary) + )); + } + + #[test] + fn fingerprint_covers_recipients_commitments_ciphertexts_and_canary() { + let (ark, ark_id, holders, _) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let efp = bundle.fingerprint(); + let mutations: Vec = vec![ + Box::new(|b| b.shares[0].recipient = x25519_identity().to_public().to_string()), + Box::new(|b| b.shares[1].holder = "mallory".into()), + Box::new(|b| b.shares[2].commitment.0[0] ^= 1), + Box::new(|b| b.shares[0].ciphertext.push('\n')), + Box::new(|b| b.canary_fp.0[31] ^= 1), + Box::new(|b| b.generation = 2), + Box::new(|b| b.ark_id.0[0] ^= 1), + ]; + for mutate in mutations { + let mut changed = bundle.clone(); + mutate(&mut changed); + assert_ne!(changed.fingerprint(), efp); + } + // Stable across a JSON round trip (what holders and the node exchange). + let json = serde_json::to_string(&bundle).unwrap(); + let back: EscrowBundle = serde_json::from_str(&json).unwrap(); + assert_eq!(back.fingerprint(), efp); + } + + #[test] + fn validate_rejects_malformed_bundles() { + let (ark, ark_id, holders, _) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let mutations: Vec = vec![ + Box::new(|b| b.version = 2), + Box::new(|b| b.threshold = 1), + Box::new(|b| { + b.shares.pop(); + }), + Box::new(|b| b.shares.swap(0, 1)), + Box::new(|b| b.shares[0].ciphertext = "not age".into()), + Box::new(|b| b.shares[2].holder = b.shares[0].holder.clone()), + ]; + for mutate in mutations { + let mut bad = bundle.clone(); + mutate(&mut bad); + assert!(bad.validate().is_err()); + } + let unknown = serde_json::to_string(&bundle) + .unwrap() + .replacen('{', "{\"extra\":1,", 1); + assert!(serde_json::from_str::(&unknown).is_err()); + } + + #[test] + fn hardware_tag_share_opens_with_the_p256_key_only() { + let (ark, ark_id, holders, ids) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + // Holder 1 is a p256tag recipient; holder 2's identity can't open it. + assert!(bundle.shares[0].recipient.starts_with("age1tag1")); + assert!(ids[1].open(&bundle.shares[0].ciphertext).is_err()); + let share = bundle + .open_share(&ids[0].open(&bundle.shares[0].ciphertext).unwrap()) + .unwrap(); + assert_eq!(share.index, 1); + } +} diff --git a/tinycloud-at-rest-escrow/src/lib.rs b/tinycloud-at-rest-escrow/src/lib.rs new file mode 100644 index 00000000..977f4629 --- /dev/null +++ b/tinycloud-at-rest-escrow/src/lib.rs @@ -0,0 +1,252 @@ +//! Escrow of the TinyCloud at-rest root key (ARK), TC-887. +//! +//! The node makes one random 32-byte ARK inside the TEE. This crate splits it +//! 2-of-3 with Shamir over GF(256), seals each share with age to a holder +//! recipient pinned in the node's config, and describes the result as an +//! [`EscrowBundle`]. The bundle's fingerprint ([`Efp`]) is what operators pin +//! once a ceremony has proven every share. Two shares rebuild the ARK; one +//! share reveals nothing about it. +//! +//! Three operations need the ARK and therefore run only inside the TEE: +//! [`EscrowBundle::create`], [`EscrowBundle::test_unlock`] and +//! [`EscrowBundle::recover`]. Everything a holder's machine does (inspecting +//! a bundle, checking a decrypted share against its commitment, re-sealing it +//! to a TEE's per-boot recovery recipient) uses public data only. +//! +//! All derivations use length-prefixed fields: `lp(x) = u32 BE len ‖ x`. + +mod bundle; +mod recipients; +mod record; +mod recovery; +mod share; + +use std::fmt; +use std::str::FromStr; + +use hkdf::Hkdf; +use sha2::{Digest, Sha256}; +use subtle::ConstantTimeEq; +use zeroize::Zeroizing; + +pub use bundle::{EscrowBundle, EscrowShare, TestUnlock}; +pub use recipients::{parse_recipient, validate_holders, HolderRecipient, RecipientKind}; +pub use record::VerificationRecord; +pub use recovery::{ + binding_digest, recovery_binding, validate_nonce, wrap_share, RecoveryIdentity, RecoveryInfo, + MAX_WRAPPED_SHARE, +}; +pub use share::SharePayload; + +/// Shares needed to rebuild the ARK. +pub const THRESHOLD: u8 = 2; +/// Shares made, one per holder. +pub const HOLDERS: u8 = 3; + +/// Errors from escrow operations. None of them carry key or share bytes. +#[derive(Debug, thiserror::Error)] +pub enum EscrowError { + #[error("escrow holders: {0}")] + Holders(String), + #[error("recipient for holder {holder:?}: {reason}")] + Recipient { holder: String, reason: String }, + #[error("escrow bundle: {0}")] + Bundle(&'static str), + #[error("share is malformed")] + ShareFormat, + #[error("share belongs to a different ARK or generation")] + ShareMismatch, + #[error("share {index} does not match its commitment in the bundle")] + Commitment { index: u8 }, + #[error("two shares with index {index}")] + DuplicateIndex { index: u8 }, + #[error("combined shares do not reproduce the escrowed key (canary mismatch)")] + Canary, + #[error("Shamir split/combine failed")] + Shamir, + #[error("age: {0}")] + Age(String), + #[error("verification record: {0}")] + Record(&'static str), + #[error("recovery binding: {0}")] + Binding(&'static str), +} + +/// The at-rest root key. Zeroized on drop; `Debug` never prints it. +#[derive(Clone)] +pub struct Ark(Zeroizing<[u8; 32]>); + +impl Ark { + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(Zeroizing::new(bytes)) + } + + /// A fresh key from the OS CSPRNG. + pub fn generate() -> Self { + let mut bytes = Zeroizing::new([0u8; 32]); + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, bytes.as_mut()); + Self(bytes) + } + + pub fn expose(&self) -> &[u8; 32] { + &self.0 + } + + /// Constant-time equality. + pub fn ct_eq(&self, other: &Ark) -> bool { + self.0.ct_eq(&*other.0).into() + } + + /// HKDF-SHA256 with `salt = ark_id`. Every escrow-side key comes from here. + pub(crate) fn derive(&self, ark_id: &ArkId, info: &[u8]) -> Zeroizing<[u8; 32]> { + let hk = Hkdf::::new(Some(&ark_id.0), self.expose()); + let mut out = Zeroizing::new([0u8; 32]); + hk.expand(info, out.as_mut()) + .expect("32 bytes is a valid HKDF-SHA256 length"); + out + } +} + +impl fmt::Debug for Ark { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Ark()") + } +} + +/// Random 16-byte id of one ARK generation. Hex on the wire. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct ArkId(pub [u8; 16]); + +impl ArkId { + pub fn generate() -> Self { + let mut id = [0u8; 16]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut id); + Self(id) + } +} + +impl fmt::Display for ArkId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&hex::encode(self.0)) + } +} + +impl fmt::Debug for ArkId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ArkId({self})") + } +} + +impl FromStr for ArkId { + type Err = EscrowError; + fn from_str(s: &str) -> Result { + Ok(Self(hex32::<16>(s).ok_or(EscrowError::Bundle( + "ark_id must be 32 hex chars", + ))?)) + } +} + +impl serde::Serialize for ArkId { + fn serialize(&self, s: S) -> Result { + s.collect_str(self) + } +} + +impl<'de> serde::Deserialize<'de> for ArkId { + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + s.parse().map_err(serde::de::Error::custom) + } +} + +/// A public 32-byte digest (fingerprint, commitment, canary). Lowercase hex on +/// the wire; equality is constant-time. +#[derive(Clone, Copy, Eq)] +pub struct Digest32(pub [u8; 32]); + +impl PartialEq for Digest32 { + fn eq(&self, other: &Self) -> bool { + self.0.ct_eq(&other.0).into() + } +} + +impl fmt::Display for Digest32 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&hex::encode(self.0)) + } +} + +impl fmt::Debug for Digest32 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self}") + } +} + +impl FromStr for Digest32 { + type Err = EscrowError; + fn from_str(s: &str) -> Result { + Ok(Self(hex32::<32>(s).ok_or(EscrowError::Bundle( + "digest must be 64 hex chars", + ))?)) + } +} + +impl serde::Serialize for Digest32 { + fn serialize(&self, s: S) -> Result { + s.collect_str(self) + } +} + +impl<'de> serde::Deserialize<'de> for Digest32 { + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + s.parse().map_err(serde::de::Error::custom) + } +} + +/// Escrow fingerprint: SHA-256 over the canonical encoding of a bundle, +/// including its recipients, commitments, ciphertext digests and canary. +/// This is the value pinned as `at_rest.escrow.verified_fp`. +pub type Efp = Digest32; + +/// Lowercase-only hex of exactly `N` bytes. +fn hex32(s: &str) -> Option<[u8; N]> { + if s.len() != N * 2 || s.bytes().any(|b| b.is_ascii_uppercase()) { + return None; + } + let mut out = [0u8; N]; + hex::decode_to_slice(s, &mut out).ok()?; + Some(out) +} + +pub(crate) fn lp(out: &mut Vec, bytes: &[u8]) { + let len = u32::try_from(bytes.len()).expect("field shorter than 4 GiB"); + out.extend_from_slice(&len.to_be_bytes()); + out.extend_from_slice(bytes); +} + +pub(crate) fn sha256(bytes: &[u8]) -> [u8; 32] { + Sha256::digest(bytes).into() +} + +const CANARY_INFO: &[u8] = b"tinycloud/at-rest/escrow-canary/v1"; +const CANARY_FP_DOMAIN: &[u8] = b"tinycloud/at-rest/escrow-canary-fp/v1"; + +/// Public fingerprint of the ARK's canary key. A holder sees it in the bundle +/// before a test-unlock; the TEE reports it after rebuilding the ARK from +/// shares, so the two can be compared without the ARK leaving the TEE. +pub fn canary_fingerprint(ark: &Ark, ark_id: &ArkId, generation: u32) -> Digest32 { + let mut info = Vec::new(); + lp(&mut info, CANARY_INFO); + info.extend_from_slice(&generation.to_be_bytes()); + let canary = ark.derive(ark_id, &info); + let mut pre = Vec::new(); + lp(&mut pre, CANARY_FP_DOMAIN); + pre.extend_from_slice(ark_id.0.as_slice()); + pre.extend_from_slice(&generation.to_be_bytes()); + lp(&mut pre, canary.as_slice()); + Digest32(sha256(&pre)) +} + +#[cfg(test)] +pub(crate) mod test_support; diff --git a/tinycloud-at-rest-escrow/src/recipients.rs b/tinycloud-at-rest-escrow/src/recipients.rs new file mode 100644 index 00000000..42b2ed23 --- /dev/null +++ b/tinycloud-at-rest-escrow/src/recipients.rs @@ -0,0 +1,180 @@ +//! Holder recipients, as pinned in the node's `at_rest.escrow.recipients`. + +use std::collections::HashSet; + +use serde::{Deserialize, Serialize}; + +use crate::{EscrowError, HOLDERS}; + +/// One escrow holder: a label and the age recipient their share is sealed to. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HolderRecipient { + /// 1-32 chars of `a-z 0-9 - _`, unique within the escrow. + pub holder: String, + /// `age1tag1…` (P-256 hardware key, e.g. a YubiKey via age-plugin-yubikey) + /// or `age1…` (X25519, software; tests and self-host only). + pub recipient: String, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RecipientKind { + /// `age1tag1…` (`p256tag` stanza). Meant for keys held in hardware; age + /// encrypts to it natively, decryption needs the holder's plugin. + HardwareTag, + /// `age1…` X25519. The identity is a file, not hardware. + SoftwareX25519, +} + +pub(crate) enum ParsedRecipient { + Tag(age::tag::Recipient), + X25519(age::x25519::Recipient), +} + +impl ParsedRecipient { + pub(crate) fn as_age(&self) -> &dyn age::Recipient { + match self { + Self::Tag(r) => r, + Self::X25519(r) => r, + } + } +} + +pub(crate) fn parse(holder: &str, recipient: &str) -> Result { + let err = |reason: &str| EscrowError::Recipient { + holder: holder.to_owned(), + reason: reason.to_owned(), + }; + if recipient.starts_with("age1tag1") { + recipient + .parse::() + .map(ParsedRecipient::Tag) + .map_err(err) + } else if recipient.starts_with("age1yubikey1") { + Err(err( + "age1yubikey1 recipients need the plugin to encrypt; use the age1tag1 form of the same key", + )) + } else if recipient.starts_with("age1") { + recipient + .parse::() + .map(ParsedRecipient::X25519) + .map_err(err) + } else { + Err(err("not an age1tag1 or age1 X25519 recipient")) + } +} + +/// Parse one recipient and report its kind. +pub fn parse_recipient(holder: &str, recipient: &str) -> Result { + Ok(match parse(holder, recipient)? { + ParsedRecipient::Tag(_) => RecipientKind::HardwareTag, + ParsedRecipient::X25519(_) => RecipientKind::SoftwareX25519, + }) +} + +fn valid_holder_name(name: &str) -> bool { + (1..=32).contains(&name.len()) + && name + .bytes() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_') +} + +/// Exactly three holders, valid unique names, unique recipients that parse. +pub fn validate_holders(holders: &[HolderRecipient]) -> Result, EscrowError> { + if holders.len() != usize::from(HOLDERS) { + return Err(EscrowError::Holders(format!( + "need exactly {HOLDERS} holders, got {}", + holders.len() + ))); + } + let mut names = HashSet::new(); + let mut recipients = HashSet::new(); + let mut kinds = Vec::with_capacity(holders.len()); + for h in holders { + if !valid_holder_name(&h.holder) { + return Err(EscrowError::Holders(format!( + "holder name {:?} must be 1-32 chars of a-z 0-9 - _", + h.holder + ))); + } + if !names.insert(h.holder.as_str()) { + return Err(EscrowError::Holders(format!( + "holder {:?} listed twice", + h.holder + ))); + } + if !recipients.insert(h.recipient.as_str()) { + return Err(EscrowError::Holders(format!( + "holder {:?} reuses another holder's recipient", + h.holder + ))); + } + kinds.push(parse_recipient(&h.holder, &h.recipient)?); + } + Ok(kinds) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_support::{tag_identity, x25519_identity}; + + fn holder(name: &str, recipient: String) -> HolderRecipient { + HolderRecipient { + holder: name.into(), + recipient, + } + } + + #[test] + fn accepts_tag_and_x25519_and_reports_kind() { + let holders = vec![ + holder("sam", tag_identity().1), + holder("hunter", tag_identity().1), + holder("safe", x25519_identity().to_public().to_string()), + ]; + assert_eq!( + validate_holders(&holders).unwrap(), + vec![ + RecipientKind::HardwareTag, + RecipientKind::HardwareTag, + RecipientKind::SoftwareX25519 + ] + ); + } + + #[test] + fn rejects_count_names_duplicates_and_bad_recipients() { + let r = || x25519_identity().to_public().to_string(); + assert!(validate_holders(&[holder("a", r()), holder("b", r())]).is_err()); + assert!(validate_holders(&[ + holder("a", r()), + holder("b", r()), + holder("c", r()), + holder("d", r()) + ]) + .is_err()); + assert!(validate_holders(&[holder("a", r()), holder("a", r()), holder("c", r())]).is_err()); + assert!( + validate_holders(&[holder("Sam", r()), holder("b", r()), holder("c", r())]).is_err() + ); + assert!(validate_holders(&[holder("", r()), holder("b", r()), holder("c", r())]).is_err()); + let same = r(); + assert!(validate_holders(&[ + holder("a", same.clone()), + holder("b", same), + holder("c", r()) + ]) + .is_err()); + for bad in [ + "age1yubikey1qwerty", + "ssh-ed25519 AAAA", + "age1notavalidrecipient", + "age1tag1notvalid", + ] { + let err = + validate_holders(&[holder("a", bad.into()), holder("b", r()), holder("c", r())]); + assert!(err.is_err(), "{bad}"); + } + } +} diff --git a/tinycloud-at-rest-escrow/src/record.rs b/tinycloud-at-rest-escrow/src/record.rs new file mode 100644 index 00000000..65a9f93a --- /dev/null +++ b/tinycloud-at-rest-escrow/src/record.rs @@ -0,0 +1,193 @@ +//! The verification record a TEE writes after a successful test-unlock. + +use hmac::{Hmac, Mac}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; + +use crate::{lp, Ark, ArkId, Digest32, Efp, EscrowBundle, EscrowError, TestUnlock}; + +const RECORD_VERSION: u32 = 1; +const MAC_INFO: &[u8] = b"tinycloud/at-rest/escrow-record/v1"; +const ALL_INDICES: [u8; 3] = [1, 2, 3]; +const ALL_PAIRS: [[u8; 2]; 3] = [[1, 2], [1, 3], [2, 3]]; + +/// Proof that all three escrowed shares were decrypted by their holders and +/// that every pair rebuilt the ARK inside the TEE. +/// +/// `mac` is HMAC-SHA256 under a key derived from the ARK, so storage that can +/// rewrite rows can't mint or alter a record. Only a holder of the ARK (the +/// TEE) can check it. Operators pin `efp` after reading a record. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VerificationRecord { + pub version: u32, + pub efp: Efp, + pub ark_id: ArkId, + pub generation: u32, + pub verified_indices: Vec, + pub pairs: Vec<[u8; 2]>, + pub canary_fp: Digest32, + /// The TEE identity that ran the test-unlock, if any (informational). + pub app_id: Option, + pub compose_hash: Option, + /// RFC 3339, the TEE's clock (informational; not a freshness proof). + pub verified_at: String, + pub mac: Digest32, +} + +impl VerificationRecord { + /// TEE only. Seal a record for a test-unlock of `bundle`. + pub fn seal( + ark: &Ark, + bundle: &EscrowBundle, + unlock: &TestUnlock, + app_id: Option, + compose_hash: Option, + verified_at: String, + ) -> Result { + if unlock.efp != bundle.fingerprint() { + return Err(EscrowError::Record( + "test-unlock was for a different bundle", + )); + } + let mut record = Self { + version: RECORD_VERSION, + efp: unlock.efp, + ark_id: bundle.ark_id, + generation: bundle.generation, + verified_indices: ALL_INDICES.to_vec(), + pairs: unlock.pairs.clone(), + canary_fp: bundle.canary_fp, + app_id, + compose_hash, + verified_at, + mac: Digest32([0; 32]), + }; + record.mac = record.compute_mac(ark); + Ok(record) + } + + fn compute_mac(&self, ark: &Ark) -> Digest32 { + let mut info = Vec::new(); + lp(&mut info, MAC_INFO); + let key = ark.derive(&self.ark_id, &info); + let mut mac = Hmac::::new_from_slice(key.as_slice()).expect("any key length"); + let mut body = Vec::with_capacity(256); + body.extend_from_slice(&self.version.to_be_bytes()); + body.extend_from_slice(&self.efp.0); + body.extend_from_slice(&self.ark_id.0); + body.extend_from_slice(&self.generation.to_be_bytes()); + lp(&mut body, &self.verified_indices); + lp(&mut body, &self.pairs.concat()); + body.extend_from_slice(&self.canary_fp.0); + for opt in [&self.app_id, &self.compose_hash] { + match opt { + None => body.push(0), + Some(v) => { + body.push(1); + lp(&mut body, v.as_bytes()); + } + } + } + lp(&mut body, self.verified_at.as_bytes()); + mac.update(&body); + Digest32(mac.finalize().into_bytes().into()) + } + + /// TEE only. The record is authentic (MAC under `ark`), belongs to + /// `bundle`, and covers all three shares and all three pairs. + pub fn verify(&self, ark: &Ark, bundle: &EscrowBundle) -> Result<(), EscrowError> { + if self.version != RECORD_VERSION { + return Err(EscrowError::Record("unsupported version")); + } + if self.compute_mac(ark) != self.mac { + return Err(EscrowError::Record("MAC does not verify under this ARK")); + } + self.covers(bundle) + } + + /// Public checks (no ARK): the record names `bundle` and claims full + /// coverage. Holders and operators run this before pinning `efp`. + pub fn covers(&self, bundle: &EscrowBundle) -> Result<(), EscrowError> { + if self.efp != bundle.fingerprint() + || self.ark_id != bundle.ark_id + || self.generation != bundle.generation + || self.canary_fp != bundle.canary_fp + { + return Err(EscrowError::Record( + "record is for a different escrow bundle", + )); + } + if self.verified_indices != ALL_INDICES || self.pairs != ALL_PAIRS { + return Err(EscrowError::Record( + "record does not cover all three shares and pairs", + )); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_support::*; + + type Edit = Box; + + fn sealed() -> (Ark, EscrowBundle, VerificationRecord) { + let (ark, ark_id, holders, ids) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let unlock = bundle.test_unlock(&ark, &open_all(&bundle, &ids)).unwrap(); + let record = VerificationRecord::seal( + &ark, + &bundle, + &unlock, + Some("app".into()), + None, + "2026-10-17T00:00:00Z".into(), + ) + .unwrap(); + (ark, bundle, record) + } + + #[test] + fn sealed_record_verifies_and_survives_json() { + let (ark, bundle, record) = sealed(); + record.verify(&ark, &bundle).unwrap(); + let back: VerificationRecord = + serde_json::from_str(&serde_json::to_string(&record).unwrap()).unwrap(); + back.verify(&ark, &bundle).unwrap(); + } + + #[test] + fn any_edit_or_other_ark_breaks_the_mac() { + let (ark, bundle, record) = sealed(); + assert!(record.verify(&Ark::generate(), &bundle).is_err()); + let edits: Vec = vec![ + Box::new(|r| r.app_id = Some("other".into())), + Box::new(|r| r.app_id = None), + Box::new(|r| r.compose_hash = Some(String::new())), + Box::new(|r| r.verified_at.push('x')), + Box::new(|r| r.mac.0[0] ^= 1), + ]; + for edit in edits { + let mut forged = record.clone(); + edit(&mut forged); + assert!(forged.verify(&ark, &bundle).is_err()); + } + } + + #[test] + fn record_does_not_cover_a_different_or_partial_escrow() { + let (ark, bundle, record) = sealed(); + // Same ARK, re-split to the same holders: new ciphertexts, new EFP. + let resplit = EscrowBundle::create(&ark, bundle.ark_id, 1, &bundle.holders()).unwrap(); + assert!(record.verify(&ark, &resplit).is_err()); + + let mut partial = record.clone(); + partial.verified_indices = vec![1, 2]; + partial.pairs = vec![[1, 2]]; + partial.mac = partial.compute_mac(&ark); + assert!(partial.verify(&ark, &bundle).is_err()); + } +} diff --git a/tinycloud-at-rest-escrow/src/recovery.rs b/tinycloud-at-rest-escrow/src/recovery.rs new file mode 100644 index 00000000..0c0f5c4f --- /dev/null +++ b/tinycloud-at-rest-escrow/src/recovery.rs @@ -0,0 +1,196 @@ +//! Moving a share from a holder into a TEE: the per-boot recovery identity, +//! the attested binding, and the wrap/unwrap of a share. + +use std::io::Read; + +use serde::{Deserialize, Serialize}; +use zeroize::Zeroizing; + +use crate::bundle::seal_armored; +use crate::recipients::ParsedRecipient; +use crate::share::PAYLOAD_LEN; +use crate::{sha256, ArkId, Digest32, Efp, EscrowError, SharePayload}; + +/// Largest wrapped share the TEE reads. A real one is about 400 bytes. +pub const MAX_WRAPPED_SHARE: usize = 4096; +const BINDING_PREFIX: &str = "xyz.tinycloud.at-rest/recovery-key/v1\\0"; + +/// What `GET /at-rest/recovery?nonce=` returns. Every field is public. Only +/// what the holder recomputes and the verified quote prove anything; the +/// rest is a convenience for humans. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RecoveryInfo { + /// `"dstack"` when `quote` is a TDX quote, `"classic"` outside a TEE. + pub mode: String, + /// `"locked"` or `"unlocked"`. + pub state: String, + pub ark_id: ArkId, + pub generation: u32, + pub efp: Efp, + pub canary_fp: Digest32, + /// Per-boot X25519 age recipient. Its identity never leaves TEE memory. + pub recovery_recipient: String, + /// See [`recovery_binding`]. + pub binding: String, + pub binding_sha256: Digest32, + /// Hex TDX quote whose REPORTDATA is `binding_sha256 ‖ 32 zero bytes`. + pub quote: Option, + /// dstack event log (JSON text) for replaying RTMR3. + pub event_log: Option, + /// Indices of valid shares the TEE holds for this boot. + pub shares_held: Vec, +} + +/// `xyz.tinycloud.at-rest/recovery-key/v1\0:::`. The +/// `\0` is two characters (backslash, zero), as in `/attestation`. +pub fn recovery_binding(recipient: &str, efp: &Efp, nonce: &str) -> String { + format!("{BINDING_PREFIX}:{recipient}:{efp}:{nonce}") +} + +/// SHA-256 of the binding's UTF-8 bytes; the TDX REPORTDATA's first half. +pub fn binding_digest(binding: &str) -> Digest32 { + Digest32(sha256(binding.as_bytes())) +} + +/// 1-64 chars of `A-Z a-z 0-9 - _` (same rule as `/attestation`). +pub fn validate_nonce(nonce: &str) -> Result<(), EscrowError> { + if (1..=64).contains(&nonce.len()) + && nonce + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') + { + Ok(()) + } else { + Err(EscrowError::Binding( + "nonce must be 1-64 chars of A-Z a-z 0-9 - _", + )) + } +} + +/// The TEE's per-boot recovery key. Made fresh at every boot, held only in +/// memory, never serialized. +pub struct RecoveryIdentity(age::x25519::Identity); + +impl Default for RecoveryIdentity { + fn default() -> Self { + Self::generate() + } +} + +impl RecoveryIdentity { + pub fn generate() -> Self { + Self(age::x25519::Identity::generate()) + } + + pub fn recipient(&self) -> String { + self.0.to_public().to_string() + } + + /// Decrypt a wrapped share (armored or binary age) and decode it. The + /// caller must still check it against the bundle. + pub fn open(&self, wrapped: &[u8]) -> Result { + if wrapped.len() > MAX_WRAPPED_SHARE { + return Err(EscrowError::ShareFormat); + } + let age_err = |_| EscrowError::Age("wrapped share does not decrypt".into()); + let decryptor = age::Decryptor::new_buffered(age::armor::ArmoredReader::new(wrapped)) + .map_err(age_err)?; + let mut reader = decryptor + .decrypt(std::iter::once(&self.0 as &dyn age::Identity)) + .map_err(age_err)?; + let mut plain = Zeroizing::new(Vec::with_capacity(PAYLOAD_LEN)); + // One byte past a payload is enough to reject anything longer. + (&mut reader) + .take(PAYLOAD_LEN as u64 + 1) + .read_to_end(&mut plain) + .map_err(|_| EscrowError::Age("wrapped share does not decrypt".into()))?; + SharePayload::decode(&plain) + } +} + +impl std::fmt::Debug for RecoveryIdentity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "RecoveryIdentity({})", self.recipient()) + } +} + +/// Holder side. Seal a decrypted share to a TEE recovery recipient. Only +/// X25519 recipients are accepted: the TEE's identity is software. +/// +/// Call this only after the TEE's quote and binding have been verified. +pub fn wrap_share(recovery_recipient: &str, share: &SharePayload) -> Result { + let recipient = recovery_recipient + .parse::() + .map_err(|_| EscrowError::Binding("recovery recipient is not an age1 X25519 recipient"))?; + seal_armored(&ParsedRecipient::X25519(recipient), &share.encode()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_support::*; + use crate::EscrowBundle; + + #[test] + fn wrapped_share_round_trips_through_the_recovery_identity_only() { + let (ark, ark_id, holders, ids) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let shares = open_all(&bundle, &ids); + let tee = RecoveryIdentity::generate(); + let wrapped = wrap_share(&tee.recipient(), &shares[2]).unwrap(); + + let opened = tee.open(wrapped.as_bytes()).unwrap(); + bundle.verify_share(&opened).unwrap(); + assert_eq!(opened.index, 3); + assert!(RecoveryIdentity::generate() + .open(wrapped.as_bytes()) + .is_err()); + } + + #[test] + fn open_rejects_oversize_and_non_share_plaintext() { + let tee = RecoveryIdentity::generate(); + assert!(tee.open(&vec![b'a'; MAX_WRAPPED_SHARE + 1]).is_err()); + assert!(tee.open(b"garbage").is_err()); + // A valid age file to the right key that does not hold a share. + let recipient: age::x25519::Recipient = tee.recipient().parse().unwrap(); + let junk = seal_armored( + &ParsedRecipient::X25519(recipient.clone()), + &[7u8; PAYLOAD_LEN], + ) + .unwrap(); + assert!(matches!( + tee.open(junk.as_bytes()), + Err(EscrowError::ShareFormat) + )); + let long = seal_armored(&ParsedRecipient::X25519(recipient), &[7u8; 10_000]).unwrap(); + assert!(tee.open(long.as_bytes()).is_err()); + } + + #[test] + fn wrap_refuses_tag_recipients() { + let (ark, ark_id, holders, ids) = escrow_fixture(); + let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let share = &open_all(&bundle, &ids)[0]; + assert!(wrap_share(&tag_identity().1, share).is_err()); + } + + #[test] + fn binding_and_nonce_rules() { + let efp = Digest32([0xab; 32]); + let b = recovery_binding("age1xyz", &efp, "n0nce"); + assert_eq!( + b, + format!("xyz.tinycloud.at-rest/recovery-key/v1\\0:age1xyz:{efp}:n0nce") + ); + assert_ne!( + binding_digest(&b), + binding_digest(&recovery_binding("age1xyz", &efp, "other")) + ); + validate_nonce("A-z_09").unwrap(); + for bad in ["", "a b", "a:b", &"x".repeat(65)] { + assert!(validate_nonce(bad).is_err(), "{bad:?}"); + } + } +} diff --git a/tinycloud-at-rest-escrow/src/share.rs b/tinycloud-at-rest-escrow/src/share.rs new file mode 100644 index 00000000..80501cb2 --- /dev/null +++ b/tinycloud-at-rest-escrow/src/share.rs @@ -0,0 +1,152 @@ +//! One holder's Shamir share and the binary form sealed inside its age file. + +use zeroize::Zeroizing; + +use crate::{lp, sha256, ArkId, Digest32, EscrowError, HOLDERS}; + +const MAGIC: &[u8; 4] = b"TCS1"; +/// x-coordinate byte followed by one y byte per ARK byte. +pub(crate) const SHARE_LEN: usize = 33; +/// `"TCS1" ‖ ark_id(16) ‖ generation u32 BE ‖ index u8 ‖ share(33)`. +pub(crate) const PAYLOAD_LEN: usize = 4 + 16 + 4 + 1 + SHARE_LEN; +const COMMIT_DOMAIN: &[u8] = b"tinycloud/at-rest/escrow-share-commit/v1"; + +/// A decrypted share. The share bytes are zeroized on drop and never printed. +#[derive(Clone)] +pub struct SharePayload { + pub ark_id: ArkId, + pub generation: u32, + /// 1..=3; equals the Shamir x-coordinate in `share[0]`. + pub index: u8, + pub(crate) share: Zeroizing<[u8; SHARE_LEN]>, +} + +impl std::fmt::Debug for SharePayload { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SharePayload") + .field("ark_id", &self.ark_id) + .field("generation", &self.generation) + .field("index", &self.index) + .finish_non_exhaustive() + } +} + +impl SharePayload { + pub(crate) fn new(ark_id: ArkId, generation: u32, share: &[u8]) -> Result { + let share: [u8; SHARE_LEN] = share.try_into().map_err(|_| EscrowError::Shamir)?; + let payload = Self { + ark_id, + generation, + index: share[0], + share: Zeroizing::new(share), + }; + payload.check_index()?; + Ok(payload) + } + + fn check_index(&self) -> Result<(), EscrowError> { + if self.index == 0 || self.index > HOLDERS || self.share[0] != self.index { + return Err(EscrowError::ShareFormat); + } + Ok(()) + } + + /// The bytes sealed to the holder. Zeroized on drop. + pub fn encode(&self) -> Zeroizing> { + let mut out = Zeroizing::new(Vec::with_capacity(PAYLOAD_LEN)); + out.extend_from_slice(MAGIC); + out.extend_from_slice(&self.ark_id.0); + out.extend_from_slice(&self.generation.to_be_bytes()); + out.push(self.index); + out.extend_from_slice(self.share.as_slice()); + out + } + + /// Strict inverse of [`encode`](Self::encode): exact length, known magic, + /// index in 1..=3 and equal to the share's x-coordinate. + pub fn decode(bytes: &[u8]) -> Result { + if bytes.len() != PAYLOAD_LEN || &bytes[..4] != MAGIC { + return Err(EscrowError::ShareFormat); + } + let ark_id = ArkId(bytes[4..20].try_into().expect("16 bytes")); + let generation = u32::from_be_bytes(bytes[20..24].try_into().expect("4 bytes")); + let index = bytes[24]; + let mut share = Zeroizing::new([0u8; SHARE_LEN]); + share.copy_from_slice(&bytes[25..]); + let payload = Self { + ark_id, + generation, + index, + share, + }; + payload.check_index()?; + Ok(payload) + } + + /// `SHA-256(lp(domain) ‖ ark_id ‖ generation ‖ index ‖ lp(share))`. + /// + /// The share is 32 uniformly random bytes plus its x-coordinate, so the + /// commitment can be public: it lets anyone holding the bundle check a + /// single share without a second one, and lets the TEE reject junk + /// before it occupies a slot. + pub fn commitment(&self) -> Digest32 { + let mut pre = Vec::with_capacity(96); + lp(&mut pre, COMMIT_DOMAIN); + pre.extend_from_slice(&self.ark_id.0); + pre.extend_from_slice(&self.generation.to_be_bytes()); + pre.push(self.index); + lp(&mut pre, self.share.as_slice()); + let digest = Digest32(sha256(&pre)); + zeroize::Zeroize::zeroize(&mut pre); + digest + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample(index: u8) -> SharePayload { + let mut share = [9u8; SHARE_LEN]; + share[0] = index; + SharePayload::new(ArkId([1; 16]), 1, &share).unwrap() + } + + #[test] + fn decode_rejects_wrong_length_magic_and_index() { + let good = sample(2).encode(); + assert_eq!(SharePayload::decode(&good).unwrap().index, 2); + assert!(SharePayload::decode(&good[..PAYLOAD_LEN - 1]).is_err()); + let mut extra = good.to_vec(); + extra.push(0); + assert!(SharePayload::decode(&extra).is_err()); + let mut bad_magic = good.to_vec(); + bad_magic[0] = b'X'; + assert!(SharePayload::decode(&bad_magic).is_err()); + for index in [0u8, 4, 255] { + let mut bad = good.to_vec(); + bad[24] = index; + bad[25] = index; + assert!(SharePayload::decode(&bad).is_err(), "index {index}"); + } + // Index byte disagreeing with the Shamir x-coordinate. + let mut skew = good.to_vec(); + skew[24] = 3; + assert!(SharePayload::decode(&skew).is_err()); + } + + #[test] + fn commitment_binds_every_field() { + let base = sample(1); + let c = base.commitment(); + let mut other_id = base.clone(); + other_id.ark_id = ArkId([2; 16]); + let mut other_gen = base.clone(); + other_gen.generation = 2; + let mut other_byte = base.clone(); + other_byte.share[32] ^= 1; + for changed in [other_id, other_gen, other_byte] { + assert_ne!(changed.commitment(), c); + } + } +} diff --git a/tinycloud-at-rest-escrow/src/test_support.rs b/tinycloud-at-rest-escrow/src/test_support.rs new file mode 100644 index 00000000..9548fb0c --- /dev/null +++ b/tinycloud-at-rest-escrow/src/test_support.rs @@ -0,0 +1,105 @@ +//! Test-only identities. `TagIdentity` is a software P-256 key that opens +//! `p256tag` stanzas the way age-plugin-yubikey does with a hardware key, so +//! tests can round-trip shares sealed to `age1tag1…` recipients. + +use std::io::Read; + +use age_core::format::{FileKey, Stanza}; +use base64::Engine; +use hpke::Deserializable; +use p256::elliptic_curve::sec1::ToEncodedPoint; +use zeroize::Zeroizing; + +use crate::{Ark, ArkId, EscrowBundle, HolderRecipient, SharePayload}; + +type Kem = hpke::kem::DhP256HkdfSha256; +const P256TAG_SALT: &[u8] = b"age-encryption.org/p256tag"; + +pub(crate) struct TagIdentity(::PrivateKey); + +/// A fresh P-256 key and its `age1tag1…` recipient. +pub(crate) fn tag_identity() -> (TagIdentity, String) { + let sk = p256::SecretKey::random(&mut rand::rngs::OsRng); + let compressed = sk.public_key().to_encoded_point(true); + let hrp = bech32::Hrp::parse("age1tag").unwrap(); + let recipient = age_core::primitives::bech32_encode(hrp, compressed.as_bytes()); + let hpke_sk = ::PrivateKey::from_bytes(&sk.to_bytes()).unwrap(); + (TagIdentity(hpke_sk), recipient) +} + +impl age::Identity for TagIdentity { + fn unwrap_stanza(&self, stanza: &Stanza) -> Option> { + if stanza.tag != "p256tag" || stanza.args.len() != 2 { + return None; + } + let enc = base64::engine::general_purpose::STANDARD_NO_PAD + .decode(&stanza.args[1]) + .ok()?; + let enc = ::EncappedKey::from_bytes(&enc).ok()?; + let opened = + age_core::primitives::hpke_open::(&enc, &self.0, P256TAG_SALT, &stanza.body) + .ok()?; + let key: [u8; 16] = opened.try_into().ok()?; + Some(Ok(FileKey::new(Box::new(key)))) + } +} + +pub(crate) fn x25519_identity() -> age::x25519::Identity { + age::x25519::Identity::generate() +} + +pub(crate) enum TestIdentity { + Tag(TagIdentity), + X25519(age::x25519::Identity), +} + +impl TestIdentity { + /// What `age -d -i share.age` prints. + pub(crate) fn open(&self, armored: &str) -> Result>, age::DecryptError> { + let identity: &dyn age::Identity = match self { + Self::Tag(i) => i, + Self::X25519(i) => i, + }; + let decryptor = + age::Decryptor::new_buffered(age::armor::ArmoredReader::new(armored.as_bytes()))?; + let mut reader = decryptor.decrypt(std::iter::once(identity))?; + let mut out = Zeroizing::new(Vec::new()); + reader.read_to_end(&mut out)?; + Ok(out) + } +} + +/// An ARK and three holders: two hardware-tag keys and one X25519 key. +pub(crate) fn escrow_fixture() -> (Ark, ArkId, Vec, Vec) { + let (sam, sam_r) = tag_identity(); + let (hunter, hunter_r) = tag_identity(); + let safe = x25519_identity(); + let safe_r = safe.to_public().to_string(); + let holders = [("sam", sam_r), ("hunter", hunter_r), ("safe", safe_r)] + .into_iter() + .map(|(holder, recipient)| HolderRecipient { + holder: holder.into(), + recipient, + }) + .collect(); + let ids = vec![ + TestIdentity::Tag(sam), + TestIdentity::Tag(hunter), + TestIdentity::X25519(safe), + ]; + (Ark::generate(), ArkId::generate(), holders, ids) +} + +/// Every holder opens their own share. +pub(crate) fn open_all(bundle: &EscrowBundle, ids: &[TestIdentity]) -> Vec { + bundle + .shares + .iter() + .zip(ids) + .map(|(share, id)| { + bundle + .open_share(&id.open(&share.ciphertext).unwrap()) + .unwrap() + }) + .collect() +} From 8713c0e63362036926c6ec2ab79654df73b7ed43 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Sat, 17 Oct 2026 07:05:26 +0000 Subject: [PATCH 2/6] fix(TC-887): escrow CLI in its own workspace; review round 1 (#325) Workspace isolation: - tinycloud-at-rest-cli is excluded from the node workspace, with its own Cargo.lock and CI job (fmt, clippy, tests incl. stock age v1.3.2 interop, sha256-pinned). dcap-qvl no longer enters the node's dependency graph: `cargo tree -p tinycloud-node -e features` is identical to main and the root lockfile only adds the escrow crate's dependencies. Escrow crate: - Accept age1yubikey1 (piv-p256) recipients, the format released age-plugin-yubikey 0.5.1 prints and decrypts, sealed exactly as the plugin's wrap_file_key; age1tag1 stays supported. - RecipientPolicy: X25519 software recipients are refused unless explicitly allowed (tests and self-host). - Bundle, record and recovery response are parsed version-first; RecoveryInfo gains a version. Clearer share/commitment errors. - forbid(unsafe_code). CLI: - TCB status is a fixed class: UpToDate passes; SWHardeningNeeded / ConfigurationNeeded / ConfigurationAndSWHardeningNeeded only if allowed and every advisory ID is pinned; OutOfDate*, TDRelaunch*, Revoked and unknown never pass. Status, advisories and collateral dates are printed. - Local checks (stale bundle, nonce, binding) run before any PCCS/DCAP work; PCCS fetch has a 60 s timeout. - --expect-efp and --allow-software on every bundle command; exit codes 0 OK / 1 REFUSED / 2 usage / 3 ERROR; verify-node shows node state and shares held; verify-record renamed record-efp and marked NOT AUTHENTICATED. - New: self-test (ceremony step 0 round trip with the TEE's sealing code), recipient-key (compare with a YubiKey attestation), and verify-compose (compose_hash + every image pinned by @sha256:). - Tests: every TCB class from a genuine quote and collateral, identity event ordering/duplication, v2 preimage, malformed logs, binary-level exit codes and messages, stock age interop (X25519 round trip and a Go-produced p256tag vector). --- .github/workflows/rust.yml | 35 + Cargo.lock | 1041 +---- Cargo.toml | 4 +- Dockerfile | 1 - docs/at-rest-escrow.md | 94 +- release-plz.toml | 5 - tinycloud-at-rest-cli/.gitignore | 1 + tinycloud-at-rest-cli/Cargo.lock | 4105 ++++++++++++++++++ tinycloud-at-rest-cli/Cargo.toml | 26 +- tinycloud-at-rest-cli/src/attest.rs | 662 ++- tinycloud-at-rest-cli/src/main.rs | 385 +- tinycloud-at-rest-cli/tests/age_interop.rs | 195 + tinycloud-at-rest-cli/tests/cli.rs | 445 ++ tinycloud-at-rest-escrow/Cargo.toml | 11 +- tinycloud-at-rest-escrow/src/bundle.rs | 170 +- tinycloud-at-rest-escrow/src/lib.rs | 48 +- tinycloud-at-rest-escrow/src/piv.rs | 96 + tinycloud-at-rest-escrow/src/recipients.rs | 143 +- tinycloud-at-rest-escrow/src/record.rs | 18 +- tinycloud-at-rest-escrow/src/recovery.rs | 19 +- tinycloud-at-rest-escrow/src/test_support.rs | 64 +- 21 files changed, 6307 insertions(+), 1261 deletions(-) create mode 100644 tinycloud-at-rest-cli/.gitignore create mode 100644 tinycloud-at-rest-cli/Cargo.lock create mode 100644 tinycloud-at-rest-cli/tests/age_interop.rs create mode 100644 tinycloud-at-rest-cli/tests/cli.rs create mode 100644 tinycloud-at-rest-escrow/src/piv.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e2ac72d5..c329ceec 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -180,6 +180,41 @@ jobs: - name: Fmt run: cargo fmt --all -- --check + # TC-887: the at-rest ceremony CLI is its own Cargo workspace (with its own + # lockfile) so its DCAP verifier never changes the node's dependency graph. + at-rest-cli: + name: at-rest ceremony CLI (own workspace) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout TinyCloud repository + uses: actions/checkout@v4 + + - name: Install reviewed Rust toolchain + uses: dtolnay/rust-toolchain@1.97.1 + with: + components: clippy, rustfmt + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 + with: + shared-key: rust-1.97.1-at-rest-cli + workspaces: tinycloud-at-rest-cli -> target + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Fmt + run: cargo fmt --manifest-path tinycloud-at-rest-cli/Cargo.toml -- --check + - name: Clippy + run: RUSTFLAGS="-Dwarnings" cargo clippy --locked --manifest-path tinycloud-at-rest-cli/Cargo.toml --all-targets + # Interop with the stock age CLI (tests/age_interop.rs), pinned by digest. + - name: Install age v1.3.2 + run: | + curl -fsSL -o "$RUNNER_TEMP/age.tgz" https://github.com/FiloSottile/age/releases/download/v1.3.2/age-v1.3.2-linux-amd64.tar.gz + echo "cbe24006683f8eb669266162894b9a522a1af52f2665fbc63a4bb032ed26ac10 $RUNNER_TEMP/age.tgz" | sha256sum -c - + tar -xzf "$RUNNER_TEMP/age.tgz" -C "$RUNNER_TEMP" + echo "AGE_BIN_DIR=$RUNNER_TEMP/age" >> "$GITHUB_ENV" + - name: Test + run: cargo test --locked --manifest-path tinycloud-at-rest-cli/Cargo.toml + capabilities: runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 8918f2fd..65fd4545 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,14 +77,14 @@ dependencies = [ "cipher", "cookie-factory", "hkdf", - "hmac 0.12.1", + "hmac", "hpke", "i18n-embed", "i18n-embed-fl", "lazy_static", "ml-kem", "nom 8.0.0", - "p256 0.13.2", + "p256", "pin-project", "rand 0.8.5", "rust-embed", @@ -490,7 +490,7 @@ dependencies = [ "alloy-primitives 0.7.7", "async-trait", "auto_impl", - "elliptic-curve 0.13.8", + "elliptic-curve", "k256", "thiserror 1.0.69", ] @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.104" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" @@ -1182,12 +1182,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "asn1_der" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" - [[package]] name = "async-attributes" version = "1.1.2" @@ -1482,29 +1476,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "aws-lc-rs" -version = "1.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - [[package]] name = "aws-sdk-dynamodb" version = "0.19.0" @@ -1864,12 +1835,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base16ct" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" - [[package]] name = "base256emoji" version = "1.0.2" @@ -1904,12 +1869,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - [[package]] name = "base64ct" version = "1.8.3" @@ -2100,7 +2059,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "hybrid-array 0.4.15", + "hybrid-array 0.4.10", ] [[package]] @@ -2130,26 +2089,25 @@ dependencies = [ [[package]] name = "borsh" -version = "1.8.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", - "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.8.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.114", ] [[package]] @@ -2354,17 +2312,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "chacha20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "rand_core 0.10.1", -] - [[package]] name = "chacha20poly1305" version = "0.10.1" @@ -2372,7 +2319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20 0.9.1", + "chacha20", "cipher", "poly1305", "zeroize", @@ -2380,9 +2327,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.45" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -2507,21 +2454,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "cmov" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" - [[package]] name = "colorchoice" version = "1.0.5" @@ -2534,16 +2466,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337cdbf3f1a0e643b4a7d1a2ffa39d22342fb6ee25739b5cfb997c28b3586422" -[[package]] -name = "combine" -version = "4.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "comfy-table" version = "7.1.2" @@ -2713,16 +2635,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2748,12 +2660,6 @@ dependencies = [ "ciborium-io", ] -[[package]] -name = "cpubits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2805,30 +2711,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -2885,21 +2767,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "crypto-bigint" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" -dependencies = [ - "cpubits", - "ctutils", - "hybrid-array 0.4.15", - "num-traits", - "rand_core 0.10.1", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -2917,8 +2784,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "hybrid-array 0.4.15", - "rand_core 0.10.1", + "hybrid-array 0.4.10", ] [[package]] @@ -2960,16 +2826,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "ctutils" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" -dependencies = [ - "cmov", - "subtle", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2980,7 +2836,7 @@ dependencies = [ "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto 0.2.9", + "fiat-crypto", "rustc_version 0.4.1", "subtle", "zeroize", @@ -3141,59 +2997,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "dcap-qvl" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b992c9511adf7135ae7c63c023c9303a3615c0f38d7b39514dcc262f91c6adda" -dependencies = [ - "anyhow", - "asn1_der", - "base64 0.23.1", - "borsh", - "byteorder", - "chrono", - "const-oid 0.10.2", - "dcap-qvl-webpki", - "der 0.8.2", - "derive_more 2.1.1", - "futures", - "hex", - "log", - "p256 0.14.0", - "parity-scale-codec", - "pem 4.0.0", - "reqwest 0.13.5", - "ring 0.17.14", - "rustls-pki-types", - "scale-info", - "serde", - "serde-human-bytes", - "serde_json", - "sha2 0.11.1", - "signature 3.0.0", - "tracing", - "urlencoding", - "x509-cert", -] - -[[package]] -name = "dcap-qvl-webpki" -version = "0.103.15+dcap.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8f2341b6a0f80654c55d0753d420aa306c97b5234c3a47f7e4ccfb6a695b2e" -dependencies = [ - "ecdsa 0.17.0", - "ed25519-dalek", - "p256 0.14.0", - "p384 0.14.0", - "ring 0.17.14", - "rustls-pki-types", - "sha2 0.10.9", - "signature 2.2.0", - "untrusted 0.9.0", -] - [[package]] name = "decoded-char" version = "0.1.1" @@ -3222,18 +3025,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" -dependencies = [ - "const-oid 0.10.2", - "der_derive", - "flagset", - "zeroize", -] - [[package]] name = "der-parser" version = "7.0.0" @@ -3262,17 +3053,6 @@ dependencies = [ "rusticata-macros", ] -[[package]] -name = "der_derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "deranged" version = "0.5.5" @@ -3318,33 +3098,13 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl 1.0.0", -] - [[package]] name = "derive_more" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ - "derive_more-impl 2.1.1", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", + "derive_more-impl", ] [[package]] @@ -3457,7 +3217,7 @@ dependencies = [ "iref", "k256", "multibase 0.9.3", - "p256 0.13.2", + "p256", "serde_json", "simple_asn1", "ssi-dids-core", @@ -3552,7 +3312,6 @@ dependencies = [ "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", - "ctutils", ] [[package]] @@ -3619,26 +3378,12 @@ checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der 0.7.10", "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", + "elliptic-curve", + "rfc6979", + "signature", "spki 0.7.3", ] -[[package]] -name = "ecdsa" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" -dependencies = [ - "der 0.8.2", - "digest 0.11.3", - "elliptic-curve 0.14.1", - "rfc6979 0.6.0", - "signature 3.0.0", - "zeroize", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -3646,7 +3391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8 0.10.2", - "signature 2.2.0", + "signature", ] [[package]] @@ -3715,48 +3460,29 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct 0.2.0", + "base16ct", "crypto-bigint 0.5.5", "digest 0.10.7", - "ff 0.13.1", + "ff", "generic-array 0.14.9", - "group 0.13.0", + "group", "hkdf", "pem-rfc7468 0.7.0", "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1 0.7.3", + "sec1", "subtle", "tap", "zeroize", ] -[[package]] -name = "elliptic-curve" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" -dependencies = [ - "base16ct 1.0.0", - "crypto-bigint 0.7.5", - "crypto-common 0.2.2", - "digest 0.11.3", - "ff 0.14.0", - "group 0.14.0", - "hybrid-array 0.4.15", - "rand_core 0.10.1", - "sec1 0.8.1", - "subtle", - "zeroize", -] - [[package]] name = "elliptic-curve-tools" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de2b6fae800f08032a6ea32995b52925b1d451bff9d445c8ab2932323277faf" dependencies = [ - "elliptic-curve 0.13.8", + "elliptic-curve", "heapless", "hex", "multiexp", @@ -3920,28 +3646,12 @@ dependencies = [ "subtle", ] -[[package]] -name = "ff" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" -dependencies = [ - "rand_core 0.10.1", - "subtle", -] - [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "fiat-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" - [[package]] name = "figment" version = "0.10.19" @@ -4003,12 +3713,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "flagset" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" - [[package]] name = "flatbuffers" version = "25.12.19" @@ -4128,12 +3832,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "funty" version = "1.1.0" @@ -4313,9 +4011,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.5" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" +checksum = "fb130435a959a8d525e6bca66ff6c40981a300ee96d70e3ef56f046556d614a3" dependencies = [ "rustversion", "typenum", @@ -4343,23 +4041,11 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", -] - [[package]] name = "ghash" version = "0.5.1" @@ -4394,22 +4080,11 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.1", + "ff", "rand_core 0.6.4", "subtle", ] -[[package]] -name = "group" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" -dependencies = [ - "ff 0.14.0", - "rand_core 0.10.1", - "subtle", -] - [[package]] name = "h2" version = "0.3.27" @@ -4593,83 +4268,13 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" -[[package]] -name = "hickory-net" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "futures-channel", - "futures-io", - "futures-util", - "hickory-proto", - "idna", - "ipnet", - "jni", - "rand 0.10.3", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" -dependencies = [ - "data-encoding", - "idna", - "ipnet", - "jni", - "once_cell", - "prefix-trie", - "rand 0.10.3", - "ring 0.17.14", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-net", - "hickory-proto", - "ipconfig", - "ipnet", - "jni", - "moka", - "ndk-context", - "once_cell", - "parking_lot", - "rand 0.10.3", - "resolv-conf", - "smallvec", - "system-configuration 0.7.0", - "thiserror 2.0.18", - "tokio", - "tracing", -] - [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac 0.12.1", + "hmac", ] [[package]] @@ -4681,15 +4286,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.3", -] - [[package]] name = "home" version = "0.5.12" @@ -4711,8 +4307,8 @@ dependencies = [ "digest 0.10.7", "generic-array 0.14.9", "hkdf", - "hmac 0.12.1", - "p256 0.13.2", + "hmac", + "p256", "rand_core 0.6.4", "sha2 0.10.9", "subtle", @@ -4797,13 +4393,11 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.15" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ - "subtle", "typenum", - "zeroize", ] [[package]] @@ -4864,7 +4458,7 @@ dependencies = [ "hyper 0.14.32", "log", "rustls 0.19.1", - "rustls-native-certs 0.5.0", + "rustls-native-certs", "tokio", "tokio-rustls 0.22.0", "webpki", @@ -5314,19 +4908,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" -[[package]] -name = "ipconfig" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" -dependencies = [ - "socket2 0.6.1", - "widestring", - "windows-registry", - "windows-result", - "windows-sys 0.61.2", -] - [[package]] name = "ipld-core" version = "0.4.2" @@ -5343,9 +4924,6 @@ name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -dependencies = [ - "serde", -] [[package]] name = "iref" @@ -5430,55 +5008,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version 0.4.1", - "simd_cesu8", - "syn 2.0.114", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.114", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -5696,11 +5225,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "once_cell", "sha2 0.10.9", - "signature 2.2.0", + "signature", ] [[package]] @@ -6230,23 +5759,6 @@ dependencies = [ "sha3", ] -[[package]] -name = "moka" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - [[package]] name = "mown" version = "0.2.2" @@ -6321,8 +5833,8 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" dependencies = [ - "ff 0.13.1", - "group 0.13.0", + "ff", + "group", "rand_core 0.6.4", "rustversion", "std-shims", @@ -6433,20 +5945,14 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.1.6", + "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - [[package]] name = "nom" version = "7.1.3" @@ -6658,10 +6164,6 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] [[package]] name = "once_cell_polyfill" @@ -6707,12 +6209,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - [[package]] name = "openssl-src" version = "300.5.4+3.5.4" @@ -6874,49 +6370,22 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", + "ecdsa", + "elliptic-curve", + "primeorder", "sha2 0.10.9", ] -[[package]] -name = "p256" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" -dependencies = [ - "ecdsa 0.17.0", - "elliptic-curve 0.14.1", - "primefield", - "primeorder 0.14.0", - "sha2 0.11.1", -] - [[package]] name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", - "sha2 0.10.9", -] - -[[package]] -name = "p384" -version = "0.14.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa 0.17.0", - "elliptic-curve 0.14.1", - "fiat-crypto 0.3.0", - "primefield", - "primeorder 0.14.0", - "sha2 0.11.1", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", ] [[package]] @@ -7000,7 +6469,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac 0.12.1", + "hmac", ] [[package]] @@ -7046,16 +6515,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "pem" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" -dependencies = [ - "base64 0.23.1", - "serde_core", -] - [[package]] name = "pem-rfc7468" version = "0.3.1" @@ -7250,12 +6709,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - [[package]] name = "potential_utf" version = "0.1.4" @@ -7280,17 +6733,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prefix-trie" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" -dependencies = [ - "either", - "ipnet", - "num-traits", -] - [[package]] name = "pretty_dtoa" version = "0.3.0" @@ -7300,38 +6742,13 @@ dependencies = [ "ryu_floating_decimal", ] -[[package]] -name = "primefield" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" -dependencies = [ - "crypto-bigint 0.7.5", - "crypto-common 0.2.2", - "ff 0.14.0", - "rand_core 0.10.1", - "subtle", - "zeroize", -] - [[package]] name = "primeorder" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve 0.13.8", -] - -[[package]] -name = "primeorder" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" -dependencies = [ - "elliptic-curve 0.14.1", - "primefield", - "wnaf", + "elliptic-curve", ] [[package]] @@ -7613,7 +7030,6 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ - "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -7658,12 +7074,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "radium" version = "0.6.2" @@ -7698,17 +7108,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" -dependencies = [ - "chacha20 0.10.2", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -7748,12 +7147,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "rand_xorshift" version = "0.4.0" @@ -7790,7 +7183,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" dependencies = [ - "pem 3.0.6", + "pem", "ring 0.17.14", "rustls-pki-types", "time", @@ -7987,69 +7380,16 @@ dependencies = [ "webpki-roots 1.0.5", ] -[[package]] -name = "reqwest" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" -dependencies = [ - "base64 0.23.1", - "bytes", - "futures-core", - "hickory-resolver", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", - "hyper-util", - "js-sys", - "log", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls 0.23.36", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper 1.0.2", - "tokio", - "tokio-rustls 0.26.4", - "tower 0.5.3", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - [[package]] name = "rfc6979" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac 0.12.1", + "hmac", "subtle", ] -[[package]] -name = "rfc6979" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" -dependencies = [ - "crypto-bigint 0.7.5", - "hmac 0.13.0", -] - [[package]] name = "ring" version = "0.16.20" @@ -8269,7 +7609,7 @@ dependencies = [ "pkcs1 0.7.5", "pkcs8 0.10.2", "rand_core 0.6.4", - "signature 2.2.0", + "signature", "spki 0.7.3", "subtle", "zeroize", @@ -8494,7 +7834,6 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring 0.17.14", @@ -8510,22 +7849,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" dependencies = [ - "openssl-probe 0.1.6", + "openssl-probe", "rustls 0.19.1", "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" -dependencies = [ - "openssl-probe 0.2.1", - "rustls-pki-types", - "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -8556,33 +7883,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls 0.23.36", - "rustls-native-certs 0.8.5", - "rustls-platform-verifier-android", - "rustls-webpki 0.103.9", - "security-framework 3.5.1", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" - [[package]] name = "rustls-webpki" version = "0.101.7" @@ -8599,7 +7899,6 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ - "aws-lc-rs", "ring 0.17.14", "rustls-pki-types", "untrusted 0.9.0", @@ -8670,31 +7969,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scale-info" -version = "2.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" -dependencies = [ - "bitvec 1.0.1", - "cfg-if", - "derive_more 1.0.0", - "parity-scale-codec", - "scale-info-derive", -] - -[[package]] -name = "scale-info-derive" -version = "2.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "schannel" version = "0.1.28" @@ -8945,7 +8219,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct 0.2.0", + "base16ct", "der 0.7.10", "generic-array 0.14.9", "pkcs8 0.10.2", @@ -8953,20 +8227,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "sec1" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" -dependencies = [ - "base16ct 1.0.0", - "ctutils", - "der 0.8.2", - "hybrid-array 0.4.15", - "subtle", - "zeroize", -] - [[package]] name = "secrecy" version = "0.10.3" @@ -8983,26 +8243,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", - "core-foundation 0.9.4", + "core-foundation", "core-foundation-sys", "libc", "num-bigint", "security-framework-sys", ] -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework-sys" version = "2.15.0" @@ -9062,17 +8309,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-human-bytes" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aff481ca1fe108deba0f217b45d9f1d494e7e7f906bcc7366d8a5648c5a1e65" -dependencies = [ - "base64 0.13.1", - "hex", - "serde", -] - [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -9176,7 +8412,6 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.13.0", "itoa", "memchr", "serde", @@ -9315,7 +8550,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" dependencies = [ - "base16ct 0.2.0", + "base16ct", "serde", ] @@ -9420,32 +8655,12 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "signature" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" -dependencies = [ - "digest 0.11.3", - "rand_core 0.10.1", -] - [[package]] name = "simd-adler32" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" -[[package]] -name = "simd_cesu8" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" -dependencies = [ - "rustc_version 0.4.1", - "simdutf8", -] - [[package]] name = "simdutf8" version = "0.1.5" @@ -9605,16 +8820,6 @@ dependencies = [ "der 0.7.10", ] -[[package]] -name = "spki" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" -dependencies = [ - "base64ct", - "der 0.8.2", -] - [[package]] name = "sqlparser" version = "0.44.0" @@ -9755,7 +8960,7 @@ dependencies = [ "generic-array 0.14.9", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "itoa", "log", "md-5", @@ -9798,7 +9003,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "home", "itoa", "log", @@ -9989,7 +9194,7 @@ dependencies = [ "iref", "k256", "keccak-hash", - "p256 0.13.2", + "p256", "pin-project", "rand 0.8.5", "ripemd160", @@ -10084,7 +9289,7 @@ dependencies = [ "linked-data", "locspan", "multibase 0.9.3", - "p256 0.13.2", + "p256", "pin-project", "rand 0.8.5", "rdf-types", @@ -10121,7 +9326,7 @@ dependencies = [ "digest 0.10.7", "getrandom 0.2.17", "hex", - "hmac 0.12.1", + "hmac", "iref", "linked-data", "rdf-types", @@ -10229,7 +9434,7 @@ dependencies = [ "num-bigint", "num-derive", "num-traits", - "p256 0.13.2", + "p256", "rand 0.8.5", "rsa 0.6.1", "serde", @@ -10256,7 +9461,7 @@ dependencies = [ "iref", "k256", "linked-data", - "p256 0.13.2", + "p256", "rand 0.8.5", "rsa 0.6.1", "serde", @@ -10301,8 +9506,8 @@ dependencies = [ "csv", "ed25519-dalek", "k256", - "p256 0.13.2", - "p384 0.13.1", + "p256", + "p384", "thiserror 1.0.69", "unsigned-varint 0.7.2", ] @@ -10484,7 +9689,7 @@ dependencies = [ "k256", "linked-data", "multibase 0.9.3", - "p256 0.13.2", + "p256", "pin-project", "rand_core 0.6.4", "rdf-types", @@ -10736,7 +9941,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -10797,7 +10001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "system-configuration-sys 0.5.0", ] @@ -10808,18 +10012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.10.0", - "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", + "core-foundation", "system-configuration-sys 0.6.0", ] @@ -10843,12 +10036,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "tap" version = "1.0.1" @@ -10997,24 +10184,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tinycloud-at-rest-cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64 0.22.1", - "clap", - "dcap-qvl", - "hex", - "rand 0.8.5", - "serde", - "serde_json", - "sha2 0.10.9", - "tinycloud-at-rest-escrow", - "tokio", - "zeroize", -] - [[package]] name = "tinycloud-at-rest-escrow" version = "0.1.0" @@ -11025,9 +10194,9 @@ dependencies = [ "bech32 0.11.1", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "hpke", - "p256 0.13.2", + "p256", "rand 0.8.5", "serde", "serde_json", @@ -11127,13 +10296,13 @@ dependencies = [ "chacha20poly1305", "chrono", "clap", - "core-foundation 0.9.4", + "core-foundation", "core-foundation-sys", "ed25519-dalek", "flate2", "futures", "hex", - "hmac 0.12.1", + "hmac", "hyper 0.14.32", "if-addrs", "k256", @@ -11145,7 +10314,7 @@ dependencies = [ "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", - "pem 3.0.6", + "pem", "percent-encoding", "pin-project", "proc-macro2", @@ -11161,7 +10330,7 @@ dependencies = [ "rustls 0.23.36", "rustls-pemfile 2.2.0", "scrypt", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "serde", "serde_ipld_dagcbor 0.6.4", @@ -11742,9 +10911,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ubyte" @@ -11995,11 +11164,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ec751bdcc8bda099e269b24cc6b4ad14f9ce8b0490c1599174070e792ecd70c" dependencies = [ "crypto-bigint 0.6.1", - "elliptic-curve 0.13.8", + "elliptic-curve", "elliptic-curve-tools", - "generic-array 1.4.5", + "generic-array 1.4.2", "hex", - "hybrid-array 0.4.15", + "hybrid-array 0.4.10", "num", "rand_core 0.6.4", "serde", @@ -12152,15 +11321,6 @@ dependencies = [ "untrusted 0.7.1", ] -[[package]] -name = "webpki-root-certs" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webpki-roots" version = "0.25.4" @@ -12195,12 +11355,6 @@ dependencies = [ "wasite", ] -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - [[package]] name = "winapi" version = "0.3.9" @@ -12576,18 +11730,6 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -[[package]] -name = "wnaf" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" -dependencies = [ - "ff 0.14.0", - "group 0.14.0", - "hybrid-array 0.4.15", - "primefield", -] - [[package]] name = "writeable" version = "0.6.2" @@ -12621,17 +11763,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "x509-cert" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b" -dependencies = [ - "const-oid 0.10.2", - "der 0.8.2", - "spki 0.8.1", -] - [[package]] name = "x509-parser" version = "0.13.2" diff --git a/Cargo.toml b/Cargo.toml index b9ceda0d..1f08753a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ members = [ "tinycloud-sdk-rs", "tinycloud-core", "tinycloud-at-rest-escrow", - "tinycloud-at-rest-cli", "dependencies/siwe", "dependencies/siwe-recap", "dependencies/cacao", @@ -23,6 +22,9 @@ exclude = [ "vendor/sqlx-core", "test/m1-realdata-e2e", "test/w5-policy-runtime-node-e2e", + # Own workspace and lockfile: its DCAP verifier (dcap-qvl) must not + # change the node's dependency graph or features (TC-887). + "tinycloud-at-rest-cli", ] [workspace.dependencies] diff --git a/Dockerfile b/Dockerfile index 60e35331..a16e3054 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,6 @@ COPY ./tinycloud-node-server/ ./tinycloud-node-server/ COPY ./tinycloud-auth/ ./tinycloud-auth/ COPY ./tinycloud-core/ ./tinycloud-core/ COPY ./tinycloud-at-rest-escrow/ ./tinycloud-at-rest-escrow/ -COPY ./tinycloud-at-rest-cli/ ./tinycloud-at-rest-cli/ COPY ./tinycloud-sdk-rs/ ./tinycloud-sdk-rs/ COPY ./tinycloud-sdk-wasm/ ./tinycloud-sdk-wasm/ COPY ./tinycloud-verifier-wasm/ ./tinycloud-verifier-wasm/ diff --git a/docs/at-rest-escrow.md b/docs/at-rest-escrow.md index b969b499..344f2024 100644 --- a/docs/at-rest-escrow.md +++ b/docs/at-rest-escrow.md @@ -1,46 +1,76 @@ # At-rest key escrow (TC-887) -Two workspace crates, both unused by the node binary so far: +Nothing in the node binary uses these two crates yet. -- `tinycloud-at-rest-escrow`: the escrow format. It splits the at-rest root key (ARK) 2-of-3 and seals the shares to age recipients. It also builds the bundle and its fingerprint, the canary, the verification record, and the recovery wire format. -- `tinycloud-at-rest-cli`: the `tinycloud-at-rest` tool that holders and operators run. +- `tinycloud-at-rest-escrow` (a node workspace member) holds the escrow format: + - the 2-of-3 split of the at-rest root key (ARK); + - shares sealed to age recipients; + - the bundle and its fingerprint; + - the canary; + - the verification record; + - the recovery wire format. +- `tinycloud-at-rest-cli` is the `tinycloud-at-rest` tool that holders and operators run. + - It is a Cargo workspace of its own, with its own `Cargo.lock`. + - Its DCAP verifier and HTTP stack never enter the node's dependency graph or feature set. Build the tool from the release tag you are verifying: ```sh -cargo build --release -p tinycloud-at-rest-cli # target/release/tinycloud-at-rest +cargo build --release --locked --manifest-path tinycloud-at-rest-cli/Cargo.toml +# tinycloud-at-rest-cli/target/release/tinycloud-at-rest ``` ## Formats -`lp(x)` is `u32 BE length ‖ x`. All digests are SHA-256 and written as lowercase hex. +`lp(x)` is `u32 BE length ‖ x`. Digests are SHA-256, written in lowercase hex. -- **Share.** The ARK is split with Shamir over GF(256) (`vsss-rs` 5.4.0), threshold 2 of 3. Share `i` has x-coordinate `i`. The sealed plaintext is `"TCS1" ‖ ark_id(16) ‖ generation u32 BE ‖ i ‖ share(33)`, 58 bytes in all. -- **Recipients.** Each share is age-encrypted (`age` 0.12.1, ASCII armor) to one holder. - - `age1tag1…`: a P-256 hardware key. Use the `age1tag1` form that `age-plugin-yubikey` prints. age encrypts to it natively, and decryption needs the plugin and the key. - - `age1…` X25519: software keys only. The CLI warns when one is used. - - `age1yubikey1…` is refused, because encrypting to it needs the plugin. +- **Share.** + - The ARK is split with Shamir over GF(256) (`vsss-rs` 5.4.0): threshold 2 of 3, and share `i` has x-coordinate `i`. + - The sealed plaintext is `"TCS1" ‖ ark_id(16) ‖ generation u32 BE ‖ i ‖ share(33)`, 58 bytes. +- **Recipients.** Each share is age-encrypted (`age` 0.12.1, ASCII armor) to one holder. The recipient type can be: + - `age1yubikey1…` (`piv-p256` stanza). This is what released age-plugin-yubikey (0.5.x) prints and decrypts. The stanza is produced exactly as the plugin's own `wrap_file_key`, so the TEE needs no plugin. + - `age1tag1…` (`p256tag` stanza). This is native age 1.3 tagged P-256. Decrypting it needs a plugin build that supports it. + - `age1…` X25519. These are software keys, refused unless software recipients are explicitly allowed (`--allow-software` on the CLI; tests and self-hosted nodes only). Every command prints a warning while that is in effect. + - A recipient format proves which kind of key decrypts, **not** where the private key lives. That a P-256 key was generated on a YubiKey is established in the escrow ceremony. - **Commitment.** `SHA-256(lp("tinycloud/at-rest/escrow-share-commit/v1") ‖ ark_id ‖ generation ‖ i ‖ lp(share))`. - **Canary.** - `canary = HKDF-SHA256(ikm = ARK, salt = ark_id, info = lp("tinycloud/at-rest/escrow-canary/v1") ‖ generation)`. - `canary_fp = SHA-256(lp("tinycloud/at-rest/escrow-canary-fp/v1") ‖ ark_id ‖ generation ‖ lp(canary))`. - **Bundle.** JSON with `version`, `ark_id`, `generation`, `threshold`, `canary_fp`, and `shares[{index, holder, recipient, commitment, ciphertext}]`. Unknown fields are rejected. - **EFP (escrow fingerprint).** `SHA-256(lp("tinycloud/at-rest/escrow-bundle/v1") ‖ version ‖ ark_id ‖ generation ‖ threshold ‖ canary_fp ‖ n ‖ for each share: i ‖ lp(holder) ‖ lp(recipient) ‖ commitment ‖ SHA-256(ciphertext))`. -- **Verification record.** JSON that names the EFP, all three indices and all three pairs. It carries an HMAC-SHA256 under `HKDF(ARK, salt = ark_id, info = lp("tinycloud/at-rest/escrow-record/v1"))`, so only the ARK holder can make or check one. -- **Recovery binding.** `xyz.tinycloud.at-rest/recovery-key/v1\0:::`. The `\0` here is two characters. A TDX quote's REPORTDATA must be `SHA-256(binding) ‖ 32 zero bytes`. +- **Verification record.** + - It is JSON naming the EFP, all three indices and all three pairs, with an HMAC-SHA256 under `HKDF(ARK, salt = ark_id, info = lp("tinycloud/at-rest/escrow-record/v1"))`. + - Only the ARK holder (the TEE) can make or check the MAC. `record-efp` checks only the public fields and says so. +- **Recovery binding.** + - The binding string is `xyz.tinycloud.at-rest/recovery-key/v1\0:::`, where `\0` is two characters. + - A TDX quote's REPORTDATA must be `SHA-256(binding) ‖ 32 zero bytes`. ## Commands +Every command that reads a bundle: +- refuses software holders unless `--allow-software` is given; +- accepts `--expect-efp E` (case-insensitive) and refuses a bundle with any other EFP. + +Bundles, records and the node's recovery response carry a `version`, which is checked before any other field. A newer format is reported as "version N; this tool reads version 1". + +Exit codes: +- `0`: OK. +- `1`: REFUSED. A check failed: don't release a share and don't pin a value. +- `2`: usage error. +- `3`: ERROR. A file, stdin or the PCCS couldn't be read. + | Command | Reads | Prints | |---|---|---| | `recipients --holder NAME=RECIPIENT` ×3 | — | `[at_rest.escrow]` TOML and the compose env line | +| `self-test --recipient R [--out DIR]` | — | a throwaway escrow whose share 1 is sealed to `R` by the TEE's own code (ceremony step 0) | | `nonce` | — | 32 random bytes, base64url | -| `inspect --bundle B [--expect-efp E]` | bundle | EFP, ark_id, generation, canary_fp, holders | +| `inspect --bundle B` | bundle | EFP, ark_id, generation, canary_fp, each holder's recipient kind | | `extract-share --bundle B --holder H` | bundle | holder H's armored age file | -| `check-share --bundle B` | decrypted share on **stdin** | which share it is; refuses on a commitment mismatch | -| `verify-node --bundle B --recovery R --nonce N --trust T [--collateral C \| --pccs-url U]` | node's recovery response | OK or REFUSED | +| `check-share --bundle B` | decrypted share on **stdin** | which share it is; refuses on a commitment mismatch (e.g. a share from an earlier split) | +| `verify-compose --app-compose A --trust T` | the deployment's `app-compose.json` | OK if it hashes to `compose_hash` and every `image:` is `…@sha256:` | +| `verify-node --bundle B --recovery R --nonce N --trust T [--collateral C \| --pccs-url U]` | node's recovery response | OK (with node state and shares held) or REFUSED | | `share-wrap` (same flags) | decrypted share on **stdin** | after `verify-node` passes: the share sealed to the node's recovery key | -| `verify-record --bundle B --record V` | record | `TINYCLOUD_AT_REST__ESCROW__VERIFIED_FP=` | +| `record-efp --bundle B --record V` | record | `TINYCLOUD_AT_REST__ESCROW__VERIFIED_FP=`, marked NOT AUTHENTICATED: the record's MAC is checked only in the TEE | A decrypted share only ever arrives on stdin (`age -d -i share.age | tinycloud-at-rest …`). It never goes on argv or to a file. @@ -48,13 +78,28 @@ A decrypted share only ever arrives on stdin (`age -d -i share.age | All of these must hold, or the command prints `REFUSED: …` and exits 1: -1. The node's `efp`, `ark_id` and `generation` equal the holder's own bundle. -2. The node's binding is exactly the one recomputed from the recovery recipient, that EFP, and the holder's nonce. The nonce must be at least 22 characters. A response made for any other nonce is stale. -3. The quote passes Intel DCAP verification with `dcap-qvl` 0.7.0. That covers the signature, the PCK chain, the CRLs, the TCB info and the QE identity. Collateral comes from the PCCS (Phala's by default) or from `--collateral`, checked at the current time. -4. The TCB status is in the trust policy's `allowed_tcb_status` (default `["UpToDate"]`), and the TD debug bit is clear. -5. MRTD and RTMR0-2 equal the trust policy. -6. The dstack event log replays to the quoted RTMR3. Its single `app-id` and `compose-hash` events come before `boot-mr-done` and equal the trust policy. -7. The quoted REPORTDATA is `SHA-256(binding) ‖ 0³²`. +1. The node answered as an attested TEE: `mode` is `dstack` and a quote is present. +2. The node's `efp`, `ark_id` and `generation` equal the holder's own bundle. +3. The node's binding is exactly the one recomputed from the recovery recipient, that EFP, and the holder's nonce (at least 22 characters). A response made for any other nonce is stale. +4. The quote passes Intel DCAP verification with `dcap-qvl` 0.7.0: signature, PCK chain, CRLs, TCB info and QE identity. Collateral comes from the PCCS (Phala's by default; Intel PCS also works) with a 60 s timeout, or from `--collateral`, and is checked at the current time. +5. The TCB status is one fixed class: + - `UpToDate` passes. + - `SWHardeningNeeded`, `ConfigurationNeeded` and `ConfigurationAndSWHardeningNeeded` pass only if the policy lists them and pins every advisory ID the TCB level carries. + - `OutOfDate*`, `TDRelaunchAdvised*`, `Revoked` and unknown statuses never pass. + - On a pass, the status, the advisory IDs and the collateral dates are printed. +6. The TD debug bit is clear. +7. MRTD and RTMR0-2 equal the trust policy. +8. The dstack event log replays to the quoted RTMR3: + - each `boot-mr-done` and `system-ready` marker appears once, in that order; + - exactly one `app-id` and one `compose-hash` event appear, each before `boot-mr-done`, and each equals the trust policy; + - v2 events must carry their canonical preimage. +9. The quoted REPORTDATA is `SHA-256(binding) ‖ 0³²`. + +Not pinned: `key-provider`, `instance-id`, `os-image-hash`, XFAM, MRCONFIGID, MROWNER and MROWNERCONFIG. RTMR0-2 and the compose hash cover what the per-boot, in-memory recovery key depends on. + +`compose_hash` pins container images only if `app-compose.json` references every image by `@sha256:` digest. Check that with `verify-compose` when building the trust policy. + +A PCCS can't forge collateral, which is Intel-signed. A malicious PCCS can, however, serve older collateral that is still valid (TCB info is valid for about 30 days), which could hide a TCB recovery for that long. Use Intel PCS when that matters. Trust policy (`--trust`), from a source independent of the node being checked: @@ -66,6 +111,7 @@ Trust policy (`--trust`), from a source independent of the node being checked: "rtmr0": "<96 hex>", "rtmr1": "<96 hex>", "rtmr2": "<96 hex>", - "allowed_tcb_status": ["UpToDate"] + "allowed_tcb_status": ["UpToDate"], + "allowed_advisories": [] } ``` diff --git a/release-plz.toml b/release-plz.toml index 5a55e262..c8f98c07 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -76,11 +76,6 @@ name = "tinycloud-at-rest-escrow" release = false publish = false -[[package]] -name = "tinycloud-at-rest-cli" -release = false -publish = false - [[package]] name = "tinycloud-sdk-wasm" release = false diff --git a/tinycloud-at-rest-cli/.gitignore b/tinycloud-at-rest-cli/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/tinycloud-at-rest-cli/.gitignore @@ -0,0 +1 @@ +/target diff --git a/tinycloud-at-rest-cli/Cargo.lock b/tinycloud-at-rest-cli/Cargo.lock new file mode 100644 index 00000000..21d235ab --- /dev/null +++ b/tinycloud-at-rest-cli/Cargo.lock @@ -0,0 +1,4105 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array 0.14.7", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "age" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd290633c2482479f70f6d1d96ae0e9f52c6a26cd5859edd47ee1fe33fc89f26" +dependencies = [ + "age-core", + "base64 0.22.1", + "bech32", + "chacha20poly1305", + "cipher", + "cookie-factory", + "hkdf", + "hmac 0.12.1", + "hpke", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "ml-kem", + "nom", + "p256 0.13.2", + "pin-project", + "rand 0.8.8", + "rust-embed", + "scrypt", + "sha2 0.10.9", + "sha3", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d4375964d1501e5f1b32aef2ead573913893ff238448d4e9fdf1522d828656" +dependencies = [ + "base64 0.22.1", + "bech32", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "hpke", + "io_tee", + "nom", + "rand 0.8.8", + "secrecy", + "sha2 0.10.9", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "asn1_der" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array 0.4.15", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96272c2ff28b807e09250b180ad1fb7889a3258f7455759b5c3c58b719467130" +dependencies = [ + "num-traits", + "rand_core 0.6.4", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array 0.4.15", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array 0.4.15", + "rand_core 0.10.1", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "dcap-qvl" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b992c9511adf7135ae7c63c023c9303a3615c0f38d7b39514dcc262f91c6adda" +dependencies = [ + "anyhow", + "asn1_der", + "base64 0.23.1", + "borsh", + "byteorder", + "chrono", + "const-oid 0.10.2", + "dcap-qvl-webpki", + "der 0.8.2", + "derive_more 2.1.1", + "futures", + "hex", + "log", + "p256 0.14.0", + "parity-scale-codec", + "pem", + "reqwest", + "ring", + "rustls-pki-types", + "scale-info", + "serde", + "serde-human-bytes", + "serde_json", + "sha2 0.11.1", + "signature 3.0.0", + "tracing", + "urlencoding", + "x509-cert", +] + +[[package]] +name = "dcap-qvl-webpki" +version = "0.103.15+dcap.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8f2341b6a0f80654c55d0753d420aa306c97b5234c3a47f7e4ccfb6a695b2e" +dependencies = [ + "ecdsa 0.17.0", + "ed25519-dalek", + "p256 0.14.0", + "p384", + "ring", + "rustls-pki-types", + "sha2 0.10.9", + "signature 2.2.0", + "untrusted", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", + "der_derive", + "flagset", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl 2.1.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der 0.8.2", + "digest 0.11.3", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2 0.10.9", + "subtle", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff 0.13.1", + "generic-array 0.14.7", + "group 0.13.0", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array 0.4.15", + "rand_core 0.10.1", + "sec1 0.8.1", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de2b6fae800f08032a6ea32995b52925b1d451bff9d445c8ab2932323277faf" +dependencies = [ + "elliptic-curve 0.13.8", + "heapless", + "hex", + "multiexp", + "serde", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror 2.0.21", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "generic-array" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" +dependencies = [ + "rustversion", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-net" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto", + "idna", + "ipnet", + "jni", + "rand 0.10.3", + "thiserror 2.0.21", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.3", + "ring", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.3", + "resolv-conf", + "smallvec", + "system-configuration", + "thiserror 2.0.21", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "hpke" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4917627a14198c3603282c5158b815ad5534795451d3c074b53cf3cee0960b11" +dependencies = [ + "aead", + "aes-gcm", + "chacha20poly1305", + "digest 0.10.7", + "generic-array 0.14.7", + "hkdf", + "hmac 0.12.1", + "p256 0.13.2", + "rand_core 0.6.4", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a217bbb075dcaefb292efa78897fc0678245ca67f265d12c351e42268fcb0305" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602e6bd30c3db2749e13e38b363a3d98d9d41de1d8de7a79c31bb69e45b47cda" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error3", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "kem" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f" +dependencies = [ + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ml-kem" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" +dependencies = [ + "hybrid-array 0.2.3", + "kem", + "rand_core 0.6.4", + "sha3", +] + +[[package]] +name = "moka" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "multiexp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" +dependencies = [ + "ff 0.13.1", + "group 0.13.0", + "rand_core 0.6.4", + "rustversion", + "std-shims", + "zeroize", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.8.8", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "rand 0.8.8", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + +[[package]] +name = "p256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", + "primeorder 0.14.0", + "sha2 0.11.1", +] + +[[package]] +name = "p384" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "fiat-crypto 0.3.0", + "primefield", + "primeorder 0.14.0", + "sha2 0.11.1", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve 0.14.1", + "primefield", + "wnaf", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr3" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error3" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" +dependencies = [ + "proc-macro-error-attr3", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20 0.10.2", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "hickory-resolver", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint 0.7.5", + "hmac 0.13.0", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-embed" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19afa5b4b6a611de00bd1bdae6ae6f39084c9399f0679c3f52d8469cf335cc23" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d8afda6374eac59e066abee06d265247ebbaf3006cf878e2879e8356e34053" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" +dependencies = [ + "sha2 0.11.1", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more 1.0.0", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2 0.10.9", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.10", + "generic-array 0.14.7", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils", + "der 0.8.2", + "hybrid-array 0.4.15", + "subtle", + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-human-bytes" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aff481ca1fe108deba0f217b45d9f1d494e7e7f906bcc7366d8a5648c5a1e65" +dependencies = [ + "base64 0.13.1", + "hex", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct 0.2.0", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "std-shims" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227c4f8561598188d0df96dbe749824576174bba278b5b6bb2eacff1066067d0" +dependencies = [ + "hashbrown 0.16.1", + "rustversion", + "spin", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinycloud-at-rest-cli" +version = "0.1.0" +dependencies = [ + "age", + "age-core", + "anyhow", + "base64 0.22.1", + "bech32", + "clap", + "dcap-qvl", + "hex", + "hpke", + "p256 0.13.2", + "rand 0.8.8", + "serde", + "serde_json", + "sha2 0.10.9", + "tinycloud-at-rest-escrow", + "tokio", + "zeroize", +] + +[[package]] +name = "tinycloud-at-rest-escrow" +version = "0.1.0" +dependencies = [ + "age", + "age-core", + "base64 0.22.1", + "hex", + "hkdf", + "hmac 0.12.1", + "p256 0.13.2", + "rand 0.8.8", + "serde", + "serde_json", + "sha2 0.10.9", + "subtle", + "thiserror 2.0.21", + "vsss-rs", + "zeroize", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsss-rs" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ec751bdcc8bda099e269b24cc6b4ad14f9ce8b0490c1599174070e792ecd70c" +dependencies = [ + "crypto-bigint 0.6.1", + "elliptic-curve 0.13.8", + "elliptic-curve-tools", + "generic-array 1.4.5", + "hex", + "hybrid-array 0.4.15", + "num", + "rand_core 0.6.4", + "serde", + "sha3", + "subtle", + "zeroize", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff 0.14.0", + "group 0.14.0", + "hybrid-array 0.4.15", + "primefield", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-cert" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b" +dependencies = [ + "const-oid 0.10.2", + "der 0.8.2", + "spki 0.8.1", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tinycloud-at-rest-cli/Cargo.toml b/tinycloud-at-rest-cli/Cargo.toml index c650adff..144498ec 100644 --- a/tinycloud-at-rest-cli/Cargo.toml +++ b/tinycloud-at-rest-cli/Cargo.toml @@ -5,6 +5,10 @@ edition = "2021" publish = false description = "Escrow ceremony and recovery tool for TinyCloud encryption at rest (TC-887)" +# A workspace of its own, with its own Cargo.lock: the DCAP verifier and its +# HTTP stack must not change the node's dependency graph or features. +[workspace] + [[bin]] name = "tinycloud-at-rest" path = "src/main.rs" @@ -16,11 +20,23 @@ tinycloud-at-rest-escrow = { path = "../tinycloud-at-rest-escrow" } dcap-qvl = { version = "=0.7.0" } anyhow = "1" clap = { version = "4", features = ["derive"] } -base64 = { workspace = true } -hex = { workspace = true } +base64 = "0.22" +hex = "0.4" rand = "0.8" -serde = { workspace = true } -serde_json = { workspace = true } +serde = { version = "1", features = ["derive"] } +serde_json = "1" sha2 = "0.10" -tokio = { workspace = true } +tokio = { version = "1", features = ["macros", "rt", "time"] } zeroize = "1" +# `self-test`: throwaway P-256 co-holders encoded as age1yubikey1. +p256 = "0.13" +bech32 = "0.11" + +[dev-dependencies] +# Tests only: drive a genuine quote and collateral through every TCB status +# class. Never enabled in the built binary. +dcap-qvl = { version = "=0.7.0", features = ["danger-allow-tcb-override"] } +# Holder-side decryption and the p256tag interop vector in tests/. +age = { version = "=0.12.1", default-features = false, features = ["armor"] } +age-core = "=0.12.0" +hpke = { version = "0.12", default-features = false, features = ["alloc", "p256"] } diff --git a/tinycloud-at-rest-cli/src/attest.rs b/tinycloud-at-rest-cli/src/attest.rs index 02882cc5..736d5500 100644 --- a/tinycloud-at-rest-cli/src/attest.rs +++ b/tinycloud-at-rest-cli/src/attest.rs @@ -6,7 +6,10 @@ //! `docs/attestation.md` and the registry's dstack verifier: //! //! 1. DCAP: quote signature, PCK chain, CRLs, TCB info and QE identity, with -//! collateral (`dcap-qvl`). TCB status must be in the policy's allow-list. +//! collateral (`dcap-qvl`). The TCB status is a fixed class: `UpToDate` +//! passes; the hardening/configuration classes pass only if the policy +//! allows them and pins every advisory ID; `OutOfDate*`, `TDRelaunch*`, +//! `Revoked` and anything unknown never pass. //! 2. TD attributes: the debug bit must be clear. //! 3. MRTD and RTMR0-2 equal the pinned OS-image measurements. //! 4. The dstack event log replays to the quoted RTMR3, and its `app-id` and @@ -14,13 +17,27 @@ //! 5. REPORTDATA equals SHA-256 of the binding the holder recomputes from //! their own fresh nonce, the bundle's EFP and the recovery recipient. +use std::time::Duration; + use anyhow::{anyhow, bail, ensure, Context, Result}; +use dcap_qvl::TcbStatus; use serde::Deserialize; -use sha2::{Digest, Sha384}; +use sha2::{Digest, Sha256, Sha384}; use tinycloud_at_rest_escrow::{ binding_digest, recovery_binding, validate_nonce, EscrowBundle, RecoveryInfo, }; +/// A TCB status a trust policy may accept. `OutOfDate*`, `TDRelaunchAdvised*` +/// and `Revoked` are deliberately not representable. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum AcceptedTcb { + UpToDate, + SWHardeningNeeded, + ConfigurationNeeded, + ConfigurationAndSWHardeningNeeded, +} + /// Measurements a holder trusts, from a source independent of the node /// being verified (the release's published measurements, Phala's dashboard /// cross-checked by Sam). Hex, lowercase. @@ -36,13 +53,17 @@ pub struct TrustPolicy { pub rtmr0: String, pub rtmr1: String, pub rtmr2: String, - /// Accepted DCAP TCB statuses. Default: only `UpToDate`. - #[serde(default = "default_tcb_status")] - pub allowed_tcb_status: Vec, + /// Accepted TCB statuses. Default: only `UpToDate`. + #[serde(default = "default_tcb")] + pub allowed_tcb_status: Vec, + /// Intel advisory IDs (e.g. `INTEL-SA-00837`) the holder accepts. Every + /// advisory the quote's TCB level carries must be listed here. + #[serde(default)] + pub allowed_advisories: Vec, } -fn default_tcb_status() -> Vec { - vec!["UpToDate".into()] +fn default_tcb() -> Vec { + vec![AcceptedTcb::UpToDate] } fn hex_exact(value: &str, bytes: usize, label: &str) -> Result> { @@ -72,6 +93,12 @@ impl TrustPolicy { !self.allowed_tcb_status.is_empty(), "trust policy allowed_tcb_status is empty" ); + for id in &self.allowed_advisories { + ensure!( + id.starts_with("INTEL-SA-") && id.len() <= 32, + "trust policy advisory {id:?} is not an INTEL-SA-… id" + ); + } Ok(()) } } @@ -79,13 +106,35 @@ impl TrustPolicy { /// The fields of a DCAP-verified TD report this tool decides on. #[derive(Clone, Debug)] pub struct AttestedTd { - pub tcb_status: String, + pub tcb_status: TcbStatus, + pub advisory_ids: Vec, pub td_attributes: [u8; 8], pub mr_td: [u8; 48], pub rtmr: [[u8; 48]; 4], pub report_data: [u8; 64], } +impl AttestedTd { + fn from_report(verified: &dcap_qvl::verify::VerifiedReport) -> Result { + // dcap-qvl prints the enum variant name; anything else is refused. + let tcb_status: TcbStatus = + serde_json::from_value(serde_json::Value::String(verified.status.clone())) + .map_err(|_| anyhow!("unknown TCB status {:?}", verified.status))?; + let td = verified + .report + .as_td10() + .ok_or_else(|| anyhow!("quote is not a TDX quote"))?; + Ok(Self { + tcb_status, + advisory_ids: verified.advisory_ids.clone(), + td_attributes: td.td_attributes, + mr_td: td.mr_td, + rtmr: [td.rt_mr0, td.rt_mr1, td.rt_mr2, td.rt_mr3], + report_data: td.report_data, + }) + } +} + /// Step 1: full DCAP verification of `quote` against `collateral` at `now`. pub fn verify_quote( quote: &[u8], @@ -94,26 +143,129 @@ pub fn verify_quote( ) -> Result { let verified = dcap_qvl::verify::verify(quote, collateral, now_secs) .map_err(|e| anyhow!("TDX quote failed DCAP verification: {e}"))?; - let td = verified - .report - .as_td10() - .ok_or_else(|| anyhow!("quote is not a TDX quote"))?; - Ok(AttestedTd { - tcb_status: verified.status.clone(), - td_attributes: td.td_attributes, - mr_td: td.mr_td, - rtmr: [td.rt_mr0, td.rt_mr1, td.rt_mr2, td.rt_mr3], - report_data: td.report_data, - }) + AttestedTd::from_report(&verified) } -/// Fetch collateral for `quote` from a PCCS (default: Phala's). +/// The TCB decision. Returns a line to print on success. +pub fn check_tcb(td: &AttestedTd, trust: &TrustPolicy) -> Result { + let accepted = match td.tcb_status { + TcbStatus::UpToDate => AcceptedTcb::UpToDate, + TcbStatus::SWHardeningNeeded => AcceptedTcb::SWHardeningNeeded, + TcbStatus::ConfigurationNeeded => AcceptedTcb::ConfigurationNeeded, + TcbStatus::ConfigurationAndSWHardeningNeeded => { + AcceptedTcb::ConfigurationAndSWHardeningNeeded + } + TcbStatus::OutOfDate + | TcbStatus::OutOfDateConfigurationNeeded + | TcbStatus::TDRelaunchAdvised + | TcbStatus::TDRelaunchAdvisedConfigurationNeeded + | TcbStatus::Revoked => bail!( + "TDX TCB status is {} (advisories {:?}); this class is never accepted", + td.tcb_status, + td.advisory_ids + ), + }; + ensure!( + trust.allowed_tcb_status.contains(&accepted), + "TDX TCB status is {} (advisories {:?}), not one of {:?}", + td.tcb_status, + td.advisory_ids, + trust.allowed_tcb_status + ); + let unpinned: Vec<&String> = td + .advisory_ids + .iter() + .filter(|id| !trust.allowed_advisories.contains(id)) + .collect(); + ensure!( + unpinned.is_empty(), + "TDX TCB status {} carries advisories {unpinned:?} that the trust policy does not pin", + td.tcb_status + ); + Ok(format!( + "TCB status {} with advisories {:?}", + td.tcb_status, td.advisory_ids + )) +} + +/// Fetch collateral for `quote` from a PCCS (default: Phala's). Intel PCS +/// (`https://api.trustedservices.intel.com`) also works. pub async fn fetch_collateral(quote: &[u8], pccs_url: &str) -> Result { - dcap_qvl::collateral::CollateralClient::with_default_http(pccs_url) - .map_err(|e| anyhow!("PCCS client: {e}"))? - .fetch(quote) + let fetch = async { + dcap_qvl::collateral::CollateralClient::with_default_http(pccs_url) + .map_err(|e| anyhow!("PCCS client: {e}"))? + .fetch(quote) + .await + .map_err(|e| anyhow!("fetching DCAP collateral from {pccs_url}: {e}")) + }; + tokio::time::timeout(Duration::from_secs(60), fetch) .await - .map_err(|e| anyhow!("fetching DCAP collateral from {pccs_url}: {e}")) + .map_err(|_| anyhow!("fetching DCAP collateral from {pccs_url} timed out"))? +} + +/// The issue/next-update window of the TCB info used, for the holder's record. +pub fn collateral_dates(collateral: &dcap_qvl::QuoteCollateralV3) -> String { + let info: Result = serde_json::from_str(&collateral.tcb_info); + match info { + Ok(i) => format!( + "TCB info issued {} (next update {}, evaluation data #{})", + i.issue_date, i.next_update, i.tcb_evaluation_data_number + ), + Err(_) => "TCB info dates unavailable".into(), + } +} + +/// What the node answered must describe an attested TEE before anything +/// else is checked: `mode` dstack, a non-empty hex quote. +pub fn require_attested(info: &RecoveryInfo) -> Result> { + ensure!( + info.mode == "dstack", + "the node is not in a TEE (mode {:?}); a share is only released to an attested TEE", + info.mode + ); + let quote = hex::decode(info.quote.as_deref().unwrap_or_default()) + .context("the node's quote is not hex")?; + ensure!(!quote.is_empty(), "the node sent no TDX quote"); + Ok(quote) +} + +/// `compose-hash` pins container images only if the compose file names each +/// image by digest. Check `app-compose.json` against the trust policy's +/// `compose_hash`, and that every `image:` line carries `@sha256:`. +pub fn check_app_compose(app_compose: &[u8], trust: &TrustPolicy) -> Result> { + let digest = hex::encode(Sha256::digest(app_compose)); + ensure!( + digest == trust.compose_hash, + "app-compose.json hashes to {digest}, but the trust policy pins compose_hash {}", + trust.compose_hash + ); + let doc: serde_json::Value = + serde_json::from_slice(app_compose).context("app-compose.json is not JSON")?; + let compose = doc + .get("docker_compose_file") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("app-compose.json has no docker_compose_file"))?; + let mut images = Vec::new(); + for line in compose.lines() { + let trimmed = line.trim_start().trim_start_matches("- "); + let Some(rest) = trimmed.strip_prefix("image:") else { + continue; + }; + let image = rest + .trim() + .trim_matches(|c| c == '"' || c == '\'') + .to_owned(); + let pinned = image + .rsplit_once("@sha256:") + .is_some_and(|(_, d)| d.len() == 64 && d.bytes().all(|b| b.is_ascii_hexdigit())); + ensure!( + pinned, + "image {image:?} is not referenced by @sha256: digest" + ); + images.push(image); + } + ensure!(!images.is_empty(), "the compose file names no images"); + Ok(images) } const RUNTIME_EVENT_TYPE: u32 = 0x0800_0001; @@ -238,16 +390,15 @@ fn require_boot_event(events: &[Event], name: &str, expected: &str) -> Result<() /// Smallest nonce accepted for freshness: 128 bits as base64url. pub const MIN_NONCE_LEN: usize = 22; -/// Steps 2-5, plus agreement between the node's answer and the bundle. -/// `nonce` must be the one this holder generated for this request. -pub fn check_recovery( +/// Checks that need no network or quote, run first so a stale bundle or a +/// bad nonce is reported as such: the node's answer names the holder's +/// bundle, the nonce is fresh enough, and the node's binding is the one the +/// holder recomputes. Returns the binding digest the quote must carry. +pub fn check_local( info: &RecoveryInfo, bundle: &EscrowBundle, nonce: &str, - trust: &TrustPolicy, - td: &AttestedTd, -) -> Result<()> { - trust.validate()?; +) -> Result { bundle.validate()?; validate_nonce(nonce)?; ensure!( @@ -257,25 +408,32 @@ pub fn check_recovery( let efp = bundle.fingerprint(); ensure!( info.efp == efp && info.ark_id == bundle.ark_id && info.generation == bundle.generation, - "the node's escrow ({}) is not the bundle you hold ({efp})", + "the node's escrow ({}) is not the bundle you hold ({efp}): your bundle is stale or for another node; get the current one", info.efp ); - - // Step 5 first on the cheap side: the node's own binding must be the one - // we expect, for our nonce. A reply made for another nonce is stale. + // A reply made for another nonce is stale or replayed. let expected = recovery_binding(&info.recovery_recipient, &efp, nonce); ensure!( info.binding == expected, "the node's binding is not for your nonce: stale or replayed response; request a new one" ); let digest = binding_digest(&expected); - ensure!( - trust.allowed_tcb_status.contains(&td.tcb_status), - "TDX TCB status is {}, not one of {:?}", - td.tcb_status, - trust.allowed_tcb_status + info.binding_sha256 == digest, + "binding_sha256 does not match binding" ); + Ok(digest) +} + +/// Steps 2-5 on a DCAP-verified report. Returns the TCB line to show. +pub fn check_attestation( + info: &RecoveryInfo, + trust: &TrustPolicy, + td: &AttestedTd, + digest: &tinycloud_at_rest_escrow::Digest32, +) -> Result { + trust.validate()?; + let tcb = check_tcb(td, trust)?; ensure!( td.td_attributes[0] & 0x01 == 0, "the TD is a debug TD (TUD.DEBUG set); its memory is readable by the host" @@ -311,17 +469,28 @@ pub fn check_recovery( td.report_data == want_report, "the quote's REPORTDATA does not bind this recovery key and your nonce: replayed or foreign quote" ); - ensure!( - info.binding_sha256 == digest, - "binding_sha256 does not match binding" - ); - Ok(()) + Ok(tcb) +} + +/// [`check_local`] then [`check_attestation`]. +#[cfg(test)] +fn check_recovery( + info: &RecoveryInfo, + bundle: &EscrowBundle, + nonce: &str, + trust: &TrustPolicy, + td: &AttestedTd, +) -> Result { + let digest = check_local(info, bundle, nonce)?; + check_attestation(info, trust, td, &digest) } #[cfg(test)] mod tests { use super::*; - use tinycloud_at_rest_escrow::{Ark, ArkId, Digest32, HolderRecipient, RecoveryIdentity}; + use tinycloud_at_rest_escrow::{ + Ark, ArkId, Digest32, HolderRecipient, RecipientPolicy, RecoveryIdentity, + }; const TDX_QUOTE: &[u8] = include_bytes!("../tests/fixtures/tdx_quote"); const TDX_COLLATERAL: &[u8] = include_bytes!("../tests/fixtures/tdx_quote_collateral.json"); @@ -345,7 +514,14 @@ mod tests { recipient: RecoveryIdentity::generate().recipient(), }) .collect(); - EscrowBundle::create(&Ark::generate(), ArkId::generate(), 1, &holders).unwrap() + EscrowBundle::create( + &Ark::generate(), + ArkId::generate(), + 1, + &holders, + RecipientPolicy::AllowSoftware, + ) + .unwrap() } fn event(name: &str, payload: &str) -> serde_json::Value { @@ -355,17 +531,24 @@ mod tests { }) } - fn event_log(app_id: &str, compose: &str) -> String { - serde_json::json!([ - {"imr": 0, "event_type": 1, "digest": "00", "event": "", "event_payload": ""}, + fn boot_events(app_id: &str, compose: &str) -> Vec { + vec![ + serde_json::json!({"imr": 0, "event_type": 1, "digest": "00", "event": "", "event_payload": ""}), event("system-preparing", ""), event("app-id", app_id), event("compose-hash", compose), event("instance-id", "33"), event("boot-mr-done", ""), event("system-ready", ""), - ]) - .to_string() + ] + } + + fn event_log(app_id: &str, compose: &str) -> String { + serde_json::Value::Array(boot_events(app_id, compose)).to_string() + } + + fn rtmr3_of(log: &str) -> [u8; 48] { + replay_rtmr3(&serde_json::from_str::>(log).unwrap()).unwrap() } /// A consistent, passing scenario built on synthetic measurements. @@ -382,19 +565,14 @@ mod tests { let binding = recovery_binding(&tee.recipient(), &bundle.fingerprint(), NONCE); let digest = binding_digest(&binding); let log = event_log(APP_ID, COMPOSE); - let events: Vec = serde_json::from_str(&log).unwrap(); let mut report_data = [0u8; 64]; report_data[..32].copy_from_slice(&digest.0); let td = AttestedTd { - tcb_status: "UpToDate".into(), + tcb_status: TcbStatus::UpToDate, + advisory_ids: vec![], td_attributes: [0; 8], mr_td: [0xa0; 48], - rtmr: [ - [0xa1; 48], - [0xa2; 48], - [0xa3; 48], - replay_rtmr3(&events).unwrap(), - ], + rtmr: [[0xa1; 48], [0xa2; 48], [0xa3; 48], rtmr3_of(&log)], report_data, }; let trust = TrustPolicy { @@ -404,9 +582,11 @@ mod tests { rtmr0: hex::encode([0xa1; 48]), rtmr1: hex::encode([0xa2; 48]), rtmr2: hex::encode([0xa3; 48]), - allowed_tcb_status: default_tcb_status(), + allowed_tcb_status: default_tcb(), + allowed_advisories: vec![], }; let info = RecoveryInfo { + version: tinycloud_at_rest_escrow::RECOVERY_INFO_VERSION, mode: "dstack".into(), state: "unlocked".into(), ark_id: bundle.ark_id, @@ -416,7 +596,7 @@ mod tests { recovery_recipient: tee.recipient(), binding: binding.clone(), binding_sha256: digest, - quote: Some(String::new()), + quote: Some("00".into()), event_log: Some(log), shares_held: vec![], }; @@ -428,7 +608,24 @@ mod tests { } } - fn check(s: &Scenario, nonce: &str) -> Result<()> { + impl Scenario { + /// Replace the event log and keep RTMR3 consistent with it, as a TEE + /// that really measured those events would. + fn with_events(mut self, events: Vec) -> Self { + let log = serde_json::Value::Array(events).to_string(); + self.td.rtmr[3] = rtmr3_of(&log); + self.info.event_log = Some(log); + self + } + + /// Replace the event log with one that doesn't replay (RTMR3 unchanged). + fn with_events_unchecked(mut self, events: Vec) -> Self { + self.info.event_log = Some(serde_json::Value::Array(events).to_string()); + self + } + } + + fn check(s: &Scenario, nonce: &str) -> Result { check_recovery(&s.info, &s.bundle, nonce, &s.trust, &s.td) } @@ -439,7 +636,8 @@ mod tests { #[test] fn consistent_attestation_passes() { - check(&scenario(), NONCE).unwrap(); + let line = check(&scenario(), NONCE).unwrap(); + assert!(line.contains("UpToDate"), "{line}"); } #[test] @@ -468,12 +666,8 @@ mod tests { #[test] fn wrong_measurements_are_refused() { - let base = scenario(); let mut s = scenario(); - s.td = base.td.clone(); s.td.mr_td[0] ^= 1; - s.info = base.info.clone(); - s.bundle = base.bundle.clone(); assert_refused(&s, NONCE, "MRTD"); for (i, label) in [(0usize, "RTMR0"), (1, "RTMR1"), (2, "RTMR2")] { @@ -487,35 +681,96 @@ mod tests { assert_refused(&s, NONCE, "replay to the quoted RTMR3"); // Another app (its own consistent log and RTMR3) and another compose. - for (app, compose, needle) in [ - ( - "9999999999999999999999999999999999999999", - COMPOSE, - "app-id", - ), - ( - APP_ID, - "9999999999999999999999999999999999999999999999999999999999999999", - "compose-hash", - ), - ] { - let mut s = scenario(); - let log = event_log(app, compose); - s.td.rtmr[3] = - replay_rtmr3(&serde_json::from_str::>(&log).unwrap()).unwrap(); - s.info.event_log = Some(log); - assert_refused(&s, NONCE, needle); + let other = "9999999999999999999999999999999999999999"; + let s = scenario().with_events(boot_events(other, COMPOSE)); + assert_refused(&s, NONCE, "app-id"); + let s = scenario().with_events(boot_events(APP_ID, &"9".repeat(64))); + assert_refused(&s, NONCE, "compose-hash"); + } + + #[test] + fn identity_events_after_boot_or_duplicated_are_refused() { + // A compose-hash (or app-id) measured after boot-mr-done is something + // the app itself could have extended: refused even if it matches. + for name in ["compose-hash", "app-id"] { + let mut events = boot_events(APP_ID, COMPOSE); + let at = events.iter().position(|e| e["event"] == name).unwrap(); + let moved = events.remove(at); + events.push(moved); + assert_refused( + &scenario().with_events(events), + NONCE, + "before boot completion", + ); } + // Duplicated identity events, even with equal payloads. + for (name, payload) in [("app-id", APP_ID), ("compose-hash", COMPOSE)] { + let mut events = boot_events(APP_ID, COMPOSE); + events.insert(2, event(name, payload)); + assert_refused(&scenario().with_events(events), NONCE, "exactly one"); + } + // Missing, duplicated or reordered boot markers. + let mut events = boot_events(APP_ID, COMPOSE); + events.retain(|e| e["event"] != "boot-mr-done"); + assert_refused(&scenario().with_events(events), NONCE, "boot markers"); + let mut events = boot_events(APP_ID, COMPOSE); + events.push(event("boot-mr-done", "")); + assert_refused(&scenario().with_events(events), NONCE, "boot markers"); + let mut events = boot_events(APP_ID, COMPOSE); + let ready = events + .iter() + .position(|e| e["event"] == "system-ready") + .unwrap(); + let ready = events.remove(ready); + events.insert(1, ready); + assert_refused(&scenario().with_events(events), NONCE, "ordering"); + } + + fn v2_event(name: &str, payload_hex: &str, preimage: Option) -> serde_json::Value { + let canonical = serde_json::json!({ + "name": name, "payload": payload_hex, "type": RUNTIME_EVENT_TYPE, + }) + .to_string(); + serde_json::json!({ + "imr": 3, "event_type": RUNTIME_EVENT_TYPE, "digest": "", "version": 2, + "event": name, "event_payload": payload_hex, + "preimage": preimage.unwrap_or_else(|| hex::encode(canonical)), + }) + } + + #[test] + fn v2_events_replay_and_a_non_canonical_preimage_is_refused() { + let mut events = boot_events(APP_ID, COMPOSE); + events[3] = v2_event("compose-hash", COMPOSE, None); + check(&scenario().with_events(events.clone()), NONCE).unwrap(); + + // Same digest input, different bytes: a reordered-key preimage. + let reordered = serde_json::json!({"type": RUNTIME_EVENT_TYPE, "name": "compose-hash", "payload": COMPOSE}); + events[3] = v2_event( + "compose-hash", + COMPOSE, + Some(hex::encode(reordered.to_string())), + ); + let log = serde_json::Value::Array(events.clone()).to_string(); + let err = replay_rtmr3(&serde_json::from_str::>(&log).unwrap()).unwrap_err(); + assert!(err.to_string().contains("not canonical"), "{err}"); + + // A v2 event with no preimage, and an unknown version. + let mut no_pre = v2_event("compose-hash", COMPOSE, None); + no_pre.as_object_mut().unwrap().remove("preimage"); + events[3] = no_pre; + let log = serde_json::Value::Array(events.clone()).to_string(); + assert!(replay_rtmr3(&serde_json::from_str::>(&log).unwrap()).is_err()); + events[3]["version"] = serde_json::json!(3); + let log = serde_json::Value::Array(events).to_string(); + assert!(replay_rtmr3(&serde_json::from_str::>(&log).unwrap()).is_err()); } #[test] - fn debug_td_and_bad_tcb_status_are_refused() { + fn debug_td_is_refused() { let mut s = scenario(); s.td.td_attributes[0] |= 1; assert_refused(&s, NONCE, "debug TD"); - let mut s = scenario(); - s.td.tcb_status = "OutOfDate".into(); - assert_refused(&s, NONCE, "TCB status"); } #[test] @@ -535,6 +790,81 @@ mod tests { assert_refused(&s, NONCE, "digest does not bind"); } + #[test] + fn malformed_event_logs_are_refused() { + let mut s = scenario(); + s.info.event_log = None; + assert_refused(&s, NONCE, "no event log"); + let mut s = scenario(); + s.info.event_log = Some("{not json".into()); + assert_refused(&s, NONCE, "not a JSON array"); + let mut s = scenario(); + s.info.event_log = Some("[]".into()); + assert_refused(&s, NONCE, "no RTMR3 events"); + + let mut events = boot_events(APP_ID, COMPOSE); + events[1]["imr"] = 4.into(); + assert_refused( + &scenario().with_events_unchecked(events), + NONCE, + "invalid IMR", + ); + let mut events = boot_events(APP_ID, COMPOSE); + events[2]["event_type"] = 1.into(); + assert_refused( + &scenario().with_events_unchecked(events), + NONCE, + "unsupported type", + ); + let mut events = boot_events(APP_ID, COMPOSE); + events[2]["event_payload"] = "xyz".into(); + assert_refused(&scenario().with_events_unchecked(events), NONCE, "not hex"); + } + + #[test] + fn binding_digest_must_match_the_binding() { + let mut s = scenario(); + s.info.binding_sha256 = Digest32([3; 32]); + assert_refused(&s, NONCE, "binding_sha256"); + } + + #[test] + fn local_problems_are_named_before_any_network_or_quote_check() { + // A quote that would fail every attestation check: the local cause wins. + let mut s = scenario(); + s.td.tcb_status = TcbStatus::Revoked; + s.td.mr_td = [0; 48]; + s.info.efp = Digest32([7; 32]); + let err = check_local(&s.info, &s.bundle, NONCE) + .unwrap_err() + .to_string(); + assert!(err.contains("stale"), "{err}"); + let s = scenario(); + let err = check_local(&s.info, &s.bundle, "short") + .unwrap_err() + .to_string(); + assert!(err.contains("at least 22"), "{err}"); + } + + #[test] + fn only_an_attested_tee_answer_is_considered() { + let mut s = scenario(); + assert!(require_attested(&s.info).is_ok()); + s.info.mode = "classic".into(); + assert!(require_attested(&s.info) + .unwrap_err() + .to_string() + .contains("not in a TEE")); + let mut s = scenario(); + s.info.quote = None; + assert!(require_attested(&s.info) + .unwrap_err() + .to_string() + .contains("no TDX quote")); + s.info.quote = Some("zz".into()); + assert!(require_attested(&s.info).is_err()); + } + fn collateral(bytes: &[u8]) -> dcap_qvl::QuoteCollateralV3 { serde_json::from_slice(bytes).unwrap() } @@ -549,13 +879,124 @@ mod tests { s.trust.rtmr0 = hex::encode(td.rtmr[0]); s.trust.rtmr1 = hex::encode(td.rtmr[1]); s.trust.rtmr2 = hex::encode(td.rtmr[2]); - s.trust.allowed_tcb_status = vec![td.tcb_status.clone()]; + s.trust.allowed_tcb_status = vec![ + AcceptedTcb::UpToDate, + AcceptedTcb::SWHardeningNeeded, + AcceptedTcb::ConfigurationNeeded, + AcceptedTcb::ConfigurationAndSWHardeningNeeded, + ]; + s.trust.allowed_advisories = td.advisory_ids.clone(); let log_rtmr3 = s.td.rtmr[3]; s.td = td; s.td.rtmr[3] = log_rtmr3; assert_refused(&s, NONCE, "REPORTDATA does not bind"); } + /// The genuine fixture quote and collateral, with every TCB level of the + /// (otherwise genuine) TCB info forced to `status` and `advisories`. + fn real_quote_with_status(status: TcbStatus, advisories: &[&str]) -> Result { + let advisories: Vec = advisories.iter().map(|s| s.to_string()).collect(); + let verified = dcap_qvl::verify::QuoteVerifier::new_prod() + .dangerous_verify_with_tcb_override( + TDX_QUOTE, + &collateral(TDX_COLLATERAL), + FIXTURE_NOW, + |mut info| { + for level in &mut info.tcb_levels { + level.tcb_status = status; + level.advisory_ids = advisories.clone(); + } + info + }, + ) + .map_err(|e| anyhow!("DCAP: {e}"))?; + AttestedTd::from_report(&verified) + } + + #[test] + fn every_tcb_class_from_a_real_quote() { + const SA: &str = "INTEL-SA-00837"; + let default = scenario().trust; + let mut lenient = default.clone(); + lenient.allowed_tcb_status = vec![ + AcceptedTcb::UpToDate, + AcceptedTcb::SWHardeningNeeded, + AcceptedTcb::ConfigurationNeeded, + AcceptedTcb::ConfigurationAndSWHardeningNeeded, + ]; + let mut pinned = lenient.clone(); + pinned.allowed_advisories = vec![SA.into()]; + + let up = real_quote_with_status(TcbStatus::UpToDate, &[]).unwrap(); + assert_eq!(up.tcb_status, TcbStatus::UpToDate); + check_tcb(&up, &default).unwrap(); + + for status in [ + TcbStatus::SWHardeningNeeded, + TcbStatus::ConfigurationNeeded, + TcbStatus::ConfigurationAndSWHardeningNeeded, + ] { + let td = real_quote_with_status(status, &[SA]).unwrap(); + assert_eq!(td.tcb_status, status); + let err = check_tcb(&td, &default).unwrap_err().to_string(); + assert!(err.contains("not one of"), "{status}: {err}"); + let err = check_tcb(&td, &lenient).unwrap_err().to_string(); + assert!(err.contains("does not pin"), "{status}: {err}"); + let ok = check_tcb(&td, &pinned).unwrap(); + assert!(ok.contains(SA), "{ok}"); + } + + // Never accepted, whatever the policy says. + for status in [ + TcbStatus::OutOfDate, + TcbStatus::OutOfDateConfigurationNeeded, + TcbStatus::TDRelaunchAdvised, + TcbStatus::TDRelaunchAdvisedConfigurationNeeded, + TcbStatus::Revoked, + ] { + match real_quote_with_status(status, &[SA]) { + // DCAP itself rejects some classes (e.g. Revoked). + Err(e) => assert!(e.to_string().contains("DCAP"), "{status}: {e}"), + Ok(td) => { + let err = check_tcb(&td, &pinned).unwrap_err().to_string(); + assert!(err.contains("never accepted"), "{status}: {err}"); + } + } + } + + // An unknown status string from the verifier is refused. + let mut report = dcap_qvl::verify::QuoteVerifier::new_prod() + .dangerous_verify_with_tcb_override( + TDX_QUOTE, + &collateral(TDX_COLLATERAL), + FIXTURE_NOW, + |info| info, + ) + .unwrap(); + report.status = "MostlyFine".into(); + assert!(AttestedTd::from_report(&report) + .unwrap_err() + .to_string() + .contains("unknown TCB status")); + } + + #[test] + fn trust_policy_cannot_name_refused_classes() { + let base = r#""app_id":"1111111111111111111111111111111111111111","compose_hash":"22","mrtd":"a","rtmr0":"a","rtmr1":"a","rtmr2":"a""#; + for status in ["OutOfDate", "Revoked", "TDRelaunchAdvised", "NotAStatus"] { + let json = format!(r#"{{{base},"allowed_tcb_status":["{status}"]}}"#); + assert!( + serde_json::from_str::(&json).is_err(), + "{status}" + ); + } + let json = format!(r#"{{{base},"allowed_tcb_status":["SWHardeningNeeded"]}}"#); + assert!(serde_json::from_str::(&json).is_ok()); + let mut t = scenario().trust; + t.allowed_advisories = vec!["CVE-2024-1".into()]; + assert!(t.validate().is_err()); + } + #[test] fn forged_quote_fails_dcap() { // REPORTDATA, MRTD, and the ECDSA signature (after the 48-byte header, @@ -580,15 +1021,14 @@ mod tests { } #[test] - fn outdated_platform_quote_is_refused() { - // A real quote from a platform whose TCB level the collateral no - // longer lists. DCAP refuses it outright. + fn platform_missing_from_the_tcb_info_is_refused() { + // A real quote whose platform TCB level the collateral doesn't list. let err = verify_quote( TDX_QUOTE_OUTDATED, &collateral(TDX_COLLATERAL_OUTDATED), FIXTURE_NOW_OUTDATED, ) - .expect_err("outdated platform must be refused"); + .expect_err("unlisted platform must be refused"); assert!(err.to_string().contains("DCAP"), "{err}"); } @@ -602,4 +1042,36 @@ mod tests { assert!(t.validate().is_err()); assert!(serde_json::from_str::(r#"{"app_id":"x","extra":1}"#).is_err()); } + + fn app_compose(compose_yaml: &str) -> Vec { + serde_json::to_vec(&serde_json::json!({ + "manifest_version": 2, + "name": "tinycloud", + "runner": "docker-compose", + "docker_compose_file": compose_yaml, + })) + .unwrap() + } + + #[test] + fn app_compose_must_match_the_pin_and_reference_images_by_digest() { + let digest = "a".repeat(64); + let good = app_compose(&format!( + "services:\n node:\n image: ghcr.io/tinycloudlabs/tinycloud-node@sha256:{digest}\n ingress:\n image: \"caddy@sha256:{digest}\"\n" + )); + let mut trust = scenario().trust; + trust.compose_hash = hex::encode(Sha256::digest(&good)); + assert_eq!(check_app_compose(&good, &trust).unwrap().len(), 2); + + let tagged = app_compose(&format!( + "services:\n node:\n image: ghcr.io/tinycloudlabs/tinycloud-node@sha256:{digest}\n shipper:\n image: vector:0.40\n" + )); + trust.compose_hash = hex::encode(Sha256::digest(&tagged)); + let err = check_app_compose(&tagged, &trust).unwrap_err().to_string(); + assert!(err.contains("vector:0.40"), "{err}"); + + trust.compose_hash = "0".repeat(64); + let err = check_app_compose(&good, &trust).unwrap_err().to_string(); + assert!(err.contains("trust policy pins compose_hash"), "{err}"); + } } diff --git a/tinycloud-at-rest-cli/src/main.rs b/tinycloud-at-rest-cli/src/main.rs index d18720a8..5d218a01 100644 --- a/tinycloud-at-rest-cli/src/main.rs +++ b/tinycloud-at-rest-cli/src/main.rs @@ -2,23 +2,44 @@ //! //! Secrets are never accepted on argv. A decrypted share is read from stdin //! only (pipe it from `age -d`), and nothing here writes one to disk. +//! +//! Exit codes: 0 OK; 1 REFUSED (a check failed: don't release, don't pin); +//! 2 usage error; 3 ERROR (a file, stdin or network input couldn't be read). + +#![forbid(unsafe_code)] mod attest; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Result}; use base64::Engine; use clap::{Args, Parser, Subcommand}; use tinycloud_at_rest_escrow::{ - validate_holders, wrap_share, EscrowBundle, HolderRecipient, RecipientKind, RecoveryInfo, - VerificationRecord, + validate_holders, wrap_share, Ark, ArkId, EscrowBundle, HolderRecipient, RecipientKind, + RecipientPolicy, RecoveryInfo, VerificationRecord, }; use zeroize::Zeroizing; use attest::TrustPolicy; +/// An input couldn't be read at all (as opposed to a check that failed). +#[derive(Debug)] +struct InputError(String); + +impl std::fmt::Display for InputError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for InputError {} + +fn input_err(msg: String) -> anyhow::Error { + InputError(msg).into() +} + #[derive(Parser)] #[command( name = "tinycloud-at-rest", @@ -29,48 +50,99 @@ struct Cli { command: Command, } +/// Software (X25519) holders are refused unless this is given. +#[derive(Args)] +struct SoftwareFlag { + /// Accept X25519 software-key holders. Tests and self-hosted nodes only; + /// every command warns loudly while it is in effect. + #[arg(long)] + allow_software: bool, +} + +impl SoftwareFlag { + fn policy(&self) -> RecipientPolicy { + if self.allow_software { + RecipientPolicy::AllowSoftware + } else { + RecipientPolicy::HardwareOnly + } + } +} + +/// The bundle a command works on. +#[derive(Args)] +struct BundleArgs { + /// The escrow bundle you were given. + #[arg(long)] + bundle: PathBuf, + /// Refuse unless the bundle's EFP equals this (from Sam, out of band). + #[arg(long)] + expect_efp: Option, + #[command(flatten)] + software: SoftwareFlag, +} + #[derive(Subcommand)] enum Command { /// Validate the three holder recipients and print the node config for them. Recipients { - /// `NAME=RECIPIENT`, three times (e.g. `sam=age1tag1…`). + /// `NAME=RECIPIENT`, three times (e.g. `sam=age1yubikey1…`). #[arg(long = "holder", required = true)] holders: Vec, + #[command(flatten)] + software: SoftwareFlag, }, - /// Print a fresh 256-bit nonce for a recovery request. - Nonce, - /// Validate a bundle and print its fingerprint, canary and holders. - Inspect { + /// Ceremony step 0: seal a throwaway share to RECIPIENT with the exact + /// code the TEE uses, so a holder can prove their YubiKey and age tooling + /// decrypt it. Writes selftest-bundle.json and selftest-share.age to --out. + SelfTest { #[arg(long)] - bundle: PathBuf, - /// Fail unless the bundle's EFP equals this value. + recipient: String, + #[arg(long, default_value = ".")] + out: PathBuf, + #[command(flatten)] + software: SoftwareFlag, + }, + /// Print the compressed P-256 public key (hex) inside an age1yubikey1 or + /// age1tag1 recipient, to compare with a YubiKey attestation certificate. + RecipientKey { #[arg(long)] - expect_efp: Option, + recipient: String, }, + /// Print a fresh 256-bit nonce for a recovery request. + Nonce, + /// Validate a bundle and print its fingerprint, canary and holders. + Inspect(BundleArgs), /// Print one holder's sealed share (an armored age file) to stdout. ExtractShare { - #[arg(long)] - bundle: PathBuf, + #[command(flatten)] + bundle: BundleArgs, #[arg(long)] holder: String, }, /// Offline check: read a decrypted share on stdin (from `age -d`) and /// check it against the bundle's commitment. Prints no share bytes. - CheckShare { + CheckShare(BundleArgs), + /// Check a deployment's app-compose.json against the trust policy's + /// compose_hash and require every image to be pinned by @sha256: digest. + VerifyCompose { + #[arg(long)] + app_compose: PathBuf, #[arg(long)] - bundle: PathBuf, + trust: PathBuf, }, - /// Verify a node's recovery key: DCAP quote, measurements, event log, - /// and the binding to your nonce. Releases nothing. + /// Verify a node's recovery key: DCAP quote, TCB status, measurements, + /// event log, and the binding to your nonce. Releases nothing. VerifyNode(NodeArgs), /// Verify the node exactly as `verify-node` does, then read a decrypted /// share on stdin, check it, and print it sealed to the node's recovery key. ShareWrap(NodeArgs), - /// Check that a verification record covers the bundle and print the value - /// to pin as `at_rest.escrow.verified_fp`. - VerifyRecord { - #[arg(long)] - bundle: PathBuf, + /// Print the EFP a ceremony record names, after checking that it names + /// this bundle and claims all three shares. Its authenticity (MAC) can't + /// be checked offline; the node checks it before enabling encryption. + RecordEfp { + #[command(flatten)] + bundle: BundleArgs, #[arg(long)] record: PathBuf, }, @@ -78,36 +150,65 @@ enum Command { #[derive(Args)] struct NodeArgs { - /// The escrow bundle you were given (not the node's copy). - #[arg(long)] - bundle: PathBuf, + #[command(flatten)] + bundle: BundleArgs, /// The node's `GET /at-rest/recovery?nonce=…` response. #[arg(long)] recovery: PathBuf, /// The nonce you generated for that request. #[arg(long)] nonce: String, - /// Trusted measurements (JSON; see ESCROW-CEREMONY.md). + /// Trusted measurements (JSON; see docs/at-rest-escrow.md). #[arg(long)] trust: PathBuf, /// DCAP collateral JSON for offline verification. Without it, collateral /// is fetched from `--pccs-url`. #[arg(long)] collateral: Option, + /// PCCS to fetch collateral from (Phala's, or Intel PCS + /// `https://api.trustedservices.intel.com`). #[arg(long, default_value = dcap_qvl::collateral::PHALA_PCCS_URL)] pccs_url: String, } +fn read_file(path: &Path, what: &str) -> Result> { + std::fs::read(path).map_err(|e| input_err(format!("reading {what} {}: {e}", path.display()))) +} + fn read_json(path: &Path, what: &str) -> Result { - let bytes = - std::fs::read(path).with_context(|| format!("reading {what} {}", path.display()))?; - serde_json::from_slice(&bytes).with_context(|| format!("parsing {what} {}", path.display())) + let bytes = read_file(path, what)?; + serde_json::from_slice(&bytes) + .map_err(|e| input_err(format!("parsing {what} {}: {e}", path.display()))) } -fn read_bundle(path: &Path) -> Result { - let bundle: EscrowBundle = read_json(path, "bundle")?; - bundle.validate()?; - Ok(bundle) +fn warn_software(holders: &[HolderRecipient], kinds: &[RecipientKind]) { + for (h, kind) in holders.iter().zip(kinds) { + if kind.is_software() { + eprintln!( + "!!! WARNING: holder {:?} uses an X25519 SOFTWARE key, not a hardware key. \ + Accepted only because --allow-software was given (tests and self-host only). !!!", + h.holder + ); + } + } +} + +/// Read and validate a bundle, apply the recipient policy and `--expect-efp`. +fn read_bundle(args: &BundleArgs) -> Result<(EscrowBundle, Vec)> { + let bytes = read_file(&args.bundle, "bundle")?; + let bundle = EscrowBundle::from_json(&bytes)?; + let kinds = bundle.check_recipients(args.software.policy())?; + warn_software(&bundle.holders(), &kinds); + if let Some(expected) = &args.expect_efp { + let efp = bundle.fingerprint().to_string(); + if !expected.trim().eq_ignore_ascii_case(&efp) { + bail!( + "bundle {} has EFP {efp}, not the expected {expected}: it is stale or not the one Sam pinned", + args.bundle.display() + ); + } + } + Ok((bundle, kinds)) } /// A decrypted share from stdin, bounded and zeroized. @@ -117,78 +218,92 @@ fn read_share_stdin(bundle: &EscrowBundle) -> Result `"); + return Err(input_err( + "no share on stdin: did `age -d` fail (wrong identity, YubiKey not inserted or not touched, wrong file)? \ + Pipe it as `age -d -i | tinycloud-at-rest …`" + .into(), + )); } Ok(bundle.open_share(&buf)?) } async fn verify_node(args: &NodeArgs) -> Result<(EscrowBundle, RecoveryInfo)> { - let bundle = read_bundle(&args.bundle)?; - let info: RecoveryInfo = read_json(&args.recovery, "recovery response")?; + let (bundle, _) = read_bundle(&args.bundle)?; + let info = RecoveryInfo::from_json(&read_file(&args.recovery, "recovery response")?)?; let trust: TrustPolicy = read_json(&args.trust, "trust policy")?; trust.validate()?; - if info.mode != "dstack" { - bail!( - "the node is not in a TEE (mode {:?}); a share is only released to an attested TEE", - info.mode - ); - } - let quote = - hex::decode(info.quote.as_deref().unwrap_or_default()).context("quote is not hex")?; - if quote.is_empty() { - bail!("the node sent no TDX quote"); - } + // Everything that needs no network first, so its cause is what's reported. + let quote = attest::require_attested(&info)?; + let digest = attest::check_local(&info, &bundle, &args.nonce)?; let collateral = match &args.collateral { Some(path) => read_json(path, "collateral")?, - None => attest::fetch_collateral("e, &args.pccs_url).await?, + None => attest::fetch_collateral("e, &args.pccs_url) + .await + .map_err(|e| input_err(format!("{e:#}")))?, }; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs(); let td = attest::verify_quote("e, &collateral, now)?; - attest::check_recovery(&info, &bundle, &args.nonce, &trust, &td)?; + let tcb = attest::check_attestation(&info, &trust, &td, &digest)?; + eprintln!("OK: genuine TDX quote; {tcb}"); + eprintln!(" {}", attest::collateral_dates(&collateral)); eprintln!( - "OK: genuine TDX quote ({}), measurements match the trust policy, recovery key {} bound to your nonce and escrow {}", - td.tcb_status, info.recovery_recipient, info.efp + " MRTD, RTMR0-2, app-id and compose-hash match the trust policy; recovery key {} is bound to your nonce and escrow {}", + info.recovery_recipient, info.efp + ); + eprintln!( + " node state: {} ({}); shares it holds this boot: {:?}", + info.state, + if info.state == "locked" { + "recovery: two shares unlock it" + } else { + "ceremony test-unlock: all three shares are checked" + }, + info.shares_held ); Ok((bundle, info)) } -fn print_holders(bundle: &EscrowBundle) { - for s in &bundle.shares { +fn print_holders(bundle: &EscrowBundle, kinds: &[RecipientKind]) { + for (s, kind) in bundle.shares.iter().zip(kinds) { println!( - "share {} holder {:<10} recipient {}", - s.index, s.holder, s.recipient + "share {} holder {:<10} {:<8} recipient {}", + s.index, + s.holder, + match kind { + RecipientKind::PivP256 => "piv-p256", + RecipientKind::P256Tag => "p256tag", + RecipientKind::X25519 => "X25519!", + }, + s.recipient ); } } async fn run(cli: Cli) -> Result<()> { match cli.command { - Command::Recipients { holders } => { + Command::Recipients { holders, software } => { let parsed = holders .iter() .map(|h| { - let (holder, recipient) = h - .split_once('=') - .context("each --holder must be NAME=RECIPIENT")?; + let (holder, recipient) = h.split_once('=').ok_or_else(|| { + input_err(format!("--holder {h:?} must be NAME=RECIPIENT")) + })?; Ok(HolderRecipient { holder: holder.into(), recipient: recipient.into(), }) }) .collect::>>()?; - let kinds = validate_holders(&parsed)?; - for (h, kind) in parsed.iter().zip(&kinds) { - if *kind == RecipientKind::SoftwareX25519 { - eprintln!( - "WARNING: holder {:?} uses an X25519 (software) recipient, not a hardware key", - h.holder - ); - } - } + let kinds = validate_holders(&parsed, software.policy())?; + warn_software(&parsed, &kinds); + eprintln!( + "Note: a holder name is only a label. Check each recipient really is that holder's key; \ + the ceremony test-unlock is what proves it." + ); let inline = parsed .iter() .map(|h| format!("{{holder=\"{}\",recipient=\"{}\"}}", h.holder, h.recipient)) @@ -208,6 +323,74 @@ async fn run(cli: Cli) -> Result<()> { println!("# or compose environment"); println!("TINYCLOUD_AT_REST__ESCROW__RECIPIENTS=[{inline}]"); } + Command::SelfTest { + recipient, + out, + software, + } => { + // Two throwaway co-holders: fresh P-256 keys whose private halves + // are dropped here. The ARK is random and discarded too. + let dummy = || { + use p256::elliptic_curve::sec1::ToEncodedPoint; + let pk = p256::SecretKey::random(&mut rand::rngs::OsRng).public_key(); + let hrp = bech32::Hrp::parse("age1yubikey").expect("valid HRP"); + bech32::encode::(hrp, pk.to_encoded_point(true).as_bytes()) + .expect("33-byte key encodes") + }; + let holders = vec![ + HolderRecipient { + holder: "selftest".into(), + recipient, + }, + HolderRecipient { + holder: "dummy-a".into(), + recipient: dummy(), + }, + HolderRecipient { + holder: "dummy-b".into(), + recipient: dummy(), + }, + ]; + let kinds = validate_holders(&holders, software.policy())?; + warn_software(&holders, &kinds); + let bundle = EscrowBundle::create( + &Ark::generate(), + ArkId::generate(), + 1, + &holders, + software.policy(), + )?; + let bundle_path = out.join("selftest-bundle.json"); + let share_path = out.join("selftest-share.age"); + std::fs::write(&bundle_path, serde_json::to_vec_pretty(&bundle)?) + .map_err(|e| input_err(format!("writing {}: {e}", bundle_path.display())))?; + std::fs::write(&share_path, bundle.shares[0].ciphertext.as_bytes()) + .map_err(|e| input_err(format!("writing {}: {e}", share_path.display())))?; + eprintln!( + "Wrote a THROWAWAY test escrow ({:?} kind, sealed by the TEE's code). Now run:", + kinds[0] + ); + eprintln!( + " age -d -i {} | tinycloud-at-rest check-share --bundle {}{}", + share_path.display(), + bundle_path.display(), + if software.allow_software { + " --allow-software" + } else { + "" + } + ); + eprintln!("Expect: OK: share 1 (selftest) matches escrow …"); + } + Command::RecipientKey { recipient } => { + tinycloud_at_rest_escrow::parse_recipient("recipient", &recipient)?; + let (hrp, key) = bech32::decode(&recipient) + .map_err(|e| input_err(format!("recipient is not Bech32: {e}")))?; + if !matches!(hrp.as_str(), "age1yubikey" | "age1tag") || key.len() != 33 { + bail!("{recipient} is not a P-256 (age1yubikey1/age1tag1) recipient"); + } + println!("{}", hex::encode(key)); + } Command::Nonce => { let mut bytes = [0u8; 32]; rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut bytes); @@ -216,30 +399,26 @@ async fn run(cli: Cli) -> Result<()> { base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) ); } - Command::Inspect { bundle, expect_efp } => { - let bundle = read_bundle(&bundle)?; - let efp = bundle.fingerprint(); - println!("efp {efp}"); + Command::Inspect(args) => { + let (bundle, kinds) = read_bundle(&args)?; + println!("efp {}", bundle.fingerprint()); println!("ark_id {}", bundle.ark_id); println!("generation {}", bundle.generation); println!("canary_fp {}", bundle.canary_fp); - print_holders(&bundle); - if let Some(expected) = expect_efp { - if expected != efp.to_string() { - bail!("bundle EFP {efp} is not the expected {expected}"); - } - eprintln!("OK: EFP matches"); - } + print_holders(&bundle, &kinds); } Command::ExtractShare { bundle, holder } => { - let bundle = read_bundle(&bundle)?; - let share = bundle - .share_for(&holder) - .with_context(|| format!("no holder {holder:?} in this bundle"))?; + let (bundle, _) = read_bundle(&bundle)?; + let Some(share) = bundle.share_for(&holder) else { + let names: Vec = bundle.holders().into_iter().map(|h| h.holder).collect(); + return Err(input_err(format!( + "no holder {holder:?} in this bundle (holders: {names:?})" + ))); + }; std::io::stdout().write_all(share.ciphertext.as_bytes())?; } - Command::CheckShare { bundle } => { - let bundle = read_bundle(&bundle)?; + Command::CheckShare(args) => { + let (bundle, _) = read_bundle(&args)?; let share = read_share_stdin(&bundle)?; let holder = &bundle.shares[usize::from(share.index) - 1].holder; eprintln!( @@ -249,6 +428,19 @@ async fn run(cli: Cli) -> Result<()> { bundle.ark_id ); } + Command::VerifyCompose { app_compose, trust } => { + let trust: TrustPolicy = read_json(&trust, "trust policy")?; + trust.validate()?; + let bytes = read_file(&app_compose, "app-compose.json")?; + let images = attest::check_app_compose(&bytes, &trust)?; + eprintln!( + "OK: app-compose.json matches compose_hash {}; every image is pinned by digest:", + trust.compose_hash + ); + for image in images { + eprintln!(" {image}"); + } + } Command::VerifyNode(args) => { verify_node(&args).await?; } @@ -258,17 +450,22 @@ async fn run(cli: Cli) -> Result<()> { let wrapped = wrap_share(&info.recovery_recipient, &share)?; std::io::stdout().write_all(wrapped.as_bytes())?; eprintln!( - "OK: share {} sealed to the node's recovery key; POST it to /at-rest/recovery/shares", + "OK: share {} sealed to this node's recovery key. POST it to the same node's /at-rest/recovery/shares \ + before it restarts (the key is per boot).", share.index ); } - Command::VerifyRecord { bundle, record } => { - let bundle = read_bundle(&bundle)?; - let record: VerificationRecord = read_json(&record, "verification record")?; + Command::RecordEfp { bundle, record } => { + let (bundle, _) = read_bundle(&bundle)?; + let record = VerificationRecord::from_json(&read_file(&record, "record")?)?; record.covers(&bundle)?; eprintln!( - "OK: record covers shares {:?} and pairs {:?} of escrow {} (verified {})", - record.verified_indices, record.pairs, record.efp, record.verified_at + "NOT AUTHENTICATED: this record names escrow {} and claims shares {:?} and pairs {:?} (verified_at {}),", + record.efp, record.verified_indices, record.pairs, record.verified_at + ); + eprintln!( + "but its MAC can only be checked inside the TEE. Pin this value only after you saw the node answer \ + the third share with \"verified\"; the node refuses to encrypt unless the stored record verifies." ); println!("TINYCLOUD_AT_REST__ESCROW__VERIFIED_FP={}", record.efp); } @@ -283,6 +480,10 @@ fn main() { .build() .expect("tokio runtime"); if let Err(e) = runtime.block_on(run(cli)) { + if e.downcast_ref::().is_some() { + eprintln!("ERROR: {e:#}"); + std::process::exit(3); + } eprintln!("REFUSED: {e:#}"); std::process::exit(1); } diff --git a/tinycloud-at-rest-cli/tests/age_interop.rs b/tinycloud-at-rest-cli/tests/age_interop.rs new file mode 100644 index 00000000..196ddf24 --- /dev/null +++ b/tinycloud-at-rest-cli/tests/age_interop.rs @@ -0,0 +1,195 @@ +//! Interop with the stock `age` CLI (Go reference implementation). +//! +//! Needs `AGE_BIN_DIR` pointing at a directory with `age` and `age-keygen` +//! (CI downloads a sha256-pinned age v1.3.2). Skips locally when unset; in +//! CI (`CI` set) a missing directory is a failure, never a silent pass. + +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +use age_core::format::{FileKey, Stanza}; +use base64::Engine; +use hpke::Deserializable; +use p256::elliptic_curve::sec1::ToEncodedPoint; +use tinycloud_at_rest_escrow::{Ark, ArkId, EscrowBundle, HolderRecipient, RecipientPolicy}; + +const BIN: &str = env!("CARGO_BIN_EXE_tinycloud-at-rest"); + +fn age_dir(test: &str) -> Option { + match std::env::var_os("AGE_BIN_DIR") { + Some(d) if !d.is_empty() => Some(PathBuf::from(d)), + _ => { + assert!( + std::env::var_os("CI").is_none(), + "{test} needs AGE_BIN_DIR in CI" + ); + eprintln!("skipping {test}: AGE_BIN_DIR is unset"); + None + } + } +} + +fn pipe(cmd: &mut Command, input: &[u8]) -> std::process::Output { + let mut child = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(input).unwrap(); + child.wait_with_output().unwrap() +} + +#[test] +fn stock_age_decrypts_shares_and_the_cli_accepts_them() { + let Some(age) = age_dir("stock_age_decrypts_shares_and_the_cli_accepts_them") else { + return; + }; + let dir = std::env::temp_dir().join(format!("tc887-interop-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + + let mut holders = Vec::new(); + for name in ["sam", "hunter", "safe"] { + let key = dir.join(format!("{name}.key")); + let out = Command::new(age.join("age-keygen")) + .arg("-o") + .arg(&key) + .output() + .unwrap(); + assert!(out.status.success()); + let text = std::fs::read_to_string(&key).unwrap(); + let recipient = text + .lines() + .find_map(|l| l.strip_prefix("# public key: ")) + .unwrap() + .to_owned(); + holders.push(HolderRecipient { + holder: name.into(), + recipient, + }); + } + let bundle = EscrowBundle::create( + &Ark::generate(), + ArkId::generate(), + 1, + &holders, + RecipientPolicy::AllowSoftware, + ) + .unwrap(); + let bundle_path = dir.join("bundle.json"); + std::fs::write(&bundle_path, serde_json::to_vec(&bundle).unwrap()).unwrap(); + + for (i, h) in holders.iter().enumerate() { + // extract-share → stock age -d → check-share. + let share = Command::new(BIN) + .args([ + "extract-share", + "--allow-software", + "--holder", + &h.holder, + "--bundle", + ]) + .arg(&bundle_path) + .output() + .unwrap(); + assert!(share.status.success()); + let share_file = dir.join(format!("{}.age", h.holder)); + std::fs::write(&share_file, &share.stdout).unwrap(); + let plain = Command::new(age.join("age")) + .arg("-d") + .arg("-i") + .arg(dir.join(format!("{}.key", h.holder))) + .arg(&share_file) + .output() + .unwrap(); + assert!( + plain.status.success(), + "{}", + String::from_utf8_lossy(&plain.stderr) + ); + let checked = pipe( + Command::new(BIN) + .args(["check-share", "--allow-software", "--bundle"]) + .arg(&bundle_path), + &plain.stdout, + ); + let stderr = String::from_utf8_lossy(&checked.stderr); + assert!(checked.status.success(), "{stderr}"); + assert!( + stderr.contains(&format!("share {} ({})", i + 1, h.holder)), + "{stderr}" + ); + + // Another holder's identity can't open it. + let other = holders[(i + 1) % 3].holder.clone(); + let wrong = Command::new(age.join("age")) + .arg("-d") + .arg("-i") + .arg(dir.join(format!("{other}.key"))) + .arg(&share_file) + .output() + .unwrap(); + assert!(!wrong.status.success()); + } +} + +type Kem = hpke::kem::DhP256HkdfSha256; + +/// A software P-256 identity for `p256tag` stanzas (as a hardware key's +/// plugin would open them). +struct TagIdentity(::PrivateKey); + +impl age::Identity for TagIdentity { + fn unwrap_stanza(&self, stanza: &Stanza) -> Option> { + if stanza.tag != "p256tag" || stanza.args.len() != 2 { + return None; + } + let enc = base64::engine::general_purpose::STANDARD_NO_PAD + .decode(&stanza.args[1]) + .ok()?; + let enc = ::EncappedKey::from_bytes(&enc).ok()?; + let key = age_core::primitives::hpke_open::( + &enc, + &self.0, + b"age-encryption.org/p256tag", + &stanza.body, + ) + .ok()?; + let key: [u8; 16] = key.try_into().ok()?; + Some(Ok(FileKey::new(Box::new(key)))) + } +} + +#[test] +fn stock_age_p256tag_output_opens_with_the_p256_key() { + // Cross-implementation vector: Go age encrypts to an age1tag1 recipient; + // the Rust stack the TEE uses (age-core HPKE) opens it with the key. + let Some(age) = age_dir("stock_age_p256tag_output_opens_with_the_p256_key") else { + return; + }; + let sk = p256::SecretKey::random(&mut rand::rngs::OsRng); + let hrp = bech32::Hrp::parse("age1tag").unwrap(); + let recipient = + age_core::primitives::bech32_encode(hrp, sk.public_key().to_encoded_point(true).as_bytes()); + let message = b"tinycloud p256tag interop"; + let out = pipe( + Command::new(age.join("age")).args(["-e", "-r", &recipient]), + message, + ); + assert!( + out.status.success(), + "{}", + String::from_utf8_lossy(&out.stderr) + ); + + let identity = TagIdentity(::PrivateKey::from_bytes(&sk.to_bytes()).unwrap()); + let decryptor = age::Decryptor::new_buffered(&out.stdout[..]).unwrap(); + let mut reader = decryptor + .decrypt(std::iter::once(&identity as &dyn age::Identity)) + .unwrap(); + let mut plain = Vec::new(); + reader.read_to_end(&mut plain).unwrap(); + assert_eq!(plain, message); +} diff --git a/tinycloud-at-rest-cli/tests/cli.rs b/tinycloud-at-rest-cli/tests/cli.rs new file mode 100644 index 00000000..56084766 --- /dev/null +++ b/tinycloud-at-rest-cli/tests/cli.rs @@ -0,0 +1,445 @@ +//! Binary-level tests: what a holder sees and the exit code they get. +//! 0 OK, 1 REFUSED, 3 ERROR (input could not be read). + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output, Stdio}; + +use tinycloud_at_rest_escrow::{ + binding_digest, recovery_binding, Ark, ArkId, EscrowBundle, HolderRecipient, RecipientPolicy, + RecoveryIdentity, RecoveryInfo, SharePayload, RECOVERY_INFO_VERSION, +}; + +const BIN: &str = env!("CARGO_BIN_EXE_tinycloud-at-rest"); +const NONCE: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + +struct Fixture { + dir: PathBuf, + ids: Vec, + ark: Ark, + ark_id: ArkId, + holders: Vec, + bundle: EscrowBundle, +} + +impl Fixture { + fn new(name: &str) -> Self { + let dir = std::env::temp_dir().join(format!("tc887-cli-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let ids: Vec<_> = (0..3).map(|_| age::x25519::Identity::generate()).collect(); + let holders: Vec = ["sam", "hunter", "safe"] + .iter() + .zip(&ids) + .map(|(h, id)| HolderRecipient { + holder: h.to_string(), + recipient: id.to_public().to_string(), + }) + .collect(); + let (ark, ark_id) = (Ark::generate(), ArkId::generate()); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); + let f = Self { + dir, + ids, + ark, + ark_id, + holders, + bundle, + }; + f.write("bundle.json", &serde_json::to_vec(&f.bundle).unwrap()); + f + } + + fn path(&self, name: &str) -> PathBuf { + self.dir.join(name) + } + + fn write(&self, name: &str, bytes: &[u8]) -> PathBuf { + let p = self.path(name); + std::fs::write(&p, bytes).unwrap(); + p + } + + /// What holder `i` gets from `age -d`. + fn decrypted(&self, bundle: &EscrowBundle, i: usize) -> Vec { + let armored = &bundle.shares[i].ciphertext; + let decryptor = + age::Decryptor::new_buffered(age::armor::ArmoredReader::new(armored.as_bytes())) + .unwrap(); + let mut reader = decryptor + .decrypt(std::iter::once(&self.ids[i] as &dyn age::Identity)) + .unwrap(); + let mut out = Vec::new(); + reader.read_to_end(&mut out).unwrap(); + out + } + + fn info(&self, mode: &str, quote: Option<&str>) -> RecoveryInfo { + let tee = RecoveryIdentity::generate(); + let efp = self.bundle.fingerprint(); + let binding = recovery_binding(&tee.recipient(), &efp, NONCE); + RecoveryInfo { + version: RECOVERY_INFO_VERSION, + mode: mode.into(), + state: "unlocked".into(), + ark_id: self.bundle.ark_id, + generation: 1, + efp, + canary_fp: self.bundle.canary_fp, + recovery_recipient: tee.recipient(), + binding_sha256: binding_digest(&binding), + binding, + quote: quote.map(str::to_owned), + event_log: Some("[]".into()), + shares_held: vec![], + } + } +} + +fn run(args: &[&str], stdin: Option<&[u8]>) -> Output { + let mut child = Command::new(BIN) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + { + let mut pipe = child.stdin.take().unwrap(); + if let Some(bytes) = stdin { + pipe.write_all(bytes).unwrap(); + } + } + child.wait_with_output().unwrap() +} + +fn p(path: &Path) -> &str { + path.to_str().unwrap() +} + +#[track_caller] +fn expect(out: &Output, code: i32, needle: &str) { + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(code), "stderr: {stderr}"); + assert!(stderr.contains(needle), "{stderr:?} lacks {needle:?}"); +} + +#[test] +fn software_holders_are_refused_unless_allowed_and_then_flagged() { + let f = Fixture::new("software"); + let b = f.path("bundle.json"); + expect( + &run(&["inspect", "--bundle", p(&b)], None), + 1, + "software keys", + ); + let out = run(&["inspect", "--bundle", p(&b), "--allow-software"], None); + expect(&out, 0, "!!! WARNING"); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(stdout.contains(&f.bundle.fingerprint().to_string())); + assert_eq!(stdout.matches("X25519!").count(), 3); +} + +#[test] +fn expect_efp_guards_every_bundle_command() { + let f = Fixture::new("expect"); + let b = f.path("bundle.json"); + let efp = f.bundle.fingerprint().to_string(); + let other = "0".repeat(64); + for cmd in ["inspect", "check-share"] { + expect( + &run( + &[ + cmd, + "--bundle", + p(&b), + "--allow-software", + "--expect-efp", + &other, + ], + Some(b"x"), + ), + 1, + "not the expected", + ); + } + expect( + &run( + &[ + "extract-share", + "--bundle", + p(&b), + "--allow-software", + "--expect-efp", + &other, + "--holder", + "sam", + ], + None, + ), + 1, + "stale", + ); + // Case-insensitive. + let upper = efp.to_uppercase(); + let out = run( + &[ + "inspect", + "--bundle", + p(&b), + "--allow-software", + "--expect-efp", + &upper, + ], + None, + ); + assert_eq!(out.status.code(), Some(0)); +} + +#[test] +fn check_share_names_the_real_problem() { + let f = Fixture::new("check"); + let b = f.path("bundle.json"); + let args = ["check-share", "--bundle", p(&b), "--allow-software"]; + + let good = f.decrypted(&f.bundle, 1); + expect(&run(&args, Some(&good)), 0, "share 2 (hunter)"); + + // `age -d` failed and printed nothing. + expect(&run(&args, Some(b"")), 3, "did `age -d` fail"); + + // A share from an earlier split of the same key: commitment mismatch, with a hint. + let resplit = EscrowBundle::create( + &f.ark, + f.ark_id, + 1, + &f.holders, + RecipientPolicy::AllowSoftware, + ) + .unwrap(); + let old = f.decrypted(&resplit, 0); + expect(&run(&args, Some(&old)), 1, "re-split"); + + // Tampered bytes. + let mut bad = good.clone(); + bad[40] ^= 1; + expect(&run(&args, Some(&bad)), 1, "commitment"); + assert!(SharePayload::decode(&bad).is_ok()); +} + +#[test] +fn input_problems_are_errors_not_refusals() { + let f = Fixture::new("input"); + let b = f.path("bundle.json"); + expect( + &run(&["inspect", "--bundle", p(&f.path("missing.json"))], None), + 3, + "ERROR", + ); + expect( + &run( + &[ + "extract-share", + "--bundle", + p(&b), + "--allow-software", + "--holder", + "mallory", + ], + None, + ), + 3, + "no holder \"mallory\"", + ); + // A newer bundle format is named as such. + let mut v2: serde_json::Value = serde_json::to_value(&f.bundle).unwrap(); + v2["version"] = 2.into(); + v2["kdf"] = "x".into(); + let v2 = f.write("v2.json", &serde_json::to_vec(&v2).unwrap()); + expect(&run(&["inspect", "--bundle", p(&v2)], None), 1, "version 2"); +} + +#[test] +fn verify_node_reports_local_causes_before_any_network() { + let f = Fixture::new("node"); + let b = f.path("bundle.json"); + let trust = f.write( + "trust.json", + serde_json::json!({ + "app_id": "1".repeat(40), "compose_hash": "2".repeat(64), + "mrtd": "a".repeat(96), "rtmr0": "a".repeat(96), + "rtmr1": "a".repeat(96), "rtmr2": "a".repeat(96), + }) + .to_string() + .as_bytes(), + ); + // An unroutable PCCS: reaching it would be a network error, so any local + // refusal must come first. + let node = |info: &RecoveryInfo, nonce: &str| { + let r = f.write("recovery.json", &serde_json::to_vec(info).unwrap()); + run( + &[ + "verify-node", + "--bundle", + p(&b), + "--allow-software", + "--recovery", + p(&r), + "--nonce", + nonce, + "--trust", + p(&trust), + "--pccs-url", + "http://127.0.0.1:9", + ], + None, + ) + }; + expect( + &node(&f.info("classic", Some("00")), NONCE), + 1, + "not in a TEE", + ); + expect(&node(&f.info("dstack", None), NONCE), 1, "no TDX quote"); + expect(&node(&f.info("dstack", Some("zz")), NONCE), 1, "not hex"); + expect( + &node(&f.info("dstack", Some("00")), "short"), + 1, + "at least 22", + ); + expect( + &node( + &f.info("dstack", Some("00")), + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + ), + 1, + "not for your nonce", + ); + let mut stale = f.info("dstack", Some("00")); + stale.efp = tinycloud_at_rest_escrow::Digest32([9; 32]); + expect(&node(&stale, NONCE), 1, "stale"); + // Everything local passes: now the PCCS is contacted and fails as input. + expect(&node(&f.info("dstack", Some("00")), NONCE), 3, "collateral"); + // An older/newer node response format is named. + let r = f.write("recovery.json", br#"{"version":2,"mode":"dstack"}"#); + let out = run( + &[ + "verify-node", + "--bundle", + p(&b), + "--allow-software", + "--recovery", + p(&r), + "--nonce", + NONCE, + "--trust", + p(&trust), + ], + None, + ); + expect(&out, 1, "recovery response is version 2"); +} + +#[test] +fn record_efp_never_claims_authenticity() { + use tinycloud_at_rest_escrow::VerificationRecord; + let f = Fixture::new("record"); + let b = f.path("bundle.json"); + let shares: Vec = (0..3) + .map(|i| f.bundle.open_share(&f.decrypted(&f.bundle, i)).unwrap()) + .collect(); + let unlock = f.bundle.test_unlock(&f.ark, &shares).unwrap(); + let record = + VerificationRecord::seal(&f.ark, &f.bundle, &unlock, None, None, "t".into()).unwrap(); + let mut forged = serde_json::to_value(&record).unwrap(); + forged["mac"] = "0".repeat(64).into(); + forged["app_id"] = "attacker".into(); + let forged = f.write("forged.json", &serde_json::to_vec(&forged).unwrap()); + let out = run( + &[ + "record-efp", + "--bundle", + p(&b), + "--allow-software", + "--record", + p(&forged), + ], + None, + ); + expect(&out, 0, "NOT AUTHENTICATED"); + assert!(!String::from_utf8_lossy(&out.stderr).contains("OK")); + + // A record for another escrow is refused outright. + let other = EscrowBundle::create( + &f.ark, + f.ark_id, + 1, + &f.holders, + RecipientPolicy::AllowSoftware, + ) + .unwrap(); + let other = f.write("other.json", &serde_json::to_vec(&other).unwrap()); + let real = f.write("record.json", &serde_json::to_vec(&record).unwrap()); + expect( + &run( + &[ + "record-efp", + "--bundle", + p(&other), + "--allow-software", + "--record", + p(&real), + ], + None, + ), + 1, + "different escrow bundle", + ); +} + +#[test] +fn self_test_round_trips_and_refuses_software_by_default() { + let f = Fixture::new("selftest"); + let id = &f.ids[0]; + let recipient = id.to_public().to_string(); + expect( + &run( + &["self-test", "--recipient", &recipient, "--out", p(&f.dir)], + None, + ), + 1, + "software keys", + ); + let out = run( + &[ + "self-test", + "--recipient", + &recipient, + "--allow-software", + "--out", + p(&f.dir), + ], + None, + ); + expect(&out, 0, "THROWAWAY"); + let armored = std::fs::read_to_string(f.path("selftest-share.age")).unwrap(); + let decryptor = + age::Decryptor::new_buffered(age::armor::ArmoredReader::new(armored.as_bytes())).unwrap(); + let mut plain = Vec::new(); + decryptor + .decrypt(std::iter::once(id as &dyn age::Identity)) + .unwrap() + .read_to_end(&mut plain) + .unwrap(); + let bundle = f.path("selftest-bundle.json"); + expect( + &run( + &["check-share", "--bundle", p(&bundle), "--allow-software"], + Some(&plain), + ), + 0, + "share 1 (selftest)", + ); +} diff --git a/tinycloud-at-rest-escrow/Cargo.toml b/tinycloud-at-rest-escrow/Cargo.toml index abf5d6a6..d343522f 100644 --- a/tinycloud-at-rest-escrow/Cargo.toml +++ b/tinycloud-at-rest-escrow/Cargo.toml @@ -9,6 +9,10 @@ description = "Escrow of the TinyCloud at-rest root key: 2-of-3 Shamir shares se # Pinned exactly: share and bundle formats depend on these crates' behaviour. age = { version = "=0.12.1", default-features = false, features = ["armor"] } vsss-rs = { version = "=5.4.0", default-features = false, features = ["std", "zeroize"] } +# age-plugin-yubikey's `piv-p256` stanza (see src/piv.rs); same versions age uses. +age-core = "=0.12.0" +p256 = { version = "0.13", features = ["ecdh"] } +base64 = { workspace = true } hkdf = "0.12" hmac = "0.12" sha2 = "0.10" @@ -21,10 +25,7 @@ hex = { workspace = true } thiserror = { workspace = true } [dev-dependencies] -# A software P-256 identity that opens `p256tag` stanzas, so tests can -# round-trip shares sealed to hardware-key (`age1tag1…`) recipients. -age-core = "0.12" +# Software P-256 identities that open `p256tag` and `piv-p256` stanzas, so +# tests can round-trip shares sealed to hardware-key recipients. hpke = { version = "0.12", default-features = false, features = ["alloc", "p256"] } -p256 = "0.13" -base64 = { workspace = true } bech32 = "0.11" diff --git a/tinycloud-at-rest-escrow/src/bundle.rs b/tinycloud-at-rest-escrow/src/bundle.rs index a495f08f..431808c9 100644 --- a/tinycloud-at-rest-escrow/src/bundle.rs +++ b/tinycloud-at-rest-escrow/src/bundle.rs @@ -6,7 +6,9 @@ use serde::{Deserialize, Serialize}; use vsss_rs::Gf256; use zeroize::Zeroizing; -use crate::recipients::{parse, validate_holders, HolderRecipient, ParsedRecipient}; +use crate::recipients::{ + parse, validate_holders, HolderRecipient, ParsedRecipient, RecipientKind, RecipientPolicy, +}; use crate::share::{SharePayload, PAYLOAD_LEN}; use crate::{ canary_fingerprint, lp, sha256, Ark, ArkId, Digest32, Efp, EscrowError, HOLDERS, THRESHOLD, @@ -95,6 +97,7 @@ fn combine(a: &SharePayload, b: &SharePayload) -> Result { impl EscrowBundle { /// TEE only. Split `ark` 2-of-3 and seal share i to `holders[i]`. + /// Software (X25519) holders are refused unless `policy` allows them. /// /// Before returning, every pair of fresh shares is combined and compared /// with `ark`, so a split that can't be recovered never leaves this call. @@ -103,8 +106,9 @@ impl EscrowBundle { ark_id: ArkId, generation: u32, holders: &[HolderRecipient], + policy: RecipientPolicy, ) -> Result { - validate_holders(holders)?; + validate_holders(holders, policy)?; let split = Zeroizing::new( Gf256::split_array( usize::from(THRESHOLD), @@ -145,7 +149,15 @@ impl EscrowBundle { Ok(bundle) } - /// Structural checks. Every other method calls this first. + /// Structural checks. Every other method calls this first. Any recipient + /// kind passes here; apply a policy with [`check_recipients`](Self::check_recipients). + /// Parse a bundle: version first, then strict fields, then [`validate`](Self::validate). + pub fn from_json(bytes: &[u8]) -> Result { + let bundle: Self = crate::parse_versioned(bytes, "escrow bundle", BUNDLE_VERSION)?; + bundle.validate()?; + Ok(bundle) + } + pub fn validate(&self) -> Result<(), EscrowError> { if self.version != BUNDLE_VERSION { return Err(EscrowError::Bundle("unsupported version")); @@ -169,10 +181,20 @@ impl EscrowBundle { )); } } - validate_holders(&self.holders())?; + validate_holders(&self.holders(), RecipientPolicy::AllowSoftware)?; Ok(()) } + /// The holders' recipient kinds, refusing software holders unless + /// `policy` allows them. + pub fn check_recipients( + &self, + policy: RecipientPolicy, + ) -> Result, EscrowError> { + self.validate()?; + validate_holders(&self.holders(), policy) + } + /// The holders and recipients, in index order. pub fn holders(&self) -> Vec { self.shares @@ -299,7 +321,9 @@ mod tests { #[test] fn create_then_two_holders_recover_and_one_cannot() { let (ark, ark_id, holders, ids) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let shares = open_all(&bundle, &ids); for (a, b) in [(0, 1), (0, 2), (1, 2), (2, 0)] { @@ -319,11 +343,13 @@ mod tests { // splits of the same ARK give unrelated share bytes. let (ark, ark_id, holders, ids) = escrow_fixture(); let a = open_all( - &EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(), + &EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(), &ids, ); let b = open_all( - &EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(), + &EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(), &ids, ); assert_ne!(a[0].share.as_slice(), b[0].share.as_slice()); @@ -333,7 +359,9 @@ mod tests { #[test] fn tampered_share_or_wrong_ark_fails_before_combining() { let (ark, ark_id, holders, ids) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let shares = open_all(&bundle, &ids); let mut flipped = shares[1].clone(); @@ -343,7 +371,14 @@ mod tests { Err(EscrowError::Commitment { index: 2 }) )); - let other = EscrowBundle::create(&Ark::generate(), ArkId::generate(), 1, &holders).unwrap(); + let other = EscrowBundle::create( + &Ark::generate(), + ArkId::generate(), + 1, + &holders, + RecipientPolicy::AllowSoftware, + ) + .unwrap(); let foreign = open_all(&other, &ids); assert!(matches!( bundle.recover(&shares[0], &foreign[1]), @@ -356,7 +391,9 @@ mod tests { // An attacker who controls the stored bundle can rewrite a commitment // to match a junk share. The canary still rejects the combination. let (ark, ark_id, holders, ids) = escrow_fixture(); - let mut bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let mut bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let shares = open_all(&bundle, &ids); let mut junk = shares[1].clone(); junk.share[7] ^= 1; @@ -370,7 +407,9 @@ mod tests { #[test] fn test_unlock_needs_all_three_and_the_same_ark() { let (ark, ark_id, holders, ids) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let shares = open_all(&bundle, &ids); let ok = bundle.test_unlock(&ark, &shares).unwrap(); @@ -392,7 +431,9 @@ mod tests { #[test] fn fingerprint_covers_recipients_commitments_ciphertexts_and_canary() { let (ark, ark_id, holders, _) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let efp = bundle.fingerprint(); let mutations: Vec = vec![ Box::new(|b| b.shares[0].recipient = x25519_identity().to_public().to_string()), @@ -417,7 +458,9 @@ mod tests { #[test] fn validate_rejects_malformed_bundles() { let (ark, ark_id, holders, _) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let mutations: Vec = vec![ Box::new(|b| b.version = 2), Box::new(|b| b.threshold = 1), @@ -440,15 +483,98 @@ mod tests { } #[test] - fn hardware_tag_share_opens_with_the_p256_key_only() { + fn hardware_shares_open_with_their_own_p256_key_only() { let (ark, ark_id, holders, ids) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); - // Holder 1 is a p256tag recipient; holder 2's identity can't open it. - assert!(bundle.shares[0].recipient.starts_with("age1tag1")); - assert!(ids[1].open(&bundle.shares[0].ciphertext).is_err()); - let share = bundle - .open_share(&ids[0].open(&bundle.shares[0].ciphertext).unwrap()) - .unwrap(); - assert_eq!(share.index, 1); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); + // Share 1 is piv-p256 (age1yubikey1), share 2 is p256tag (age1tag1). + assert!(bundle.shares[0].recipient.starts_with("age1yubikey1")); + assert!(bundle.shares[1].recipient.starts_with("age1tag1")); + for (i, j) in [(0, 1), (1, 0), (0, 2), (1, 2)] { + assert!( + ids[j].open(&bundle.shares[i].ciphertext).is_err(), + "{j} opened {i}" + ); + } + for (i, (id, entry)) in ids.iter().zip(&bundle.shares).take(2).enumerate() { + let share = bundle + .open_share(&id.open(&entry.ciphertext).unwrap()) + .unwrap(); + assert_eq!(usize::from(share.index), i + 1); + } + } + + #[test] + fn software_holders_need_the_explicit_policy() { + let (ark, ark_id, holders, _) = escrow_fixture(); + assert!(matches!( + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::HardwareOnly), + Err(EscrowError::Recipient { holder, .. }) if holder == "safe" + )); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); + assert!(bundle + .check_recipients(RecipientPolicy::HardwareOnly) + .is_err()); + assert_eq!( + bundle + .check_recipients(RecipientPolicy::AllowSoftware) + .unwrap(), + vec![ + RecipientKind::PivP256, + RecipientKind::P256Tag, + RecipientKind::X25519 + ] + ); + } + + #[test] + fn newer_versions_are_reported_as_versions_not_unknown_fields() { + let (ark, ark_id, holders, _) = escrow_fixture(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); + let json = serde_json::to_vec(&bundle).unwrap(); + assert_eq!(EscrowBundle::from_json(&json).unwrap(), bundle); + + let mut v2: serde_json::Value = serde_json::from_slice(&json).unwrap(); + v2["version"] = 2.into(); + v2["kdf"] = "new".into(); + let err = EscrowBundle::from_json(&serde_json::to_vec(&v2).unwrap()).unwrap_err(); + assert!( + matches!( + err, + EscrowError::Version { + found: 2, + supported: 1, + .. + } + ), + "{err}" + ); + // Same version with an unknown field is still refused. + let mut extra: serde_json::Value = serde_json::from_slice(&json).unwrap(); + extra["kdf"] = "new".into(); + assert!(matches!( + EscrowBundle::from_json(&serde_json::to_vec(&extra).unwrap()), + Err(EscrowError::Json { .. }) + )); + assert!(matches!( + EscrowBundle::from_json(b"{}"), + Err(EscrowError::Json { .. }) + )); + + let info = serde_json::json!({"version": 9, "mode": "dstack", "future": true}); + assert!(matches!( + crate::RecoveryInfo::from_json(info.to_string().as_bytes()), + Err(EscrowError::Version { found: 9, .. }) + )); + let record = serde_json::json!({"version": 3, "efp": "x"}); + assert!(matches!( + crate::VerificationRecord::from_json(record.to_string().as_bytes()), + Err(EscrowError::Version { found: 3, .. }) + )); } } diff --git a/tinycloud-at-rest-escrow/src/lib.rs b/tinycloud-at-rest-escrow/src/lib.rs index 977f4629..c3838b70 100644 --- a/tinycloud-at-rest-escrow/src/lib.rs +++ b/tinycloud-at-rest-escrow/src/lib.rs @@ -15,7 +15,10 @@ //! //! All derivations use length-prefixed fields: `lp(x) = u32 BE len ‖ x`. +#![forbid(unsafe_code)] + mod bundle; +mod piv; mod recipients; mod record; mod recovery; @@ -30,11 +33,13 @@ use subtle::ConstantTimeEq; use zeroize::Zeroizing; pub use bundle::{EscrowBundle, EscrowShare, TestUnlock}; -pub use recipients::{parse_recipient, validate_holders, HolderRecipient, RecipientKind}; +pub use recipients::{ + parse_recipient, validate_holders, HolderRecipient, RecipientKind, RecipientPolicy, +}; pub use record::VerificationRecord; pub use recovery::{ binding_digest, recovery_binding, validate_nonce, wrap_share, RecoveryIdentity, RecoveryInfo, - MAX_WRAPPED_SHARE, + MAX_WRAPPED_SHARE, RECOVERY_INFO_VERSION, }; pub use share::SharePayload; @@ -56,8 +61,16 @@ pub enum EscrowError { ShareFormat, #[error("share belongs to a different ARK or generation")] ShareMismatch, - #[error("share {index} does not match its commitment in the bundle")] + #[error("share {index} does not match its commitment in this bundle: the share may come from a different escrow (a re-split) of the same key; check the bundle's EFP")] Commitment { index: u8 }, + #[error("{what} is not valid: {reason}")] + Json { what: &'static str, reason: String }, + #[error("{what} is version {found}, but this tool reads version {supported}; use the tool built from the release that matches the node")] + Version { + what: &'static str, + found: u64, + supported: u32, + }, #[error("two shares with index {index}")] DuplicateIndex { index: u8 }, #[error("combined shares do not reproduce the escrowed key (canary mismatch)")] @@ -229,6 +242,35 @@ pub(crate) fn sha256(bytes: &[u8]) -> [u8; 32] { Sha256::digest(bytes).into() } +/// Parse a versioned JSON document: read `version` first, so a newer format +/// is reported as such instead of as an unknown field; then parse strictly. +pub(crate) fn parse_versioned( + bytes: &[u8], + what: &'static str, + supported: u32, +) -> Result { + let json_err = |e: serde_json::Error| EscrowError::Json { + what, + reason: e.to_string(), + }; + let value: serde_json::Value = serde_json::from_slice(bytes).map_err(json_err)?; + let found = value + .get("version") + .and_then(serde_json::Value::as_u64) + .ok_or_else(|| EscrowError::Json { + what, + reason: "no numeric `version` field".into(), + })?; + if found != u64::from(supported) { + return Err(EscrowError::Version { + what, + found, + supported, + }); + } + serde_json::from_value(value).map_err(json_err) +} + const CANARY_INFO: &[u8] = b"tinycloud/at-rest/escrow-canary/v1"; const CANARY_FP_DOMAIN: &[u8] = b"tinycloud/at-rest/escrow-canary-fp/v1"; diff --git a/tinycloud-at-rest-escrow/src/piv.rs b/tinycloud-at-rest-escrow/src/piv.rs new file mode 100644 index 00000000..a92338cb --- /dev/null +++ b/tinycloud-at-rest-escrow/src/piv.rs @@ -0,0 +1,96 @@ +//! `age1yubikey1…` recipients: the `piv-p256` stanza of age-plugin-yubikey. +//! +//! Released age-plugin-yubikey (0.5.x) prints and decrypts only this type; +//! native `age1tag1` support is unreleased. Encrypting to it needs no +//! YubiKey, only the public key, so the TEE can seal shares to a YubiKey +//! without the plugin binary. This mirrors the plugin's own +//! `RecipientLine::wrap_file_key` (format.rs, 0.5.1): +//! +//! ```text +//! tag = SHA-256(pk_compressed)[..4] +//! epk = fresh P-256 key, compressed SEC-1 (33 bytes) +//! shared = ECDH(esk, pk).x +//! enc_key = HKDF-SHA256(salt = epk ‖ pk_compressed, ikm = shared, info = "piv-p256") +//! body = ChaCha20-Poly1305(enc_key, nonce = 0, file_key) (32 bytes) +//! stanza = "-> piv-p256 b64(tag) b64(epk)" + body +//! ``` + +use std::collections::HashSet; + +use age_core::{ + format::{FileKey, Stanza}, + primitives::{aead_encrypt, bech32_decode, hkdf}, + secrecy::ExposeSecret, +}; +use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine}; +use p256::{ + ecdh::EphemeralSecret, + elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}, + EncodedPoint, PublicKey, +}; +use sha2::{Digest, Sha256}; + +pub(crate) const HRP: &str = "age1yubikey"; +pub(crate) const STANZA_TAG: &str = "piv-p256"; +pub(crate) const STANZA_KEY_LABEL: &[u8] = b"piv-p256"; + +#[derive(Clone)] +pub(crate) struct PivRecipient(PublicKey); + +impl std::str::FromStr for PivRecipient { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let bytes = bech32_decode( + s, + |_| "invalid Bech32 encoding", + |hrp| (hrp.as_str() == HRP).then_some(()).ok_or("incorrect HRP"), + |_, bytes| Ok(bytes.collect::>()), + )?; + let encoded = EncodedPoint::from_bytes(bytes).map_err(|_| "invalid SEC-1 encoding")?; + if !encoded.is_compressed() { + return Err("not a compressed SEC-1 encoding"); + } + PublicKey::from_encoded_point(&encoded) + .into_option() + .map(Self) + .ok_or("invalid P-256 point") + } +} + +impl PivRecipient { + fn compressed(&self) -> EncodedPoint { + self.0.to_encoded_point(true) + } +} + +impl age::Recipient for PivRecipient { + fn wrap_file_key( + &self, + file_key: &FileKey, + ) -> Result<(Vec, HashSet), age::EncryptError> { + let esk = EphemeralSecret::random(&mut rand::rngs::OsRng); + let epk = esk.public_key().to_encoded_point(true); + let shared = esk.diffie_hellman(&self.0); + let pk = self.compressed(); + + let mut salt = Vec::with_capacity(66); + salt.extend_from_slice(epk.as_bytes()); + salt.extend_from_slice(pk.as_bytes()); + let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared.raw_secret_bytes()); + let body = aead_encrypt(&enc_key, file_key.expose_secret()); + let tag = &Sha256::digest(pk.as_bytes())[..4]; + + Ok(( + vec![Stanza { + tag: STANZA_TAG.to_owned(), + args: vec![ + BASE64_STANDARD_NO_PAD.encode(tag), + BASE64_STANDARD_NO_PAD.encode(epk.as_bytes()), + ], + body, + }], + HashSet::new(), + )) + } +} diff --git a/tinycloud-at-rest-escrow/src/recipients.rs b/tinycloud-at-rest-escrow/src/recipients.rs index 42b2ed23..cf36dc11 100644 --- a/tinycloud-at-rest-escrow/src/recipients.rs +++ b/tinycloud-at-rest-escrow/src/recipients.rs @@ -4,6 +4,7 @@ use std::collections::HashSet; use serde::{Deserialize, Serialize}; +use crate::piv::PivRecipient; use crate::{EscrowError, HOLDERS}; /// One escrow holder: a label and the age recipient their share is sealed to. @@ -12,21 +13,44 @@ use crate::{EscrowError, HOLDERS}; pub struct HolderRecipient { /// 1-32 chars of `a-z 0-9 - _`, unique within the escrow. pub holder: String, - /// `age1tag1…` (P-256 hardware key, e.g. a YubiKey via age-plugin-yubikey) - /// or `age1…` (X25519, software; tests and self-host only). + /// See [`RecipientKind`]. pub recipient: String, } +/// The recipient format. The format says which kind of key can decrypt; it +/// does not prove where the private key lives. That a P-256 key was made on +/// a YubiKey is established in the escrow ceremony, not here. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RecipientKind { - /// `age1tag1…` (`p256tag` stanza). Meant for keys held in hardware; age - /// encrypts to it natively, decryption needs the holder's plugin. - HardwareTag, - /// `age1…` X25519. The identity is a file, not hardware. - SoftwareX25519, + /// `age1yubikey1…` (`piv-p256` stanza): what released age-plugin-yubikey + /// prints and decrypts. + PivP256, + /// `age1tag1…` (`p256tag` stanza): native age tagged P-256 recipient, + /// meant for hardware keys. + P256Tag, + /// `age1…` X25519: a software key file. Test and self-host only. + X25519, +} + +impl RecipientKind { + pub fn is_software(self) -> bool { + self == Self::X25519 + } +} + +/// Which recipient kinds an escrow may use. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum RecipientPolicy { + /// P-256 key formats only (`age1yubikey1`, `age1tag1`). The default. + #[default] + HardwareOnly, + /// Also X25519 software keys. Only for tests and self-hosted nodes that + /// explicitly opt in; tools warn loudly whenever it is in effect. + AllowSoftware, } pub(crate) enum ParsedRecipient { + Piv(PivRecipient), Tag(age::tag::Recipient), X25519(age::x25519::Recipient), } @@ -34,10 +58,19 @@ pub(crate) enum ParsedRecipient { impl ParsedRecipient { pub(crate) fn as_age(&self) -> &dyn age::Recipient { match self { + Self::Piv(r) => r, Self::Tag(r) => r, Self::X25519(r) => r, } } + + fn kind(&self) -> RecipientKind { + match self { + Self::Piv(_) => RecipientKind::PivP256, + Self::Tag(_) => RecipientKind::P256Tag, + Self::X25519(_) => RecipientKind::X25519, + } + } } pub(crate) fn parse(holder: &str, recipient: &str) -> Result { @@ -45,31 +78,31 @@ pub(crate) fn parse(holder: &str, recipient: &str) -> Result() + .map(ParsedRecipient::Piv) + .map_err(err) + } else if recipient.starts_with("age1tag1") { recipient .parse::() .map(ParsedRecipient::Tag) .map_err(err) - } else if recipient.starts_with("age1yubikey1") { - Err(err( - "age1yubikey1 recipients need the plugin to encrypt; use the age1tag1 form of the same key", - )) } else if recipient.starts_with("age1") { recipient .parse::() .map(ParsedRecipient::X25519) .map_err(err) } else { - Err(err("not an age1tag1 or age1 X25519 recipient")) + Err(err( + "not an age1yubikey1, age1tag1 or age1 X25519 recipient", + )) } } /// Parse one recipient and report its kind. pub fn parse_recipient(holder: &str, recipient: &str) -> Result { - Ok(match parse(holder, recipient)? { - ParsedRecipient::Tag(_) => RecipientKind::HardwareTag, - ParsedRecipient::X25519(_) => RecipientKind::SoftwareX25519, - }) + parse(holder, recipient).map(|r| r.kind()) } fn valid_holder_name(name: &str) -> bool { @@ -79,8 +112,12 @@ fn valid_holder_name(name: &str) -> bool { .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_') } -/// Exactly three holders, valid unique names, unique recipients that parse. -pub fn validate_holders(holders: &[HolderRecipient]) -> Result, EscrowError> { +/// Exactly three holders, valid unique names, unique recipients that parse, +/// and no software recipient unless `policy` allows it. +pub fn validate_holders( + holders: &[HolderRecipient], + policy: RecipientPolicy, +) -> Result, EscrowError> { if holders.len() != usize::from(HOLDERS) { return Err(EscrowError::Holders(format!( "need exactly {HOLDERS} holders, got {}", @@ -109,7 +146,14 @@ pub fn validate_holders(holders: &[HolderRecipient]) -> Result Result HolderRecipient { HolderRecipient { @@ -127,40 +171,50 @@ mod tests { } #[test] - fn accepts_tag_and_x25519_and_reports_kind() { + fn reports_each_kind_and_refuses_software_by_default() { let holders = vec![ - holder("sam", tag_identity().1), + holder("sam", piv_identity().1), holder("hunter", tag_identity().1), holder("safe", x25519_identity().to_public().to_string()), ]; assert_eq!( - validate_holders(&holders).unwrap(), + validate_holders(&holders, RecipientPolicy::AllowSoftware).unwrap(), vec![ - RecipientKind::HardwareTag, - RecipientKind::HardwareTag, - RecipientKind::SoftwareX25519 + RecipientKind::PivP256, + RecipientKind::P256Tag, + RecipientKind::X25519 ] ); + let err = validate_holders(&holders, RecipientPolicy::HardwareOnly).unwrap_err(); + assert!( + matches!(&err, EscrowError::Recipient { holder, .. } if holder == "safe"), + "{err}" + ); + let hardware = vec![ + holder("sam", piv_identity().1), + holder("hunter", tag_identity().1), + holder("safe", piv_identity().1), + ]; + validate_holders(&hardware, RecipientPolicy::HardwareOnly).unwrap(); } #[test] fn rejects_count_names_duplicates_and_bad_recipients() { - let r = || x25519_identity().to_public().to_string(); - assert!(validate_holders(&[holder("a", r()), holder("b", r())]).is_err()); - assert!(validate_holders(&[ + let r = || piv_identity().1; + let v = |h: &[HolderRecipient]| validate_holders(h, RecipientPolicy::AllowSoftware); + assert!(v(&[holder("a", r()), holder("b", r())]).is_err()); + assert!(v(&[ holder("a", r()), holder("b", r()), holder("c", r()), holder("d", r()) ]) .is_err()); - assert!(validate_holders(&[holder("a", r()), holder("a", r()), holder("c", r())]).is_err()); - assert!( - validate_holders(&[holder("Sam", r()), holder("b", r()), holder("c", r())]).is_err() - ); - assert!(validate_holders(&[holder("", r()), holder("b", r()), holder("c", r())]).is_err()); + assert!(v(&[holder("a", r()), holder("a", r()), holder("c", r())]).is_err()); + assert!(v(&[holder("Sam", r()), holder("b", r()), holder("c", r())]).is_err()); + assert!(v(&[holder("", r()), holder("b", r()), holder("c", r())]).is_err()); let same = r(); - assert!(validate_holders(&[ + assert!(v(&[ holder("a", same.clone()), holder("b", same), holder("c", r()) @@ -172,9 +226,20 @@ mod tests { "age1notavalidrecipient", "age1tag1notvalid", ] { - let err = - validate_holders(&[holder("a", bad.into()), holder("b", r()), holder("c", r())]); - assert!(err.is_err(), "{bad}"); + assert!( + v(&[holder("a", bad.into()), holder("b", r()), holder("c", r())]).is_err(), + "{bad}" + ); } } + + #[test] + fn parses_a_real_age_plugin_yubikey_recipient() { + // From age-plugin-yubikey 0.5.1 tests/integration.rs. + let real = "age1yubikey1q2w7u3vpya839jxxuq8g0sedh3d740d4xvn639sqhr95ejj8vu3hyfumptt"; + assert_eq!( + parse_recipient("sam", real).unwrap(), + RecipientKind::PivP256 + ); + } } diff --git a/tinycloud-at-rest-escrow/src/record.rs b/tinycloud-at-rest-escrow/src/record.rs index 65a9f93a..24a90020 100644 --- a/tinycloud-at-rest-escrow/src/record.rs +++ b/tinycloud-at-rest-escrow/src/record.rs @@ -36,6 +36,11 @@ pub struct VerificationRecord { } impl VerificationRecord { + /// Parse a record: version first, then strict fields. + pub fn from_json(bytes: &[u8]) -> Result { + crate::parse_versioned(bytes, "verification record", RECORD_VERSION) + } + /// TEE only. Seal a record for a test-unlock of `bundle`. pub fn seal( ark: &Ark, @@ -136,7 +141,9 @@ mod tests { fn sealed() -> (Ark, EscrowBundle, VerificationRecord) { let (ark, ark_id, holders, ids) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let unlock = bundle.test_unlock(&ark, &open_all(&bundle, &ids)).unwrap(); let record = VerificationRecord::seal( &ark, @@ -181,7 +188,14 @@ mod tests { fn record_does_not_cover_a_different_or_partial_escrow() { let (ark, bundle, record) = sealed(); // Same ARK, re-split to the same holders: new ciphertexts, new EFP. - let resplit = EscrowBundle::create(&ark, bundle.ark_id, 1, &bundle.holders()).unwrap(); + let resplit = EscrowBundle::create( + &ark, + bundle.ark_id, + 1, + &bundle.holders(), + RecipientPolicy::AllowSoftware, + ) + .unwrap(); assert!(record.verify(&ark, &resplit).is_err()); let mut partial = record.clone(); diff --git a/tinycloud-at-rest-escrow/src/recovery.rs b/tinycloud-at-rest-escrow/src/recovery.rs index 0c0f5c4f..748b718c 100644 --- a/tinycloud-at-rest-escrow/src/recovery.rs +++ b/tinycloud-at-rest-escrow/src/recovery.rs @@ -14,6 +14,8 @@ use crate::{sha256, ArkId, Digest32, Efp, EscrowError, SharePayload}; /// Largest wrapped share the TEE reads. A real one is about 400 bytes. pub const MAX_WRAPPED_SHARE: usize = 4096; const BINDING_PREFIX: &str = "xyz.tinycloud.at-rest/recovery-key/v1\\0"; +/// Wire version of [`RecoveryInfo`]. +pub const RECOVERY_INFO_VERSION: u32 = 1; /// What `GET /at-rest/recovery?nonce=` returns. Every field is public. Only /// what the holder recomputes and the verified quote prove anything; the @@ -21,6 +23,8 @@ const BINDING_PREFIX: &str = "xyz.tinycloud.at-rest/recovery-key/v1\\0"; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RecoveryInfo { + /// [`RECOVERY_INFO_VERSION`]; checked before any other field. + pub version: u32, /// `"dstack"` when `quote` is a TDX quote, `"classic"` outside a TEE. pub mode: String, /// `"locked"` or `"unlocked"`. @@ -42,6 +46,13 @@ pub struct RecoveryInfo { pub shares_held: Vec, } +impl RecoveryInfo { + /// Parse a node's answer: version first, then strict fields. + pub fn from_json(bytes: &[u8]) -> Result { + crate::parse_versioned(bytes, "recovery response", RECOVERY_INFO_VERSION) + } +} + /// `xyz.tinycloud.at-rest/recovery-key/v1\0:::`. The /// `\0` is two characters (backslash, zero), as in `/attestation`. pub fn recovery_binding(recipient: &str, efp: &Efp, nonce: &str) -> String { @@ -135,7 +146,9 @@ mod tests { #[test] fn wrapped_share_round_trips_through_the_recovery_identity_only() { let (ark, ark_id, holders, ids) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let shares = open_all(&bundle, &ids); let tee = RecoveryIdentity::generate(); let wrapped = wrap_share(&tee.recipient(), &shares[2]).unwrap(); @@ -171,7 +184,9 @@ mod tests { #[test] fn wrap_refuses_tag_recipients() { let (ark, ark_id, holders, ids) = escrow_fixture(); - let bundle = EscrowBundle::create(&ark, ark_id, 1, &holders).unwrap(); + let bundle = + EscrowBundle::create(&ark, ark_id, 1, &holders, RecipientPolicy::AllowSoftware) + .unwrap(); let share = &open_all(&bundle, &ids)[0]; assert!(wrap_share(&tag_identity().1, share).is_err()); } diff --git a/tinycloud-at-rest-escrow/src/test_support.rs b/tinycloud-at-rest-escrow/src/test_support.rs index 9548fb0c..0bf744e7 100644 --- a/tinycloud-at-rest-escrow/src/test_support.rs +++ b/tinycloud-at-rest-escrow/src/test_support.rs @@ -1,20 +1,25 @@ -//! Test-only identities. `TagIdentity` is a software P-256 key that opens -//! `p256tag` stanzas the way age-plugin-yubikey does with a hardware key, so -//! tests can round-trip shares sealed to `age1tag1…` recipients. +//! Test-only identities: software P-256 keys that open `piv-p256` and +//! `p256tag` stanzas exactly as age-plugin-yubikey does with the key on a +//! YubiKey, so tests can round-trip shares sealed to hardware-key recipients. use std::io::Read; use age_core::format::{FileKey, Stanza}; use base64::Engine; use hpke::Deserializable; -use p256::elliptic_curve::sec1::ToEncodedPoint; +use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; use zeroize::Zeroizing; +pub(crate) use crate::RecipientPolicy; use crate::{Ark, ArkId, EscrowBundle, HolderRecipient, SharePayload}; type Kem = hpke::kem::DhP256HkdfSha256; const P256TAG_SALT: &[u8] = b"age-encryption.org/p256tag"; +fn b64() -> base64::engine::GeneralPurpose { + base64::engine::general_purpose::STANDARD_NO_PAD +} + pub(crate) struct TagIdentity(::PrivateKey); /// A fresh P-256 key and its `age1tag1…` recipient. @@ -32,9 +37,7 @@ impl age::Identity for TagIdentity { if stanza.tag != "p256tag" || stanza.args.len() != 2 { return None; } - let enc = base64::engine::general_purpose::STANDARD_NO_PAD - .decode(&stanza.args[1]) - .ok()?; + let enc = b64().decode(&stanza.args[1]).ok()?; let enc = ::EncappedKey::from_bytes(&enc).ok()?; let opened = age_core::primitives::hpke_open::(&enc, &self.0, P256TAG_SALT, &stanza.body) @@ -44,11 +47,50 @@ impl age::Identity for TagIdentity { } } +pub(crate) struct PivIdentity(p256::SecretKey); + +/// A fresh P-256 key and its `age1yubikey1…` recipient. +pub(crate) fn piv_identity() -> (PivIdentity, String) { + let sk = p256::SecretKey::random(&mut rand::rngs::OsRng); + let compressed = sk.public_key().to_encoded_point(true); + let hrp = bech32::Hrp::parse("age1yubikey").unwrap(); + // age-plugin-yubikey encodes with standard Bech32 (bech32 0.9 Variant::Bech32). + let recipient = bech32::encode::(hrp, compressed.as_bytes()).unwrap(); + (PivIdentity(sk), recipient) +} + +impl age::Identity for PivIdentity { + /// age-plugin-yubikey 0.5.1 `YubiKey::unwrap_file_key`, with the + /// YubiKey's scalar multiplication done in software. + fn unwrap_stanza(&self, stanza: &Stanza) -> Option> { + if stanza.tag != "piv-p256" || stanza.args.len() != 2 { + return None; + } + let pk = self.0.public_key().to_encoded_point(true); + let tag = b64().decode(&stanza.args[0]).ok()?; + if tag[..] != ::digest(pk.as_bytes())[..4] { + return None; + } + let epk_bytes = b64().decode(&stanza.args[1]).ok()?; + let epk = + p256::PublicKey::from_encoded_point(&p256::EncodedPoint::from_bytes(&epk_bytes).ok()?) + .into_option()?; + let shared = p256::ecdh::diffie_hellman(self.0.to_nonzero_scalar(), epk.as_affine()); + let mut salt = epk_bytes.clone(); + salt.extend_from_slice(pk.as_bytes()); + let enc_key = age_core::primitives::hkdf(&salt, b"piv-p256", shared.raw_secret_bytes()); + let key = age_core::primitives::aead_decrypt(&enc_key, 16, &stanza.body).ok()?; + let key: [u8; 16] = key.try_into().ok()?; + Some(Ok(FileKey::new(Box::new(key)))) + } +} + pub(crate) fn x25519_identity() -> age::x25519::Identity { age::x25519::Identity::generate() } pub(crate) enum TestIdentity { + Piv(PivIdentity), Tag(TagIdentity), X25519(age::x25519::Identity), } @@ -57,6 +99,7 @@ impl TestIdentity { /// What `age -d -i share.age` prints. pub(crate) fn open(&self, armored: &str) -> Result>, age::DecryptError> { let identity: &dyn age::Identity = match self { + Self::Piv(i) => i, Self::Tag(i) => i, Self::X25519(i) => i, }; @@ -69,9 +112,10 @@ impl TestIdentity { } } -/// An ARK and three holders: two hardware-tag keys and one X25519 key. +/// An ARK and three holders, one of each recipient kind: `age1yubikey1`, +/// `age1tag1` and X25519 (so bundles need `RecipientPolicy::AllowSoftware`). pub(crate) fn escrow_fixture() -> (Ark, ArkId, Vec, Vec) { - let (sam, sam_r) = tag_identity(); + let (sam, sam_r) = piv_identity(); let (hunter, hunter_r) = tag_identity(); let safe = x25519_identity(); let safe_r = safe.to_public().to_string(); @@ -83,7 +127,7 @@ pub(crate) fn escrow_fixture() -> (Ark, ArkId, Vec, Vec Date: Sat, 17 Oct 2026 07:14:46 +0000 Subject: [PATCH 3/6] fix(TC-887): build the ARK only from zeroizing buffers (#325) --- tinycloud-at-rest-escrow/src/bundle.rs | 6 +----- tinycloud-at-rest-escrow/src/lib.rs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tinycloud-at-rest-escrow/src/bundle.rs b/tinycloud-at-rest-escrow/src/bundle.rs index 431808c9..b155989e 100644 --- a/tinycloud-at-rest-escrow/src/bundle.rs +++ b/tinycloud-at-rest-escrow/src/bundle.rs @@ -88,11 +88,7 @@ pub(crate) fn seal_armored( fn combine(a: &SharePayload, b: &SharePayload) -> Result { let shares = Zeroizing::new(vec![a.share.to_vec(), b.share.to_vec()]); let secret = Zeroizing::new(Gf256::combine_array(&*shares).map_err(|_| EscrowError::Shamir)?); - let bytes: [u8; 32] = secret - .as_slice() - .try_into() - .map_err(|_| EscrowError::Shamir)?; - Ok(Ark::from_bytes(bytes)) + Ark::from_slice(&secret).ok_or(EscrowError::Shamir) } impl EscrowBundle { diff --git a/tinycloud-at-rest-escrow/src/lib.rs b/tinycloud-at-rest-escrow/src/lib.rs index c3838b70..b6fd745a 100644 --- a/tinycloud-at-rest-escrow/src/lib.rs +++ b/tinycloud-at-rest-escrow/src/lib.rs @@ -90,8 +90,20 @@ pub enum EscrowError { pub struct Ark(Zeroizing<[u8; 32]>); impl Ark { - pub fn from_bytes(bytes: [u8; 32]) -> Self { - Self(Zeroizing::new(bytes)) + /// Take ownership of key bytes already in a zeroizing container. + pub fn from_zeroizing(bytes: Zeroizing<[u8; 32]>) -> Self { + Self(bytes) + } + + /// Copy 32 key bytes straight into a zeroizing buffer; `None` for any + /// other length. No plain array copy is made. + pub fn from_slice(bytes: &[u8]) -> Option { + if bytes.len() != 32 { + return None; + } + let mut key = Zeroizing::new([0u8; 32]); + key.copy_from_slice(bytes); + Some(Self(key)) } /// A fresh key from the OS CSPRNG. From 4222883bbe4ad1ca85ddc64cf96b21885d62a578 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Sat, 17 Oct 2026 07:45:51 +0000 Subject: [PATCH 4/6] fix(TC-887): strict compose check, real plugin vector; review round 2 (#325) - verify-compose parses the compose YAML with yaml-rust2 (=0.13.0, CLI workspace only) and accepts a strict subset: one document, no anchors, aliases, merge keys, tags or duplicate keys, a services mapping without include, and every service with an `image` of exactly `[:tag]@sha256:<64 hex>` and no build/extends. It also lists the other app-compose settings the hash covers. The four reported bypasses (comment, flow mapping, quoted key, build) are tests. - Checked-in piv-p256 vector produced by the released age-plugin-yubikey 0.5.1; the test identity now rejects what the plugin's from_stanza rejects (argument lengths, uncompressed epk, body length). - Warn on age1tag1 holders (0.5.1 can't decrypt p256tag). - Version-first parsing keeps duplicate-key rejection; RecoveryInfo mode and state are validated. - CLI: self-test creates --out; malformed bundle/recovery/record JSON is exit 3 (ERROR); a hardening TCB class without advisories is refused. --- docs/at-rest-escrow.md | 12 +- tinycloud-at-rest-cli/Cargo.lock | 39 +++ tinycloud-at-rest-cli/Cargo.toml | 1 + tinycloud-at-rest-cli/src/attest.rs | 51 ++-- tinycloud-at-rest-cli/src/compose.rs | 262 ++++++++++++++++++ tinycloud-at-rest-cli/src/main.rs | 48 +++- tinycloud-at-rest-cli/tests/cli.rs | 30 +- tinycloud-at-rest-escrow/src/bundle.rs | 14 +- tinycloud-at-rest-escrow/src/lib.rs | 17 +- tinycloud-at-rest-escrow/src/piv.rs | 67 +++++ tinycloud-at-rest-escrow/src/recovery.rs | 13 +- tinycloud-at-rest-escrow/src/test_support.rs | 71 ++++- .../tests/fixtures/NOTICE.md | 12 + .../tests/fixtures/piv_p256_plugin_0_5_1.age | 8 + 14 files changed, 584 insertions(+), 61 deletions(-) create mode 100644 tinycloud-at-rest-cli/src/compose.rs create mode 100644 tinycloud-at-rest-escrow/tests/fixtures/NOTICE.md create mode 100644 tinycloud-at-rest-escrow/tests/fixtures/piv_p256_plugin_0_5_1.age diff --git a/docs/at-rest-escrow.md b/docs/at-rest-escrow.md index 344f2024..0d2d53c8 100644 --- a/docs/at-rest-escrow.md +++ b/docs/at-rest-escrow.md @@ -29,7 +29,7 @@ cargo build --release --locked --manifest-path tinycloud-at-rest-cli/Cargo.toml - The sealed plaintext is `"TCS1" ‖ ark_id(16) ‖ generation u32 BE ‖ i ‖ share(33)`, 58 bytes. - **Recipients.** Each share is age-encrypted (`age` 0.12.1, ASCII armor) to one holder. The recipient type can be: - `age1yubikey1…` (`piv-p256` stanza). This is what released age-plugin-yubikey (0.5.x) prints and decrypts. The stanza is produced exactly as the plugin's own `wrap_file_key`, so the TEE needs no plugin. - - `age1tag1…` (`p256tag` stanza). This is native age 1.3 tagged P-256. Decrypting it needs a plugin build that supports it. + - `age1tag1…` (`p256tag` stanza). This is native age 1.3 tagged P-256. The released age-plugin-yubikey 0.5.1 **cannot** decrypt it, so every command warns when a holder uses one. - `age1…` X25519. These are software keys, refused unless software recipients are explicitly allowed (`--allow-software` on the CLI; tests and self-hosted nodes only). Every command prints a warning while that is in effect. - A recipient format proves which kind of key decrypts, **not** where the private key lives. That a P-256 key was generated on a YubiKey is established in the escrow ceremony. - **Commitment.** `SHA-256(lp("tinycloud/at-rest/escrow-share-commit/v1") ‖ ark_id ‖ generation ‖ i ‖ lp(share))`. @@ -67,7 +67,7 @@ Exit codes: | `inspect --bundle B` | bundle | EFP, ark_id, generation, canary_fp, each holder's recipient kind | | `extract-share --bundle B --holder H` | bundle | holder H's armored age file | | `check-share --bundle B` | decrypted share on **stdin** | which share it is; refuses on a commitment mismatch (e.g. a share from an earlier split) | -| `verify-compose --app-compose A --trust T` | the deployment's `app-compose.json` | OK if it hashes to `compose_hash` and every `image:` is `…@sha256:` | +| `verify-compose --app-compose A --trust T` | the deployment's `app-compose.json` | OK if it hashes to `compose_hash` and its compose YAML passes the strict check below; also lists the other app-compose settings to review | | `verify-node --bundle B --recovery R --nonce N --trust T [--collateral C \| --pccs-url U]` | node's recovery response | OK (with node state and shares held) or REFUSED | | `share-wrap` (same flags) | decrypted share on **stdin** | after `verify-node` passes: the share sealed to the node's recovery key | | `record-efp --bundle B --record V` | record | `TINYCLOUD_AT_REST__ESCROW__VERIFIED_FP=`, marked NOT AUTHENTICATED: the record's MAC is checked only in the TEE | @@ -97,7 +97,13 @@ All of these must hold, or the command prints `REFUSED: …` and exits 1: Not pinned: `key-provider`, `instance-id`, `os-image-hash`, XFAM, MRCONFIGID, MROWNER and MROWNERCONFIG. RTMR0-2 and the compose hash cover what the per-boot, in-memory recovery key depends on. -`compose_hash` pins container images only if `app-compose.json` references every image by `@sha256:` digest. Check that with `verify-compose` when building the trust policy. +`compose_hash` pins container images only if `app-compose.json` references every image by digest. `verify-compose` parses the compose YAML with a real parser and accepts only a small subset: +- exactly one document; +- no anchors, aliases, merge keys, tags or duplicate keys; +- a `services` mapping, and no `include`; +- every service has a string `image` of the form `[:tag]@sha256:<64 lowercase hex>`, with no `build` or `extends`. + +Anything else is refused. Run it when building the trust policy. A PCCS can't forge collateral, which is Intel-signed. A malicious PCCS can, however, serve older collateral that is still valid (TCB info is valid for about 30 days), which could hide a TCB recovery for that long. Use Intel PCS when that matters. diff --git a/tinycloud-at-rest-cli/Cargo.lock b/tinycloud-at-rest-cli/Cargo.lock index 21d235ab..5195516f 100644 --- a/tinycloud-at-rest-cli/Cargo.lock +++ b/tinycloud-at-rest-cli/Cargo.lock @@ -153,6 +153,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.8" @@ -1022,6 +1028,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.3" @@ -1345,6 +1360,18 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +dependencies = [ + "hashbrown 0.17.1", +] [[package]] name = "heapless" @@ -3340,6 +3367,7 @@ dependencies = [ "sha2 0.10.9", "tinycloud-at-rest-escrow", "tokio", + "yaml-rust2", "zeroize", ] @@ -3980,6 +4008,17 @@ dependencies = [ "spki 0.8.1", ] +[[package]] +name = "yaml-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b818a27a4cd30884ea380857a5e56f7ec3ba24a3990a3cc0b95af3238e18" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/tinycloud-at-rest-cli/Cargo.toml b/tinycloud-at-rest-cli/Cargo.toml index 144498ec..0916d2af 100644 --- a/tinycloud-at-rest-cli/Cargo.toml +++ b/tinycloud-at-rest-cli/Cargo.toml @@ -31,6 +31,7 @@ zeroize = "1" # `self-test`: throwaway P-256 co-holders encoded as age1yubikey1. p256 = "0.13" bech32 = "0.11" +yaml-rust2 = "=0.13.0" [dev-dependencies] # Tests only: drive a genuine quote and collateral through every TCB status diff --git a/tinycloud-at-rest-cli/src/attest.rs b/tinycloud-at-rest-cli/src/attest.rs index 736d5500..eb0cbece 100644 --- a/tinycloud-at-rest-cli/src/attest.rs +++ b/tinycloud-at-rest-cli/src/attest.rs @@ -172,6 +172,13 @@ pub fn check_tcb(td: &AttestedTd, trust: &TrustPolicy) -> Result { td.advisory_ids, trust.allowed_tcb_status ); + // A non-UpToDate class always comes with the advisories that caused it; + // one without any is anomalous. + ensure!( + accepted == AcceptedTcb::UpToDate || !td.advisory_ids.is_empty(), + "TDX TCB status {} carries no advisory IDs, which is anomalous; refused", + td.tcb_status + ); let unpinned: Vec<&String> = td .advisory_ids .iter() @@ -229,10 +236,18 @@ pub fn require_attested(info: &RecoveryInfo) -> Result> { Ok(quote) } +/// What `verify-compose` found: each service's pinned image, plus the other +/// app-compose settings the hash covers. +#[derive(Debug)] +pub struct ComposeReport { + pub images: Vec<(String, String)>, + pub settings: Vec, +} + /// `compose-hash` pins container images only if the compose file names each /// image by digest. Check `app-compose.json` against the trust policy's -/// `compose_hash`, and that every `image:` line carries `@sha256:`. -pub fn check_app_compose(app_compose: &[u8], trust: &TrustPolicy) -> Result> { +/// `compose_hash`, then parse its compose YAML strictly (see `compose.rs`). +pub fn check_app_compose(app_compose: &[u8], trust: &TrustPolicy) -> Result { let digest = hex::encode(Sha256::digest(app_compose)); ensure!( digest == trust.compose_hash, @@ -245,27 +260,10 @@ pub fn check_app_compose(app_compose: &[u8], trust: &TrustPolicy) -> Result[:tag]@sha256:<64 +//! lowercase hex>`, and no `build` or `extends`. + +use std::collections::HashSet; + +use anyhow::{anyhow, bail, ensure, Result}; +use sha2::Digest as _; +use yaml_rust2::parser::{Event, Parser}; + +#[derive(Debug)] +enum Node { + Scalar(String), + /// Items are validated (no aliases, anchors, tags) but not kept: no + /// check needs a sequence's contents. + Seq, + Map(Vec<(String, Node)>), +} + +struct Reader<'a> { + parser: Parser>, +} + +impl Reader<'_> { + fn next(&mut self) -> Result { + let (event, mark) = self + .parser + .next_token() + .map_err(|e| anyhow!("the compose file is not valid YAML: {e}"))?; + match &event { + Event::Alias(_) => bail!("YAML aliases are not allowed (line {})", mark.line()), + Event::Scalar(_, _, anchor, tag) + | Event::SequenceStart(anchor, tag) + | Event::MappingStart(anchor, tag) => { + ensure!( + *anchor == 0, + "YAML anchors are not allowed (line {})", + mark.line() + ); + ensure!( + tag.is_none(), + "YAML tags are not allowed (line {})", + mark.line() + ); + } + _ => {} + } + Ok(event) + } + + fn node(&mut self, event: Event) -> Result { + match event { + Event::Scalar(value, ..) => Ok(Node::Scalar(value)), + Event::SequenceStart(..) => loop { + match self.next()? { + Event::SequenceEnd => return Ok(Node::Seq), + e => { + self.node(e)?; + } + } + }, + Event::MappingStart(..) => { + let mut entries = Vec::new(); + let mut seen = HashSet::new(); + loop { + let key = match self.next()? { + Event::MappingEnd => return Ok(Node::Map(entries)), + Event::Scalar(key, ..) => key, + _ => bail!("only plain string mapping keys are allowed"), + }; + ensure!(key != "<<", "YAML merge keys are not allowed"); + ensure!(seen.insert(key.clone()), "duplicate key {key:?}"); + let value = self.next()?; + entries.push((key, self.node(value)?)); + } + } + other => bail!("unexpected YAML structure: {other:?}"), + } + } +} + +fn parse(yaml: &str) -> Result { + let mut r = Reader { + parser: Parser::new_from_str(yaml), + }; + ensure!(matches!(r.next()?, Event::StreamStart), "not a YAML stream"); + ensure!( + matches!(r.next()?, Event::DocumentStart), + "the compose file is empty" + ); + let first = r.next()?; + let root = r.node(first)?; + ensure!( + matches!(r.next()?, Event::DocumentEnd), + "trailing YAML content" + ); + ensure!( + matches!(r.next()?, Event::StreamEnd), + "the compose file must be a single YAML document" + ); + Ok(root) +} + +fn get<'a>(entries: &'a [(String, Node)], key: &str) -> Option<&'a Node> { + entries.iter().find(|(k, _)| k == key).map(|(_, v)| v) +} + +/// `[:tag]@sha256:<64 lowercase hex>`, no whitespace or comments. +fn pinned_reference(image: &str) -> bool { + let Some((name, digest)) = image.split_once("@sha256:") else { + return false; + }; + !name.is_empty() + && name + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-' | b'/' | b':')) + && digest.len() == 64 + && digest + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) +} + +/// Every service's image, each pinned by digest; refuses anything else. +pub fn pinned_images(compose_yaml: &str) -> Result> { + let Node::Map(top) = parse(compose_yaml)? else { + bail!("the compose file must be a mapping"); + }; + ensure!( + get(&top, "include").is_none(), + "`include` pulls in other compose files and is not allowed" + ); + let Some(Node::Map(services)) = get(&top, "services") else { + bail!("the compose file has no `services` mapping"); + }; + ensure!(!services.is_empty(), "the compose file has no services"); + let mut images = Vec::new(); + for (name, service) in services { + let Node::Map(fields) = service else { + bail!("service {name:?} is not a mapping"); + }; + for forbidden in ["build", "extends"] { + ensure!( + get(fields, forbidden).is_none(), + "service {name:?} uses `{forbidden}`; only digest-pinned images are allowed" + ); + } + let image = match get(fields, "image") { + Some(Node::Scalar(image)) => image, + Some(_) => bail!("service {name:?} has a non-string image"), + None => bail!("service {name:?} has no image"), + }; + ensure!( + pinned_reference(image), + "service {name:?} image {image:?} is not referenced as @sha256:<64 hex>" + ); + images.push((name.clone(), image.clone())); + } + Ok(images) +} + +/// The `app-compose.json` fields besides the compose file that the hash also +/// covers, for the holder to review. +pub fn other_settings(app_compose: &serde_json::Value) -> Vec { + let Some(map) = app_compose.as_object() else { + return Vec::new(); + }; + map.iter() + .filter(|(k, _)| k.as_str() != "docker_compose_file") + .map(|(k, v)| { + let shown = match v { + serde_json::Value::String(s) if s.len() > 80 => { + format!( + "<{} chars, sha256 {}>", + s.len(), + hex::encode(sha2::Sha256::digest(s.as_bytes())) + ) + } + other => other.to_string(), + }; + format!("{k} = {shown}") + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn digest() -> String { + "a".repeat(64) + } + + #[test] + fn digest_pinned_services_pass() { + let d = digest(); + let yaml = format!( + "services:\n node:\n image: ghcr.io/tinycloudlabs/tinycloud-node@sha256:{d}\n environment:\n - A=1\n ingress:\n image: \"caddy:2.8@sha256:{d}\"\nvolumes:\n data: {{}}\n" + ); + let images = pinned_images(&yaml).unwrap(); + assert_eq!(images.len(), 2); + assert_eq!(images[1].1, format!("caddy:2.8@sha256:{d}")); + } + + #[test] + fn every_known_bypass_is_refused() { + let d = digest(); + let cases = [ + // 1. A comment is not part of the value: the image is nginx:latest. + (format!("services:\n web:\n image: nginx:latest # @sha256:{d}\n"), "nginx:latest"), + // 2. Flow mapping. + ("services:\n web: {image: nginx:latest}\n".to_string(), "nginx:latest"), + // 3. Quoted key. + ("services:\n web:\n \"image\": nginx:latest\n".to_string(), "nginx:latest"), + // 4. build instead of an image. + ("services:\n web:\n build: .\n".to_string(), "build"), + // Anchors, aliases and merges that hide the real image. + (format!("x: &img\n image: a@sha256:{d}\nservices:\n web:\n <<: *img\n image: nginx\n"), "anchor"), + (format!("services:\n a:\n image: &i a@sha256:{d}\n b:\n image: *i\n"), "anchor"), + // Duplicate keys: last-wins parsers would run the second. + (format!("services:\n web:\n image: a@sha256:{d}\n image: nginx\n"), "duplicate"), + // Tags, extends, include, tag-only, digest of the wrong length/case. + (format!("services:\n web:\n image: !!str a@sha256:{d}\n"), "tags"), + (format!("services:\n web:\n image: a@sha256:{d}\n extends: base\n"), "extends"), + (format!("include:\n - other.yaml\nservices:\n web:\n image: a@sha256:{d}\n"), "include"), + ("services:\n web:\n image: nginx:1.27\n".to_string(), "nginx:1.27"), + (format!("services:\n web:\n image: a@sha256:{}\n", "A".repeat(64)), "is not referenced"), + (format!("services:\n web:\n image: a@sha256:{d}0\n"), "is not referenced"), + ("services:\n web:\n environment: [A=1]\n".to_string(), "no image"), + // Several documents, or no services. + (format!("services:\n web:\n image: a@sha256:{d}\n---\nservices: {{}}\n"), "single YAML document"), + ("version: '3'\n".to_string(), "no `services`"), + ("services: [\n".to_string(), "not valid YAML"), + ]; + for (yaml, needle) in cases { + let err = pinned_images(&yaml).expect_err(&yaml).to_string(); + assert!(err.contains(needle), "{yaml:?}: {err}"); + } + } + + #[test] + fn other_settings_are_listed_for_review() { + let doc = serde_json::json!({ + "docker_compose_file": "services: {}", + "kms_enabled": true, + "pre_launch_script": "x".repeat(100), + }); + let lines = other_settings(&doc); + assert!(lines.iter().any(|l| l == "kms_enabled = true")); + assert!(lines + .iter() + .any(|l| l.starts_with("pre_launch_script = <100 chars"))); + assert!(!lines.iter().any(|l| l.starts_with("docker_compose_file"))); + } +} diff --git a/tinycloud-at-rest-cli/src/main.rs b/tinycloud-at-rest-cli/src/main.rs index 5d218a01..52302e2b 100644 --- a/tinycloud-at-rest-cli/src/main.rs +++ b/tinycloud-at-rest-cli/src/main.rs @@ -9,6 +9,7 @@ #![forbid(unsafe_code)] mod attest; +mod compose; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; @@ -181,22 +182,41 @@ fn read_json(path: &Path, what: &str) -> Result< .map_err(|e| input_err(format!("parsing {what} {}: {e}", path.display()))) } +/// Loud warnings for holder kinds that need care: software keys, and +/// `age1tag1` keys that the released plugin can't decrypt. fn warn_software(holders: &[HolderRecipient], kinds: &[RecipientKind]) { for (h, kind) in holders.iter().zip(kinds) { - if kind.is_software() { - eprintln!( + match kind { + RecipientKind::X25519 => eprintln!( "!!! WARNING: holder {:?} uses an X25519 SOFTWARE key, not a hardware key. \ Accepted only because --allow-software was given (tests and self-host only). !!!", h.holder - ); + ), + RecipientKind::P256Tag => eprintln!( + "!!! WARNING: holder {:?} uses an age1tag1 (p256tag) recipient. The released \ + age-plugin-yubikey 0.5.1 CANNOT decrypt it; use the age1yubikey1 recipient \ + that `age-plugin-yubikey --list` prints. !!!", + h.holder + ), + RecipientKind::PivP256 => {} } } } +/// A file that isn't the expected JSON at all (a bad download, an HTML +/// error page) is an input error (exit 3); a well-formed but unacceptable +/// one is a refusal (exit 1). +fn parse_err(e: tinycloud_at_rest_escrow::EscrowError) -> anyhow::Error { + match e { + tinycloud_at_rest_escrow::EscrowError::Json { .. } => input_err(e.to_string()), + other => other.into(), + } +} + /// Read and validate a bundle, apply the recipient policy and `--expect-efp`. fn read_bundle(args: &BundleArgs) -> Result<(EscrowBundle, Vec)> { let bytes = read_file(&args.bundle, "bundle")?; - let bundle = EscrowBundle::from_json(&bytes)?; + let bundle = EscrowBundle::from_json(&bytes).map_err(parse_err)?; let kinds = bundle.check_recipients(args.software.policy())?; warn_software(&bundle.holders(), &kinds); if let Some(expected) = &args.expect_efp { @@ -231,7 +251,8 @@ fn read_share_stdin(bundle: &EscrowBundle) -> Result Result<(EscrowBundle, RecoveryInfo)> { let (bundle, _) = read_bundle(&args.bundle)?; - let info = RecoveryInfo::from_json(&read_file(&args.recovery, "recovery response")?)?; + let info = RecoveryInfo::from_json(&read_file(&args.recovery, "recovery response")?) + .map_err(parse_err)?; let trust: TrustPolicy = read_json(&args.trust, "trust policy")?; trust.validate()?; // Everything that needs no network first, so its cause is what's reported. @@ -360,6 +381,8 @@ async fn run(cli: Cli) -> Result<()> { &holders, software.policy(), )?; + std::fs::create_dir_all(&out) + .map_err(|e| input_err(format!("creating {}: {e}", out.display())))?; let bundle_path = out.join("selftest-bundle.json"); let share_path = out.join("selftest-share.age"); std::fs::write(&bundle_path, serde_json::to_vec_pretty(&bundle)?) @@ -432,13 +455,17 @@ async fn run(cli: Cli) -> Result<()> { let trust: TrustPolicy = read_json(&trust, "trust policy")?; trust.validate()?; let bytes = read_file(&app_compose, "app-compose.json")?; - let images = attest::check_app_compose(&bytes, &trust)?; + let report = attest::check_app_compose(&bytes, &trust)?; eprintln!( - "OK: app-compose.json matches compose_hash {}; every image is pinned by digest:", + "OK: app-compose.json matches compose_hash {}; every service's image is pinned by digest:", trust.compose_hash ); - for image in images { - eprintln!(" {image}"); + for (service, image) in report.images { + eprintln!(" {service}: {image}"); + } + eprintln!("Other settings the hash covers (review them):"); + for line in report.settings { + eprintln!(" {line}"); } } Command::VerifyNode(args) => { @@ -457,7 +484,8 @@ async fn run(cli: Cli) -> Result<()> { } Command::RecordEfp { bundle, record } => { let (bundle, _) = read_bundle(&bundle)?; - let record = VerificationRecord::from_json(&read_file(&record, "record")?)?; + let record = + VerificationRecord::from_json(&read_file(&record, "record")?).map_err(parse_err)?; record.covers(&bundle)?; eprintln!( "NOT AUTHENTICATED: this record names escrow {} and claims shares {:?} and pairs {:?} (verified_at {}),", diff --git a/tinycloud-at-rest-cli/tests/cli.rs b/tinycloud-at-rest-cli/tests/cli.rs index 56084766..d820ae54 100644 --- a/tinycloud-at-rest-cli/tests/cli.rs +++ b/tinycloud-at-rest-cli/tests/cli.rs @@ -259,6 +259,29 @@ fn input_problems_are_errors_not_refusals() { v2["kdf"] = "x".into(); let v2 = f.write("v2.json", &serde_json::to_vec(&v2).unwrap()); expect(&run(&["inspect", "--bundle", p(&v2)], None), 1, "version 2"); + // A bad download (HTML, truncated JSON) is an input error, not a refusal. + let html = f.write("html.json", b"502 Bad Gateway"); + expect(&run(&["inspect", "--bundle", p(&html)], None), 3, "ERROR"); + // A recovery response in an unknown state is malformed input too. + let mut info = serde_json::to_value(f.info("dstack", Some("00"))).unwrap(); + info["state"] = "rebooting".into(); + let r = f.write("weird.json", &serde_json::to_vec(&info).unwrap()); + let out = run( + &[ + "verify-node", + "--bundle", + p(&b), + "--allow-software", + "--recovery", + p(&r), + "--nonce", + NONCE, + "--trust", + p(&b), + ], + None, + ); + expect(&out, 3, "state must be"); } #[test] @@ -419,12 +442,13 @@ fn self_test_round_trips_and_refuses_software_by_default() { &recipient, "--allow-software", "--out", - p(&f.dir), + // A directory that doesn't exist yet: self-test creates it. + p(&f.dir.join("st/new")), ], None, ); expect(&out, 0, "THROWAWAY"); - let armored = std::fs::read_to_string(f.path("selftest-share.age")).unwrap(); + let armored = std::fs::read_to_string(f.dir.join("st/new/selftest-share.age")).unwrap(); let decryptor = age::Decryptor::new_buffered(age::armor::ArmoredReader::new(armored.as_bytes())).unwrap(); let mut plain = Vec::new(); @@ -433,7 +457,7 @@ fn self_test_round_trips_and_refuses_software_by_default() { .unwrap() .read_to_end(&mut plain) .unwrap(); - let bundle = f.path("selftest-bundle.json"); + let bundle = f.dir.join("st/new/selftest-bundle.json"); expect( &run( &["check-share", "--bundle", p(&bundle), "--allow-software"], diff --git a/tinycloud-at-rest-escrow/src/bundle.rs b/tinycloud-at-rest-escrow/src/bundle.rs index b155989e..5f60781f 100644 --- a/tinycloud-at-rest-escrow/src/bundle.rs +++ b/tinycloud-at-rest-escrow/src/bundle.rs @@ -145,8 +145,6 @@ impl EscrowBundle { Ok(bundle) } - /// Structural checks. Every other method calls this first. Any recipient - /// kind passes here; apply a policy with [`check_recipients`](Self::check_recipients). /// Parse a bundle: version first, then strict fields, then [`validate`](Self::validate). pub fn from_json(bytes: &[u8]) -> Result { let bundle: Self = crate::parse_versioned(bytes, "escrow bundle", BUNDLE_VERSION)?; @@ -154,6 +152,8 @@ impl EscrowBundle { Ok(bundle) } + /// Structural checks. Every other method calls this first. Any recipient + /// kind passes here; apply a policy with [`check_recipients`](Self::check_recipients). pub fn validate(&self) -> Result<(), EscrowError> { if self.version != BUNDLE_VERSION { return Err(EscrowError::Bundle("unsupported version")); @@ -550,6 +550,16 @@ mod tests { ), "{err}" ); + // Duplicate keys are refused (the typed parse sees them all). + let dup = String::from_utf8(json.clone()).unwrap().replacen( + "\"generation\":1", + "\"generation\":7,\"generation\":1", + 1, + ); + assert!(matches!( + EscrowBundle::from_json(dup.as_bytes()), + Err(EscrowError::Json { .. }) + )); // Same version with an unknown field is still refused. let mut extra: serde_json::Value = serde_json::from_slice(&json).unwrap(); extra["kdf"] = "new".into(); diff --git a/tinycloud-at-rest-escrow/src/lib.rs b/tinycloud-at-rest-escrow/src/lib.rs index b6fd745a..b65d89b9 100644 --- a/tinycloud-at-rest-escrow/src/lib.rs +++ b/tinycloud-at-rest-escrow/src/lib.rs @@ -255,20 +255,25 @@ pub(crate) fn sha256(bytes: &[u8]) -> [u8; 32] { } /// Parse a versioned JSON document: read `version` first, so a newer format -/// is reported as such instead of as an unknown field; then parse strictly. +/// is reported as such instead of as an unknown field; then parse the typed +/// struct strictly from the original bytes (duplicate keys and unknown +/// fields are refused). pub(crate) fn parse_versioned( bytes: &[u8], what: &'static str, supported: u32, ) -> Result { + #[derive(serde::Deserialize)] + struct Peek { + version: Option, + } let json_err = |e: serde_json::Error| EscrowError::Json { what, reason: e.to_string(), }; - let value: serde_json::Value = serde_json::from_slice(bytes).map_err(json_err)?; - let found = value - .get("version") - .and_then(serde_json::Value::as_u64) + let found = serde_json::from_slice::(bytes) + .map_err(json_err)? + .version .ok_or_else(|| EscrowError::Json { what, reason: "no numeric `version` field".into(), @@ -280,7 +285,7 @@ pub(crate) fn parse_versioned( supported, }); } - serde_json::from_value(value).map_err(json_err) + serde_json::from_slice(bytes).map_err(json_err) } const CANARY_INFO: &[u8] = b"tinycloud/at-rest/escrow-canary/v1"; diff --git a/tinycloud-at-rest-escrow/src/piv.rs b/tinycloud-at-rest-escrow/src/piv.rs index a92338cb..3626ec02 100644 --- a/tinycloud-at-rest-escrow/src/piv.rs +++ b/tinycloud-at-rest-escrow/src/piv.rs @@ -94,3 +94,70 @@ impl age::Recipient for PivRecipient { )) } } + +#[cfg(test)] +mod tests { + use std::io::Read; + + use age::Identity as _; + + use super::*; + use crate::test_support::*; + + #[test] + fn the_released_plugins_output_opens_with_the_test_identity() { + let decryptor = + age::Decryptor::new_buffered(age::armor::ArmoredReader::new(PLUGIN_VECTOR.as_bytes())) + .unwrap(); + let identity = plugin_vector_identity(); + let mut plain = Vec::new(); + decryptor + .decrypt(std::iter::once(&identity as &dyn age::Identity)) + .unwrap() + .read_to_end(&mut plain) + .unwrap(); + assert_eq!(plain, PLUGIN_VECTOR_MESSAGE); + // Our recipient parser reads the plugin's recipient string. + assert!(PLUGIN_VECTOR_RECIPIENT.parse::().is_ok()); + } + + #[test] + fn our_stanza_has_the_plugins_shape_and_bad_shapes_are_refused() { + let identity = plugin_vector_identity(); + let recipient: PivRecipient = PLUGIN_VECTOR_RECIPIENT.parse().unwrap(); + let file_key = FileKey::new(Box::new([7u8; 16])); + let (stanzas, _) = age::Recipient::wrap_file_key(&recipient, &file_key).unwrap(); + let stanza = &stanzas[0]; + assert_eq!(stanza.tag, "piv-p256"); + assert_eq!((stanza.args[0].len(), stanza.args[1].len()), (6, 44)); + assert_eq!(stanza.body.len(), 32); + let opened = identity.unwrap_stanza(stanza).unwrap().unwrap(); + assert_eq!(opened.expose_secret(), &[7u8; 16]); + + // What the plugin rejects as structurally invalid, the identity does too. + let epk = BASE64_STANDARD_NO_PAD.decode(&stanza.args[1]).unwrap(); + let point = PublicKey::from_sec1_bytes(&epk).unwrap(); + let uncompressed = BASE64_STANDARD_NO_PAD.encode(point.to_encoded_point(false).as_bytes()); + type Mutation = Box; + let mutations: Vec = vec![ + Box::new(move |s| s.args[1] = uncompressed.clone()), + Box::new(|s| s.args[0].push('A')), + Box::new(|s| { + s.args.pop(); + }), + Box::new(|s| s.body.push(0)), + ]; + for mutate in mutations { + let mut bad = Stanza { + tag: stanza.tag.clone(), + args: stanza.args.clone(), + body: stanza.body.clone(), + }; + mutate(&mut bad); + assert!(matches!(identity.unwrap_stanza(&bad), Some(Err(_)))); + } + // Another key's stanza is not ours. + let (other, _) = piv_identity(); + assert!(other.unwrap_stanza(stanza).is_none()); + } +} diff --git a/tinycloud-at-rest-escrow/src/recovery.rs b/tinycloud-at-rest-escrow/src/recovery.rs index 748b718c..1b2f4792 100644 --- a/tinycloud-at-rest-escrow/src/recovery.rs +++ b/tinycloud-at-rest-escrow/src/recovery.rs @@ -49,7 +49,18 @@ pub struct RecoveryInfo { impl RecoveryInfo { /// Parse a node's answer: version first, then strict fields. pub fn from_json(bytes: &[u8]) -> Result { - crate::parse_versioned(bytes, "recovery response", RECOVERY_INFO_VERSION) + let info: Self = crate::parse_versioned(bytes, "recovery response", RECOVERY_INFO_VERSION)?; + let invalid = |reason: &str| EscrowError::Json { + what: "recovery response", + reason: reason.into(), + }; + if !matches!(info.mode.as_str(), "dstack" | "classic") { + return Err(invalid("mode must be \"dstack\" or \"classic\"")); + } + if !matches!(info.state.as_str(), "locked" | "unlocked") { + return Err(invalid("state must be \"locked\" or \"unlocked\"")); + } + Ok(info) } } diff --git a/tinycloud-at-rest-escrow/src/test_support.rs b/tinycloud-at-rest-escrow/src/test_support.rs index 0bf744e7..9b199753 100644 --- a/tinycloud-at-rest-escrow/src/test_support.rs +++ b/tinycloud-at-rest-escrow/src/test_support.rs @@ -59,32 +59,79 @@ pub(crate) fn piv_identity() -> (PivIdentity, String) { (PivIdentity(sk), recipient) } +/// The released plugin's `RecipientLine::from_stanza` argument check: +/// standard unpadded base64 of exactly `N` bytes. +fn plugin_base64_arg(arg: &str) -> Option<[u8; N]> { + if arg.len() != (4 * N).div_ceil(3) { + return None; + } + let mut out = [0u8; N]; + match b64().decode_slice_unchecked(arg, &mut out) { + Ok(n) if n == N => Some(out), + _ => None, + } +} + impl age::Identity for PivIdentity { - /// age-plugin-yubikey 0.5.1 `YubiKey::unwrap_file_key`, with the - /// YubiKey's scalar multiplication done in software. + /// age-plugin-yubikey 0.5.1: `RecipientLine::from_stanza` (structure) and + /// `YubiKey::unwrap_file_key` (math), with the YubiKey's scalar + /// multiplication done in software. Anything the plugin treats as a + /// structurally invalid stanza is an error here too. fn unwrap_stanza(&self, stanza: &Stanza) -> Option> { - if stanza.tag != "piv-p256" || stanza.args.len() != 2 { + if stanza.tag != "piv-p256" { return None; } + let invalid = Some(Err(age::DecryptError::InvalidHeader)); + let [tag_arg, epk_arg] = &stanza.args[..] else { + return invalid; + }; + let (Some(tag), Some(epk_bytes)) = ( + plugin_base64_arg::<4>(tag_arg), + plugin_base64_arg::<33>(epk_arg), + ) else { + return invalid; + }; + let Ok(encoded) = p256::EncodedPoint::from_bytes(epk_bytes) else { + return invalid; + }; + let Some(epk) = p256::PublicKey::from_encoded_point(&encoded).into_option() else { + return invalid; + }; + if !encoded.is_compressed() || stanza.body.len() != 32 { + return invalid; + } let pk = self.0.public_key().to_encoded_point(true); - let tag = b64().decode(&stanza.args[0]).ok()?; if tag[..] != ::digest(pk.as_bytes())[..4] { return None; } - let epk_bytes = b64().decode(&stanza.args[1]).ok()?; - let epk = - p256::PublicKey::from_encoded_point(&p256::EncodedPoint::from_bytes(&epk_bytes).ok()?) - .into_option()?; let shared = p256::ecdh::diffie_hellman(self.0.to_nonzero_scalar(), epk.as_affine()); - let mut salt = epk_bytes.clone(); + let mut salt = epk_bytes.to_vec(); salt.extend_from_slice(pk.as_bytes()); let enc_key = age_core::primitives::hkdf(&salt, b"piv-p256", shared.raw_secret_bytes()); - let key = age_core::primitives::aead_decrypt(&enc_key, 16, &stanza.body).ok()?; - let key: [u8; 16] = key.try_into().ok()?; - Some(Ok(FileKey::new(Box::new(key)))) + Some( + age_core::primitives::aead_decrypt(&enc_key, 16, &stanza.body) + .ok() + .and_then(|key| <[u8; 16]>::try_from(key).ok()) + .map(|key| FileKey::new(Box::new(key))) + .ok_or(age::DecryptError::DecryptionFailed), + ) } } +/// The checked-in vector produced by the released plugin (see +/// `tests/fixtures/NOTICE.md`). +pub(crate) const PLUGIN_VECTOR: &str = include_str!("../tests/fixtures/piv_p256_plugin_0_5_1.age"); +pub(crate) const PLUGIN_VECTOR_MESSAGE: &[u8] = + b"tinycloud age-plugin-yubikey 0.5.1 piv-p256 vector"; +pub(crate) const PLUGIN_VECTOR_RECIPIENT: &str = + "age1yubikey1qg3fps8csqk30gjdt552hf7jyjnmt753z9yhk4p8f0x5vvr5m53mju3vr5u"; + +pub(crate) fn plugin_vector_identity() -> PivIdentity { + let scalar = + hex::decode("c2d3663dcc50dd691a502185a3f4de30f4c7ef664548414eca5c6a72790f36d2").unwrap(); + PivIdentity(p256::SecretKey::from_slice(&scalar).unwrap()) +} + pub(crate) fn x25519_identity() -> age::x25519::Identity { age::x25519::Identity::generate() } diff --git a/tinycloud-at-rest-escrow/tests/fixtures/NOTICE.md b/tinycloud-at-rest-escrow/tests/fixtures/NOTICE.md new file mode 100644 index 00000000..d1247130 --- /dev/null +++ b/tinycloud-at-rest-escrow/tests/fixtures/NOTICE.md @@ -0,0 +1,12 @@ +`piv_p256_plugin_0_5_1.age` was produced by the **released** age-plugin-yubikey +0.5.1 (crates.io, built `--locked`) through stock age v1.3.2: + +```sh +printf 'tinycloud age-plugin-yubikey 0.5.1 piv-p256 vector' > msg.txt +age -e -a -r age1yubikey1qg3fps8csqk30gjdt552hf7jyjnmt753z9yhk4p8f0x5vvr5m53mju3vr5u -o piv_p256_plugin_0_5_1.age msg.txt +``` + +The recipient is a software TEST key, not a YubiKey. Its secret scalar is +`c2d3663dcc50dd691a502185a3f4de30f4c7ef664548414eca5c6a72790f36d2`. It exists +only so tests can check, with the plugin's own output, that the test identity +opens `piv-p256` stanzas exactly as the plugin does and rejects what it rejects. diff --git a/tinycloud-at-rest-escrow/tests/fixtures/piv_p256_plugin_0_5_1.age b/tinycloud-at-rest-escrow/tests/fixtures/piv_p256_plugin_0_5_1.age new file mode 100644 index 00000000..6b5c4df9 --- /dev/null +++ b/tinycloud-at-rest-escrow/tests/fixtures/piv_p256_plugin_0_5_1.age @@ -0,0 +1,8 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IDdsVFhqZyBBaTBpbDZP +YnpDMGxPRmcvcmI1RDZWR3djSnhUUlNuRGlkaEM4ZkVhVFBOTwo3SUloU2t4ZWpD +OFZma3EyQjJTa2lDOGZNRSswZkpqcFNWVDFkTXE5MExZCi0tLSBjb0YxZ00xR2pF +Q3haQXdkaG9USENmdXczenJEQVo3QzJPQ3VzZHFBNDBRCsysaVBs1OAcJyu783vZ +9JKQ2RG8pY9YQeVp+EDVABD87jtO34f56POVXml2gQxEc6Ul7X8KTknNhFybxyGs +SuZdvLe9xOCw11pec2n3IPjujt0= +-----END AGE ENCRYPTED FILE----- From f423ba91ab94cf13bb6f31dbe2f6124cb8ad82bc Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Sat, 17 Oct 2026 11:03:07 +0000 Subject: [PATCH 5/6] fix(TC-887): ASCII-only compose, shared strict reader, escaped output; review round 3 (#325) - The strict app-compose reader moves to tinycloud_at_rest_escrow::compose (yaml-rust2 =0.13.0), shared by the CLI and the node. - docker_compose_file may contain only newlines and printable ASCII: Unicode line breaks (U+0085, U+2028, U+2029), CR, tabs, BOM and control characters are refused before parsing. The review's four line-break PoCs are fixtures; the production compose is a test. - runner must be docker-compose. - literal_env: the escrow recipients and pin, if set, must be literal environment values (no interpolation or pass-through). - verify-compose prints the escrow literals and every other app-compose setting in full (pre_launch_script included); all untrusted text the CLI echoes is escaped (\u{..}), including REFUSED/ERROR lines. - docker-compose.dstack-postgres.yaml: an em dash in a comment replaced, so the production compose is ASCII-only. --- Cargo.lock | 38 +- docker-compose.dstack-postgres.yaml | 4 +- docs/at-rest-escrow.md | 9 +- tinycloud-at-rest-cli/Cargo.lock | 2 +- tinycloud-at-rest-cli/Cargo.toml | 1 - tinycloud-at-rest-cli/src/attest.rs | 25 +- tinycloud-at-rest-cli/src/compose.rs | 262 --------- tinycloud-at-rest-cli/src/main.rs | 33 +- tinycloud-at-rest-escrow/Cargo.toml | 3 + tinycloud-at-rest-escrow/src/compose.rs | 552 ++++++++++++++++++ tinycloud-at-rest-escrow/src/lib.rs | 3 + .../fixtures/compose/ls_comment_quote.yaml | 4 + .../fixtures/compose/ls_extra_service.yaml | 3 + .../fixtures/compose/nel_comment_quote.yaml | 4 + .../fixtures/compose/ps_comment_quote.yaml | 4 + 15 files changed, 657 insertions(+), 290 deletions(-) delete mode 100644 tinycloud-at-rest-cli/src/compose.rs create mode 100644 tinycloud-at-rest-escrow/src/compose.rs create mode 100644 tinycloud-at-rest-escrow/tests/fixtures/compose/ls_comment_quote.yaml create mode 100644 tinycloud-at-rest-escrow/tests/fixtures/compose/ls_extra_service.yaml create mode 100644 tinycloud-at-rest-escrow/tests/fixtures/compose/nel_comment_quote.yaml create mode 100644 tinycloud-at-rest-escrow/tests/fixtures/compose/ps_comment_quote.yaml diff --git a/Cargo.lock b/Cargo.lock index 65fd4545..7bef0741 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,6 +874,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayref" version = "0.3.9" @@ -4198,6 +4204,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + [[package]] name = "hashlink" version = "0.9.1" @@ -4216,6 +4231,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashlink" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "heapless" version = "0.8.0" @@ -10204,6 +10228,7 @@ dependencies = [ "subtle", "thiserror 2.0.18", "vsss-rs", + "yaml-rust2", "zeroize", ] @@ -10247,7 +10272,7 @@ dependencies = [ "futures", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "iri-string", "k256", "libp2p", @@ -11835,6 +11860,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "yaml-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b818a27a4cd30884ea380857a5e56f7ec3ba24a3990a3cc0b95af3238e18" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.12.2", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/docker-compose.dstack-postgres.yaml b/docker-compose.dstack-postgres.yaml index d661d530..73fae181 100644 --- a/docker-compose.dstack-postgres.yaml +++ b/docker-compose.dstack-postgres.yaml @@ -65,8 +65,8 @@ services: # entrypoint cannot work here) and a dstack deployment uploads only # this compose file (so there is no host path to bind-mount from). # - # Seal the base64 of the reviewed trust-bundle.production.json — the - # same bytes share-api serves — into the CVM's encrypted environment as + # Seal the base64 of the reviewed trust-bundle.production.json (the + # same bytes share-api serves) into the CVM's encrypted environment as # SHARE_TRUST_BUNDLE_BASE64. Same variable name and same wire form # share-api already uses, so one sealed value feeds both services and # cannot drift between them. diff --git a/docs/at-rest-escrow.md b/docs/at-rest-escrow.md index 0d2d53c8..908477e9 100644 --- a/docs/at-rest-escrow.md +++ b/docs/at-rest-escrow.md @@ -97,13 +97,16 @@ All of these must hold, or the command prints `REFUSED: …` and exits 1: Not pinned: `key-provider`, `instance-id`, `os-image-hash`, XFAM, MRCONFIGID, MROWNER and MROWNERCONFIG. RTMR0-2 and the compose hash cover what the per-boot, in-memory recovery key depends on. -`compose_hash` pins container images only if `app-compose.json` references every image by digest. `verify-compose` parses the compose YAML with a real parser and accepts only a small subset: +`compose_hash` pins container images only if `app-compose.json` references every image by digest. `verify-compose` reads it with a real parser (`tinycloud_at_rest_escrow::compose`, shared with the node) and accepts only a small subset: +- `runner` is `docker-compose`; +- the compose text is newlines and printable ASCII only, because YAML parsers disagree on Unicode line breaks such as U+2028; - exactly one document; - no anchors, aliases, merge keys, tags or duplicate keys; - a `services` mapping, and no `include`; -- every service has a string `image` of the form `[:tag]@sha256:<64 lowercase hex>`, with no `build` or `extends`. +- every service has a string `image` of the form `[:tag]@sha256:<64 lowercase hex>`, with no `build` or `extends`; +- `TINYCLOUD_AT_REST__ESCROW__RECIPIENTS` and `TINYCLOUD_AT_REST__ESCROW__VERIFIED_FP`, if set, are literal values: no `$` interpolation and no pass-through from the runtime environment. -Anything else is refused. Run it when building the trust policy. +Anything else is refused. `verify-compose` prints the escrow values the compose sets and every other app-compose setting in full (scripts included). All untrusted text is escaped, so control characters and terminal escapes show up as `\u{…}`. Run it when building the trust policy. A PCCS can't forge collateral, which is Intel-signed. A malicious PCCS can, however, serve older collateral that is still valid (TCB info is valid for about 30 days), which could hide a TCB recovery for that long. Use Intel PCS when that matters. diff --git a/tinycloud-at-rest-cli/Cargo.lock b/tinycloud-at-rest-cli/Cargo.lock index 5195516f..032f8656 100644 --- a/tinycloud-at-rest-cli/Cargo.lock +++ b/tinycloud-at-rest-cli/Cargo.lock @@ -3367,7 +3367,6 @@ dependencies = [ "sha2 0.10.9", "tinycloud-at-rest-escrow", "tokio", - "yaml-rust2", "zeroize", ] @@ -3389,6 +3388,7 @@ dependencies = [ "subtle", "thiserror 2.0.21", "vsss-rs", + "yaml-rust2", "zeroize", ] diff --git a/tinycloud-at-rest-cli/Cargo.toml b/tinycloud-at-rest-cli/Cargo.toml index 0916d2af..144498ec 100644 --- a/tinycloud-at-rest-cli/Cargo.toml +++ b/tinycloud-at-rest-cli/Cargo.toml @@ -31,7 +31,6 @@ zeroize = "1" # `self-test`: throwaway P-256 co-holders encoded as age1yubikey1. p256 = "0.13" bech32 = "0.11" -yaml-rust2 = "=0.13.0" [dev-dependencies] # Tests only: drive a genuine quote and collateral through every TCB status diff --git a/tinycloud-at-rest-cli/src/attest.rs b/tinycloud-at-rest-cli/src/attest.rs index eb0cbece..c3c7fa6c 100644 --- a/tinycloud-at-rest-cli/src/attest.rs +++ b/tinycloud-at-rest-cli/src/attest.rs @@ -23,6 +23,7 @@ use anyhow::{anyhow, bail, ensure, Context, Result}; use dcap_qvl::TcbStatus; use serde::Deserialize; use sha2::{Digest, Sha256, Sha384}; +use tinycloud_at_rest_escrow::compose::{escape, AppCompose, RECIPIENTS_ENV, VERIFIED_FP_ENV}; use tinycloud_at_rest_escrow::{ binding_digest, recovery_binding, validate_nonce, EscrowBundle, RecoveryInfo, }; @@ -236,17 +237,20 @@ pub fn require_attested(info: &RecoveryInfo) -> Result> { Ok(quote) } -/// What `verify-compose` found: each service's pinned image, plus the other -/// app-compose settings the hash covers. +/// What `verify-compose` found: each service's pinned image, the escrow +/// values the compose sets literally, and the other app-compose settings +/// the hash covers. All text is escaped for printing. #[derive(Debug)] pub struct ComposeReport { pub images: Vec<(String, String)>, - pub settings: Vec, + pub recipients: Option, + pub verified_fp: Option, + pub settings: Vec<(String, String)>, } /// `compose-hash` pins container images only if the compose file names each /// image by digest. Check `app-compose.json` against the trust policy's -/// `compose_hash`, then parse its compose YAML strictly (see `compose.rs`). +/// `compose_hash`, then read it strictly (`tinycloud_at_rest_escrow::compose`). pub fn check_app_compose(app_compose: &[u8], trust: &TrustPolicy) -> Result { let digest = hex::encode(Sha256::digest(app_compose)); ensure!( @@ -254,15 +258,12 @@ pub fn check_app_compose(app_compose: &[u8], trust: &TrustPolicy) -> Result[:tag]@sha256:<64 -//! lowercase hex>`, and no `build` or `extends`. - -use std::collections::HashSet; - -use anyhow::{anyhow, bail, ensure, Result}; -use sha2::Digest as _; -use yaml_rust2::parser::{Event, Parser}; - -#[derive(Debug)] -enum Node { - Scalar(String), - /// Items are validated (no aliases, anchors, tags) but not kept: no - /// check needs a sequence's contents. - Seq, - Map(Vec<(String, Node)>), -} - -struct Reader<'a> { - parser: Parser>, -} - -impl Reader<'_> { - fn next(&mut self) -> Result { - let (event, mark) = self - .parser - .next_token() - .map_err(|e| anyhow!("the compose file is not valid YAML: {e}"))?; - match &event { - Event::Alias(_) => bail!("YAML aliases are not allowed (line {})", mark.line()), - Event::Scalar(_, _, anchor, tag) - | Event::SequenceStart(anchor, tag) - | Event::MappingStart(anchor, tag) => { - ensure!( - *anchor == 0, - "YAML anchors are not allowed (line {})", - mark.line() - ); - ensure!( - tag.is_none(), - "YAML tags are not allowed (line {})", - mark.line() - ); - } - _ => {} - } - Ok(event) - } - - fn node(&mut self, event: Event) -> Result { - match event { - Event::Scalar(value, ..) => Ok(Node::Scalar(value)), - Event::SequenceStart(..) => loop { - match self.next()? { - Event::SequenceEnd => return Ok(Node::Seq), - e => { - self.node(e)?; - } - } - }, - Event::MappingStart(..) => { - let mut entries = Vec::new(); - let mut seen = HashSet::new(); - loop { - let key = match self.next()? { - Event::MappingEnd => return Ok(Node::Map(entries)), - Event::Scalar(key, ..) => key, - _ => bail!("only plain string mapping keys are allowed"), - }; - ensure!(key != "<<", "YAML merge keys are not allowed"); - ensure!(seen.insert(key.clone()), "duplicate key {key:?}"); - let value = self.next()?; - entries.push((key, self.node(value)?)); - } - } - other => bail!("unexpected YAML structure: {other:?}"), - } - } -} - -fn parse(yaml: &str) -> Result { - let mut r = Reader { - parser: Parser::new_from_str(yaml), - }; - ensure!(matches!(r.next()?, Event::StreamStart), "not a YAML stream"); - ensure!( - matches!(r.next()?, Event::DocumentStart), - "the compose file is empty" - ); - let first = r.next()?; - let root = r.node(first)?; - ensure!( - matches!(r.next()?, Event::DocumentEnd), - "trailing YAML content" - ); - ensure!( - matches!(r.next()?, Event::StreamEnd), - "the compose file must be a single YAML document" - ); - Ok(root) -} - -fn get<'a>(entries: &'a [(String, Node)], key: &str) -> Option<&'a Node> { - entries.iter().find(|(k, _)| k == key).map(|(_, v)| v) -} - -/// `[:tag]@sha256:<64 lowercase hex>`, no whitespace or comments. -fn pinned_reference(image: &str) -> bool { - let Some((name, digest)) = image.split_once("@sha256:") else { - return false; - }; - !name.is_empty() - && name - .bytes() - .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-' | b'/' | b':')) - && digest.len() == 64 - && digest - .bytes() - .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) -} - -/// Every service's image, each pinned by digest; refuses anything else. -pub fn pinned_images(compose_yaml: &str) -> Result> { - let Node::Map(top) = parse(compose_yaml)? else { - bail!("the compose file must be a mapping"); - }; - ensure!( - get(&top, "include").is_none(), - "`include` pulls in other compose files and is not allowed" - ); - let Some(Node::Map(services)) = get(&top, "services") else { - bail!("the compose file has no `services` mapping"); - }; - ensure!(!services.is_empty(), "the compose file has no services"); - let mut images = Vec::new(); - for (name, service) in services { - let Node::Map(fields) = service else { - bail!("service {name:?} is not a mapping"); - }; - for forbidden in ["build", "extends"] { - ensure!( - get(fields, forbidden).is_none(), - "service {name:?} uses `{forbidden}`; only digest-pinned images are allowed" - ); - } - let image = match get(fields, "image") { - Some(Node::Scalar(image)) => image, - Some(_) => bail!("service {name:?} has a non-string image"), - None => bail!("service {name:?} has no image"), - }; - ensure!( - pinned_reference(image), - "service {name:?} image {image:?} is not referenced as @sha256:<64 hex>" - ); - images.push((name.clone(), image.clone())); - } - Ok(images) -} - -/// The `app-compose.json` fields besides the compose file that the hash also -/// covers, for the holder to review. -pub fn other_settings(app_compose: &serde_json::Value) -> Vec { - let Some(map) = app_compose.as_object() else { - return Vec::new(); - }; - map.iter() - .filter(|(k, _)| k.as_str() != "docker_compose_file") - .map(|(k, v)| { - let shown = match v { - serde_json::Value::String(s) if s.len() > 80 => { - format!( - "<{} chars, sha256 {}>", - s.len(), - hex::encode(sha2::Sha256::digest(s.as_bytes())) - ) - } - other => other.to_string(), - }; - format!("{k} = {shown}") - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn digest() -> String { - "a".repeat(64) - } - - #[test] - fn digest_pinned_services_pass() { - let d = digest(); - let yaml = format!( - "services:\n node:\n image: ghcr.io/tinycloudlabs/tinycloud-node@sha256:{d}\n environment:\n - A=1\n ingress:\n image: \"caddy:2.8@sha256:{d}\"\nvolumes:\n data: {{}}\n" - ); - let images = pinned_images(&yaml).unwrap(); - assert_eq!(images.len(), 2); - assert_eq!(images[1].1, format!("caddy:2.8@sha256:{d}")); - } - - #[test] - fn every_known_bypass_is_refused() { - let d = digest(); - let cases = [ - // 1. A comment is not part of the value: the image is nginx:latest. - (format!("services:\n web:\n image: nginx:latest # @sha256:{d}\n"), "nginx:latest"), - // 2. Flow mapping. - ("services:\n web: {image: nginx:latest}\n".to_string(), "nginx:latest"), - // 3. Quoted key. - ("services:\n web:\n \"image\": nginx:latest\n".to_string(), "nginx:latest"), - // 4. build instead of an image. - ("services:\n web:\n build: .\n".to_string(), "build"), - // Anchors, aliases and merges that hide the real image. - (format!("x: &img\n image: a@sha256:{d}\nservices:\n web:\n <<: *img\n image: nginx\n"), "anchor"), - (format!("services:\n a:\n image: &i a@sha256:{d}\n b:\n image: *i\n"), "anchor"), - // Duplicate keys: last-wins parsers would run the second. - (format!("services:\n web:\n image: a@sha256:{d}\n image: nginx\n"), "duplicate"), - // Tags, extends, include, tag-only, digest of the wrong length/case. - (format!("services:\n web:\n image: !!str a@sha256:{d}\n"), "tags"), - (format!("services:\n web:\n image: a@sha256:{d}\n extends: base\n"), "extends"), - (format!("include:\n - other.yaml\nservices:\n web:\n image: a@sha256:{d}\n"), "include"), - ("services:\n web:\n image: nginx:1.27\n".to_string(), "nginx:1.27"), - (format!("services:\n web:\n image: a@sha256:{}\n", "A".repeat(64)), "is not referenced"), - (format!("services:\n web:\n image: a@sha256:{d}0\n"), "is not referenced"), - ("services:\n web:\n environment: [A=1]\n".to_string(), "no image"), - // Several documents, or no services. - (format!("services:\n web:\n image: a@sha256:{d}\n---\nservices: {{}}\n"), "single YAML document"), - ("version: '3'\n".to_string(), "no `services`"), - ("services: [\n".to_string(), "not valid YAML"), - ]; - for (yaml, needle) in cases { - let err = pinned_images(&yaml).expect_err(&yaml).to_string(); - assert!(err.contains(needle), "{yaml:?}: {err}"); - } - } - - #[test] - fn other_settings_are_listed_for_review() { - let doc = serde_json::json!({ - "docker_compose_file": "services: {}", - "kms_enabled": true, - "pre_launch_script": "x".repeat(100), - }); - let lines = other_settings(&doc); - assert!(lines.iter().any(|l| l == "kms_enabled = true")); - assert!(lines - .iter() - .any(|l| l.starts_with("pre_launch_script = <100 chars"))); - assert!(!lines.iter().any(|l| l.starts_with("docker_compose_file"))); - } -} diff --git a/tinycloud-at-rest-cli/src/main.rs b/tinycloud-at-rest-cli/src/main.rs index 52302e2b..5053170c 100644 --- a/tinycloud-at-rest-cli/src/main.rs +++ b/tinycloud-at-rest-cli/src/main.rs @@ -9,7 +9,6 @@ #![forbid(unsafe_code)] mod attest; -mod compose; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; @@ -17,6 +16,7 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Result}; use base64::Engine; use clap::{Args, Parser, Subcommand}; +use tinycloud_at_rest_escrow::compose::escape; use tinycloud_at_rest_escrow::{ validate_holders, wrap_share, Ark, ArkId, EscrowBundle, HolderRecipient, RecipientKind, RecipientPolicy, RecoveryInfo, VerificationRecord, @@ -273,7 +273,8 @@ async fn verify_node(args: &NodeArgs) -> Result<(EscrowBundle, RecoveryInfo)> { eprintln!(" {}", attest::collateral_dates(&collateral)); eprintln!( " MRTD, RTMR0-2, app-id and compose-hash match the trust policy; recovery key {} is bound to your nonce and escrow {}", - info.recovery_recipient, info.efp + escape(&info.recovery_recipient), + info.efp ); eprintln!( " node state: {} ({}); shares it holds this boot: {:?}", @@ -463,9 +464,22 @@ async fn run(cli: Cli) -> Result<()> { for (service, image) in report.images { eprintln!(" {service}: {image}"); } - eprintln!("Other settings the hash covers (review them):"); - for line in report.settings { - eprintln!(" {line}"); + eprintln!("At-rest escrow values set literally in the compose (compare with the bundle and the pin):"); + for (key, value) in [ + ( + tinycloud_at_rest_escrow::compose::RECIPIENTS_ENV, + report.recipients, + ), + ( + tinycloud_at_rest_escrow::compose::VERIFIED_FP_ENV, + report.verified_fp, + ), + ] { + eprintln!(" {key} = {}", value.as_deref().unwrap_or("(not set)")); + } + eprintln!("Other settings the hash covers, in full (review them):"); + for (key, value) in report.settings { + eprintln!(" {key} = {value}"); } } Command::VerifyNode(args) => { @@ -489,7 +503,10 @@ async fn run(cli: Cli) -> Result<()> { record.covers(&bundle)?; eprintln!( "NOT AUTHENTICATED: this record names escrow {} and claims shares {:?} and pairs {:?} (verified_at {}),", - record.efp, record.verified_indices, record.pairs, record.verified_at + record.efp, + record.verified_indices, + record.pairs, + escape(&record.verified_at) ); eprintln!( "but its MAC can only be checked inside the TEE. Pin this value only after you saw the node answer \ @@ -509,10 +526,10 @@ fn main() { .expect("tokio runtime"); if let Err(e) = runtime.block_on(run(cli)) { if e.downcast_ref::().is_some() { - eprintln!("ERROR: {e:#}"); + eprintln!("ERROR: {}", escape(&format!("{e:#}"))); std::process::exit(3); } - eprintln!("REFUSED: {e:#}"); + eprintln!("REFUSED: {}", escape(&format!("{e:#}"))); std::process::exit(1); } } diff --git a/tinycloud-at-rest-escrow/Cargo.toml b/tinycloud-at-rest-escrow/Cargo.toml index d343522f..a96496e9 100644 --- a/tinycloud-at-rest-escrow/Cargo.toml +++ b/tinycloud-at-rest-escrow/Cargo.toml @@ -23,6 +23,9 @@ serde = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } thiserror = { workspace = true } +# Strict app-compose reading (src/compose.rs); pinned: what it accepts is a +# security property. +yaml-rust2 = "=0.13.0" [dev-dependencies] # Software P-256 identities that open `p256tag` and `piv-p256` stanzas, so diff --git a/tinycloud-at-rest-escrow/src/compose.rs b/tinycloud-at-rest-escrow/src/compose.rs new file mode 100644 index 00000000..e1da298a --- /dev/null +++ b/tinycloud-at-rest-escrow/src/compose.rs @@ -0,0 +1,552 @@ +//! What a dstack `app-compose.json` pins, read strictly. +//! +//! dstack's `compose_hash` is SHA-256 of `app-compose.json`, which embeds the +//! docker-compose YAML. Two things depend on reading that YAML exactly as +//! docker compose does: +//! +//! - the holders' `verify-compose`: `compose_hash` pins the images only if +//! every service names its image by digest; +//! - the node in a TEE: the escrow recipients and the pinned fingerprint +//! must be literal values in the measured compose. +//! +//! Only a deliberately small subset is accepted, so no YAML feature, and no +//! difference between this parser and docker compose's, can hide anything: +//! +//! - the compose text is `\n` plus printable ASCII only (YAML parsers +//! disagree on Unicode line breaks such as U+2028); +//! - one document; no anchors, aliases, merge keys, tags or duplicate keys; +//! - a top-level `services` mapping, and no `include`; +//! - `runner` is `docker-compose`. + +use std::collections::HashSet; + +use yaml_rust2::parser::{Event, Parser}; + +use crate::EscrowError; + +/// The compose environment variables that must be literal in a TEE. +pub const RECIPIENTS_ENV: &str = "TINYCLOUD_AT_REST__ESCROW__RECIPIENTS"; +pub const VERIFIED_FP_ENV: &str = "TINYCLOUD_AT_REST__ESCROW__VERIFIED_FP"; +type Result = std::result::Result; + +fn err(msg: impl Into) -> Result { + Err(EscrowError::Compose(msg.into())) +} + +macro_rules! ensure { + ($cond:expr, $($fmt:tt)+) => { + if !$cond { + return err(format!($($fmt)+)); + } + }; +} + +/// Untrusted text, made safe to print: printable ASCII stays, everything +/// else (control characters, terminal escapes, any non-ASCII) becomes a +/// visible `\u{…}` escape. +pub fn escape(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + ' '..='~' if c != '\\' => out.push(c), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + other => out.push_str(&format!("\\u{{{:x}}}", other as u32)), + } + } + out +} + +#[derive(Debug)] +enum Node { + Scalar(String), + Seq(Vec), + Map(Vec<(String, Node)>), +} + +struct Reader<'a> { + parser: Parser>, +} + +impl Reader<'_> { + fn next(&mut self) -> Result { + let (event, mark) = self.parser.next_token().map_err(|e| { + EscrowError::Compose(format!("the compose file is not valid YAML: {e}")) + })?; + match &event { + Event::Alias(_) => { + return err(format!( + "YAML aliases are not allowed (line {})", + mark.line() + )) + } + Event::Scalar(_, _, anchor, tag) + | Event::SequenceStart(anchor, tag) + | Event::MappingStart(anchor, tag) => { + ensure!( + *anchor == 0, + "YAML anchors are not allowed (line {})", + mark.line() + ); + ensure!( + tag.is_none(), + "YAML tags are not allowed (line {})", + mark.line() + ); + } + _ => {} + } + Ok(event) + } + + fn node(&mut self, event: Event) -> Result { + match event { + Event::Scalar(value, ..) => Ok(Node::Scalar(value)), + Event::SequenceStart(..) => { + let mut items = Vec::new(); + loop { + match self.next()? { + Event::SequenceEnd => return Ok(Node::Seq(items)), + e => items.push(self.node(e)?), + } + } + } + Event::MappingStart(..) => { + let mut entries = Vec::new(); + let mut seen = HashSet::new(); + loop { + let key = match self.next()? { + Event::MappingEnd => return Ok(Node::Map(entries)), + Event::Scalar(key, ..) => key, + _ => return err("only plain string mapping keys are allowed"), + }; + ensure!(key != "<<", "YAML merge keys are not allowed"); + ensure!(seen.insert(key.clone()), "duplicate key {:?}", escape(&key)); + let value = self.next()?; + entries.push((key, self.node(value)?)); + } + } + other => err(format!("unexpected YAML structure: {other:?}")), + } + } +} + +/// Only `\n` and printable ASCII: no Unicode line breaks (U+0085, U+2028, +/// U+2029), no CR, tabs, BOM or control characters. +fn check_charset(yaml: &str) -> Result<()> { + if let Some((i, b)) = yaml + .bytes() + .enumerate() + .find(|(_, b)| *b != b'\n' && !(0x20..=0x7e).contains(b)) + { + return err(format!( + "the compose file may contain only newlines and printable ASCII; byte 0x{b:02x} at offset {i} is not allowed" + )); + } + Ok(()) +} + +fn parse(yaml: &str) -> Result> { + check_charset(yaml)?; + let mut r = Reader { + parser: Parser::new_from_str(yaml), + }; + ensure!(matches!(r.next()?, Event::StreamStart), "not a YAML stream"); + ensure!( + matches!(r.next()?, Event::DocumentStart), + "the compose file is empty" + ); + let first = r.next()?; + let root = r.node(first)?; + ensure!( + matches!(r.next()?, Event::DocumentEnd), + "trailing YAML content" + ); + ensure!( + matches!(r.next()?, Event::StreamEnd), + "the compose file must be a single YAML document" + ); + let Node::Map(top) = root else { + return err("the compose file must be a mapping"); + }; + ensure!( + get(&top, "include").is_none(), + "`include` pulls in other compose files and is not allowed" + ); + Ok(top) +} + +fn get<'a>(entries: &'a [(String, Node)], key: &str) -> Option<&'a Node> { + entries.iter().find(|(k, _)| k == key).map(|(_, v)| v) +} + +/// A YAML mapping's entries, in document order. +type Entries = [(String, Node)]; + +fn services(top: &Entries) -> Result> { + let Some(Node::Map(services)) = get(top, "services") else { + return err("the compose file has no `services` mapping"); + }; + ensure!(!services.is_empty(), "the compose file has no services"); + services + .iter() + .map(|(name, service)| match service { + Node::Map(fields) => Ok((name.as_str(), fields.as_slice())), + _ => err(format!("service {:?} is not a mapping", escape(name))), + }) + .collect() +} + +/// `[:tag]@sha256:<64 lowercase hex>`, no whitespace or comments. +fn pinned_reference(image: &str) -> bool { + let Some((name, digest)) = image.split_once("@sha256:") else { + return false; + }; + !name.is_empty() + && name + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-' | b'/' | b':')) + && digest.len() == 64 + && digest + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) +} + +/// The parsed `app-compose.json` and its compose YAML. +pub struct AppCompose { + doc: serde_json::Map, + top: Vec<(String, Node)>, +} + +impl AppCompose { + /// Parse `app-compose.json` and its `docker_compose_file` strictly. + pub fn parse(app_compose: &[u8]) -> Result { + let doc: serde_json::Value = serde_json::from_slice(app_compose) + .map_err(|e| EscrowError::Compose(format!("app-compose.json is not JSON: {e}")))?; + let serde_json::Value::Object(doc) = doc else { + return err("app-compose.json is not a JSON object"); + }; + match doc.get("runner").and_then(|v| v.as_str()) { + Some("docker-compose") => {} + other => { + return err(format!( + "app-compose.json runner must be \"docker-compose\", not {:?}", + other.map(escape) + )) + } + } + let compose = doc + .get("docker_compose_file") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + EscrowError::Compose("app-compose.json has no docker_compose_file".into()) + })?; + let top = parse(compose)?; + services(&top)?; + Ok(Self { doc, top }) + } + + /// Every service's image, each pinned by digest; refuses anything else. + pub fn pinned_images(&self) -> Result> { + let mut images = Vec::new(); + for (name, fields) in services(&self.top)? { + let name = escape(name); + for forbidden in ["build", "extends"] { + ensure!( + get(fields, forbidden).is_none(), + "service {name:?} uses `{forbidden}`; only digest-pinned images are allowed" + ); + } + let image = match get(fields, "image") { + Some(Node::Scalar(image)) => image, + Some(_) => return err(format!("service {name:?} has a non-string image")), + None => return err(format!("service {name:?} has no image")), + }; + ensure!( + pinned_reference(image), + "service {name:?} image {:?} is not referenced as @sha256:<64 hex>", + escape(image) + ); + images.push((name, image.clone())); + } + Ok(images) + } + + /// The other `app-compose.json` fields the hash also covers, in full + /// (scripts included), escaped, for the holder to review. + pub fn other_settings(&self) -> Vec<(String, String)> { + self.doc + .iter() + .filter(|(k, _)| k.as_str() != "docker_compose_file") + .map(|(k, v)| { + let shown = match v { + serde_json::Value::String(s) => escape(s), + other => escape(&other.to_string()), + }; + (escape(k), shown) + }) + .collect() + } + + /// The literal value of `key` in the services' `environment`, if set. + /// + /// Refused: a value that docker compose would interpolate (`$`), a key + /// passed through from the runtime environment (`- KEY` or `KEY:` + /// with no value), and different values in different services. + pub fn literal_env(&self, key: &str) -> Result> { + let mut found: Option = None; + for (service, fields) in services(&self.top)? { + let service = escape(service); + let value = match get(fields, "environment") { + None => None, + Some(Node::Map(env)) => match get(env, key) { + None => None, + Some(Node::Scalar(v)) => Some(v.clone()), + Some(_) => { + return err(format!("service {service:?} sets {key} to a non-string")) + } + }, + Some(Node::Seq(items)) => { + let mut value = None; + for item in items { + let Node::Scalar(item) = item else { + return err(format!( + "service {service:?} has a non-string environment entry" + )); + }; + let (k, v) = match item.split_once('=') { + Some((k, v)) => (k, Some(v)), + None => (item.as_str(), None), + }; + if k == key { + ensure!(value.is_none(), "service {service:?} sets {key} twice"); + value = Some(v.map(str::to_owned).ok_or_else(|| { + EscrowError::Compose(format!( + "service {service:?} passes {key} through from the runtime environment; it must be a literal value" + )) + })?); + } + } + value + } + Some(_) => { + return err(format!( + "service {service:?} environment is not a mapping or list" + )) + } + }; + let Some(value) = value else { continue }; + ensure!( + !value.is_empty() && value != "~" && value != "null", + "service {service:?} passes {key} through from the runtime environment; it must be a literal value" + ); + ensure!( + !value.contains('$'), + "service {service:?} sets {key} by interpolation; it must be a literal value" + ); + match &found { + Some(prev) => ensure!(*prev == value, "services set different values for {key}"), + None => found = Some(value), + } + } + Ok(found) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn digest() -> String { + "a".repeat(64) + } + + fn app(yaml: &str) -> Vec { + serde_json::to_vec(&serde_json::json!({ + "manifest_version": 2, + "runner": "docker-compose", + "docker_compose_file": yaml, + })) + .unwrap() + } + + fn images(yaml: &str) -> Result> { + AppCompose::parse(&app(yaml))?.pinned_images() + } + + #[test] + fn digest_pinned_services_pass() { + let d = digest(); + let yaml = format!( + "services:\n node:\n image: ghcr.io/tinycloudlabs/tinycloud-node@sha256:{d}\n environment:\n - A=1\n ingress:\n image: \"caddy:2.8@sha256:{d}\"\nvolumes:\n data: {{}}\n" + ); + let images = images(&yaml).unwrap(); + assert_eq!(images.len(), 2); + assert_eq!(images[1].1, format!("caddy:2.8@sha256:{d}")); + } + + #[test] + fn every_known_bypass_is_refused() { + let d = digest(); + let cases = [ + // A comment is not part of the value: the image is nginx:latest. + (format!("services:\n web:\n image: nginx:latest # @sha256:{d}\n"), "nginx:latest"), + // Flow mapping. + ("services:\n web: {image: nginx:latest}\n".to_string(), "nginx:latest"), + // Quoted key. + ("services:\n web:\n \"image\": nginx:latest\n".to_string(), "nginx:latest"), + // build instead of an image. + ("services:\n web:\n build: .\n".to_string(), "build"), + // Anchors, aliases and merges that hide the real image. + (format!("x: &img\n image: a@sha256:{d}\nservices:\n web:\n <<: *img\n image: nginx\n"), "anchor"), + (format!("services:\n a:\n image: &i a@sha256:{d}\n b:\n image: *i\n"), "anchor"), + // Duplicate keys: last-wins parsers would run the second. + (format!("services:\n web:\n image: a@sha256:{d}\n image: nginx\n"), "duplicate"), + // Tags, extends, include, tag-only, digest of the wrong length/case. + (format!("services:\n web:\n image: !!str a@sha256:{d}\n"), "tags"), + (format!("services:\n web:\n image: a@sha256:{d}\n extends: base\n"), "extends"), + (format!("include:\n - other.yaml\nservices:\n web:\n image: a@sha256:{d}\n"), "include"), + ("services:\n web:\n image: nginx:1.27\n".to_string(), "nginx:1.27"), + (format!("services:\n web:\n image: a@sha256:{}\n", "A".repeat(64)), "is not referenced"), + (format!("services:\n web:\n image: a@sha256:{d}0\n"), "is not referenced"), + ("services:\n web:\n environment: [A=1]\n".to_string(), "no image"), + // Several documents, or no services. + (format!("services:\n web:\n image: a@sha256:{d}\n---\nservices: {{}}\n"), "single YAML document"), + ("version: '3'\n".to_string(), "no `services`"), + ("services: [\n".to_string(), "not valid YAML"), + // Anything but newline and printable ASCII. + (format!("services:\r\n web:\r\n image: a@sha256:{d}\r\n"), "0x0d"), + (format!("\u{feff}services:\n web:\n image: a@sha256:{d}\n"), "0xef"), + (format!("services:\n\tweb:\n image: a@sha256:{d}\n"), "0x09"), + ]; + for (yaml, needle) in cases { + let err = images(&yaml).expect_err(&yaml).to_string(); + assert!(err.contains(needle), "{yaml:?}: {err}"); + } + } + + /// The security review's proofs of concept: Unicode line breaks that Go + /// yaml.v3 (docker compose) honours and yaml-rust2 doesn't, so each one + /// runs `nginx:latest` while a lax reader sees only the pinned image. + #[test] + fn unicode_line_breaks_are_refused() { + for (name, yaml) in [ + ( + "U+2028", + include_str!("../tests/fixtures/compose/ls_comment_quote.yaml"), + ), + ( + "U+0085", + include_str!("../tests/fixtures/compose/nel_comment_quote.yaml"), + ), + ( + "U+2029", + include_str!("../tests/fixtures/compose/ps_comment_quote.yaml"), + ), + ( + "hidden service", + include_str!("../tests/fixtures/compose/ls_extra_service.yaml"), + ), + ] { + assert!(!yaml.is_ascii(), "{name}: fixture lost its line break"); + let err = images(yaml).expect_err(name).to_string(); + assert!(err.contains("printable ASCII"), "{name}: {err}"); + } + } + + #[test] + fn production_compose_passes() { + let yaml = include_str!("../../docker-compose.dstack-postgres.yaml"); + assert_eq!(images(yaml).unwrap().len(), 4); + } + + #[test] + fn only_the_docker_compose_runner_is_accepted() { + let d = digest(); + let yaml = format!("services:\n web:\n image: a@sha256:{d}\n"); + for runner in [serde_json::json!("bash"), serde_json::Value::Null] { + let doc = serde_json::json!({"runner": runner, "docker_compose_file": yaml}); + let err = AppCompose::parse(&serde_json::to_vec(&doc).unwrap()) + .err() + .unwrap() + .to_string(); + assert!(err.contains("runner"), "{err}"); + } + } + + #[test] + fn other_settings_are_shown_in_full_and_escaped() { + let script = format!("docker pull nginx:latest && {}", "x".repeat(200)); + let doc = serde_json::json!({ + "runner": "docker-compose", + "docker_compose_file": format!("services:\n w:\n image: a@sha256:{}\n", digest()), + "kms_enabled": true, + "pre_launch_script": script, + "zz\u{1b}[1A\u{1b}[2K": "x", + }); + let parsed = AppCompose::parse(&serde_json::to_vec(&doc).unwrap()).unwrap(); + let settings = parsed.other_settings(); + assert!(settings.contains(&("kms_enabled".into(), "true".into()))); + assert!(settings.contains(&("pre_launch_script".into(), script.clone()))); + assert!(settings.contains(&("zz\\u{1b}[1A\\u{1b}[2K".into(), "x".into()))); + assert!(!settings.iter().any(|(k, _)| k == "docker_compose_file")); + assert!(settings.iter().all(|(k, v)| k + .bytes() + .chain(v.bytes()) + .all(|b| (0x20..=0x7e).contains(&b)))); + } + + #[test] + fn escrow_values_must_be_literal() { + let d = digest(); + let with_env = |env: &str| { + AppCompose::parse(&app(&format!( + "services:\n node:\n image: a@sha256:{d}\n environment:\n{env}" + ))) + .unwrap() + }; + let r = "'[{holder=\"sam\",recipient=\"age1yubikey1q\"}]'"; + // Map and list forms give the literal. + let map = with_env(&format!( + " {RECIPIENTS_ENV}: {r}\n {VERIFIED_FP_ENV}: {d}\n" + )); + assert_eq!( + map.literal_env(RECIPIENTS_ENV).unwrap().as_deref(), + Some("[{holder=\"sam\",recipient=\"age1yubikey1q\"}]") + ); + assert_eq!(map.literal_env(VERIFIED_FP_ENV).unwrap(), Some(d.clone())); + let list = with_env(&format!(" - {VERIFIED_FP_ENV}={d}\n")); + assert_eq!(list.literal_env(VERIFIED_FP_ENV).unwrap(), Some(d.clone())); + assert_eq!(list.literal_env(RECIPIENTS_ENV).unwrap(), None); + // Interpolated or passed through from the runtime environment. + for env in [ + format!(" {VERIFIED_FP_ENV}: ${{FP}}\n"), + format!(" {VERIFIED_FP_ENV}: $FP\n"), + format!(" {VERIFIED_FP_ENV}:\n"), + format!(" - {VERIFIED_FP_ENV}\n"), + format!(" - {VERIFIED_FP_ENV}=${{FP:-{d}}}\n"), + ] { + let err = with_env(&env) + .literal_env(VERIFIED_FP_ENV) + .expect_err(&env) + .to_string(); + assert!(err.contains("literal"), "{env:?}: {err}"); + } + // Two services that disagree. + let two = AppCompose::parse(&app(&format!( + "services:\n a:\n image: a@sha256:{d}\n environment:\n {VERIFIED_FP_ENV}: {d}\n b:\n image: a@sha256:{d}\n environment:\n {VERIFIED_FP_ENV}: {}\n", + "b".repeat(64) + ))) + .unwrap(); + assert!(two.literal_env(VERIFIED_FP_ENV).is_err()); + } + + #[test] + fn escape_makes_untrusted_text_printable() { + assert_eq!(escape("ok -_./:@"), "ok -_./:@"); + assert_eq!( + escape("\u{1b}[2K\r\u{2028}\\"), + "\\u{1b}[2K\\u{d}\\u{2028}\\\\" + ); + } +} diff --git a/tinycloud-at-rest-escrow/src/lib.rs b/tinycloud-at-rest-escrow/src/lib.rs index b65d89b9..0600f8ca 100644 --- a/tinycloud-at-rest-escrow/src/lib.rs +++ b/tinycloud-at-rest-escrow/src/lib.rs @@ -18,6 +18,7 @@ #![forbid(unsafe_code)] mod bundle; +pub mod compose; mod piv; mod recipients; mod record; @@ -83,6 +84,8 @@ pub enum EscrowError { Record(&'static str), #[error("recovery binding: {0}")] Binding(&'static str), + #[error("app-compose: {0}")] + Compose(String), } /// The at-rest root key. Zeroized on drop; `Debug` never prints it. diff --git a/tinycloud-at-rest-escrow/tests/fixtures/compose/ls_comment_quote.yaml b/tinycloud-at-rest-escrow/tests/fixtures/compose/ls_comment_quote.yaml new file mode 100644 index 00000000..4914cd97 --- /dev/null +++ b/tinycloud-at-rest-escrow/tests/fixtures/compose/ls_comment_quote.yaml @@ -0,0 +1,4 @@ +services: + web: #
 image: nginx:latest
 x-hide: ' + image: ghcr.io/x/node@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + #' diff --git a/tinycloud-at-rest-escrow/tests/fixtures/compose/ls_extra_service.yaml b/tinycloud-at-rest-escrow/tests/fixtures/compose/ls_extra_service.yaml new file mode 100644 index 00000000..e0a5f79e --- /dev/null +++ b/tinycloud-at-rest-escrow/tests/fixtures/compose/ls_extra_service.yaml @@ -0,0 +1,3 @@ +services: + web: + image: ghcr.io/x/node@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa #
 evil:
 image: nginx:latest diff --git a/tinycloud-at-rest-escrow/tests/fixtures/compose/nel_comment_quote.yaml b/tinycloud-at-rest-escrow/tests/fixtures/compose/nel_comment_quote.yaml new file mode 100644 index 00000000..5a91bc22 --- /dev/null +++ b/tinycloud-at-rest-escrow/tests/fixtures/compose/nel_comment_quote.yaml @@ -0,0 +1,4 @@ +services: + web: #… image: nginx:latest… x-hide: ' + image: ghcr.io/x/node@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + #' diff --git a/tinycloud-at-rest-escrow/tests/fixtures/compose/ps_comment_quote.yaml b/tinycloud-at-rest-escrow/tests/fixtures/compose/ps_comment_quote.yaml new file mode 100644 index 00000000..31766e16 --- /dev/null +++ b/tinycloud-at-rest-escrow/tests/fixtures/compose/ps_comment_quote.yaml @@ -0,0 +1,4 @@ +services: + web: #
 image: nginx:latest
 x-hide: ' + image: ghcr.io/x/node@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + #' From a0d57fa8fa0a59d9c1abe2006bdd0fc4860a62dd Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Sat, 17 Oct 2026 13:05:41 +0000 Subject: [PATCH 6/6] fix(TC-887): refuse duplicate app-compose keys and every YAML null spelling (#325) - app-compose.json with an object key repeated at any depth is refused (serde_json would keep the last; dstack's struct parse refuses it). - Null, NULL, null, ~ and empty escrow values are pass-through, refused explicitly rather than failing later. --- tinycloud-at-rest-escrow/src/compose.rs | 100 +++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/tinycloud-at-rest-escrow/src/compose.rs b/tinycloud-at-rest-escrow/src/compose.rs index e1da298a..e6a8f9df 100644 --- a/tinycloud-at-rest-escrow/src/compose.rs +++ b/tinycloud-at-rest-escrow/src/compose.rs @@ -218,9 +218,81 @@ pub struct AppCompose { top: Vec<(String, Node)>, } +/// Walks any JSON value and fails on an object key that appears twice at +/// any depth. serde_json keeps the last duplicate; dstack's own struct +/// parse would refuse it, so every reader of the hashed bytes must too. +struct NoDuplicateKeys; + +impl<'de> serde::Deserialize<'de> for NoDuplicateKeys { + fn deserialize>(d: D) -> std::result::Result { + d.deserialize_any(NoDuplicateKeysVisitor) + } +} + +struct NoDuplicateKeysVisitor; + +impl<'de> serde::de::Visitor<'de> for NoDuplicateKeysVisitor { + type Value = NoDuplicateKeys; + + fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("a JSON value") + } + + fn visit_bool(self, _: bool) -> std::result::Result { + Ok(NoDuplicateKeys) + } + + fn visit_i64(self, _: i64) -> std::result::Result { + Ok(NoDuplicateKeys) + } + + fn visit_u64(self, _: u64) -> std::result::Result { + Ok(NoDuplicateKeys) + } + + fn visit_f64(self, _: f64) -> std::result::Result { + Ok(NoDuplicateKeys) + } + + fn visit_str(self, _: &str) -> std::result::Result { + Ok(NoDuplicateKeys) + } + + fn visit_unit(self) -> std::result::Result { + Ok(NoDuplicateKeys) + } + + fn visit_seq>( + self, + mut seq: A, + ) -> std::result::Result { + while seq.next_element::()?.is_some() {} + Ok(NoDuplicateKeys) + } + + fn visit_map>( + self, + mut map: A, + ) -> std::result::Result { + let mut seen = HashSet::new(); + while let Some(key) = map.next_key::()? { + if !seen.insert(key.clone()) { + return Err(serde::de::Error::custom(format!( + "duplicate key {:?}", + escape(&key) + ))); + } + map.next_value::()?; + } + Ok(NoDuplicateKeys) + } +} + impl AppCompose { /// Parse `app-compose.json` and its `docker_compose_file` strictly. pub fn parse(app_compose: &[u8]) -> Result { + serde_json::from_slice::(app_compose) + .map_err(|e| EscrowError::Compose(format!("app-compose.json is not JSON: {e}")))?; let doc: serde_json::Value = serde_json::from_slice(app_compose) .map_err(|e| EscrowError::Compose(format!("app-compose.json is not JSON: {e}")))?; let serde_json::Value::Object(doc) = doc else { @@ -336,8 +408,10 @@ impl AppCompose { } }; let Some(value) = value else { continue }; + // Empty or a YAML 1.2 null (any spelling docker compose reads as + // null) passes the variable through from the runtime. ensure!( - !value.is_empty() && value != "~" && value != "null", + !matches!(value.as_str(), "" | "~" | "null" | "Null" | "NULL"), "service {service:?} passes {key} through from the runtime environment; it must be a literal value" ); ensure!( @@ -460,6 +534,26 @@ mod tests { assert_eq!(images(yaml).unwrap().len(), 4); } + #[test] + fn duplicate_json_keys_are_refused_at_any_depth() { + let yaml = format!("services:\\n w:\\n image: a@sha256:{}\\n", digest()); + for doc in [ + format!( + r#"{{"runner":"docker-compose","runner":"bash","docker_compose_file":"{yaml}"}}"# + ), + format!( + r#"{{"runner":"docker-compose","docker_compose_file":"{yaml}","x":{{"a":1,"a":2}}}}"# + ), + ] { + let err = AppCompose::parse(doc.as_bytes()).err().unwrap().to_string(); + assert!(err.contains("duplicate key"), "{doc}: {err}"); + } + let ok = format!( + r#"{{"runner":"docker-compose","docker_compose_file":"{yaml}","x":[{{"a":1}},{{"a":2}}]}}"# + ); + AppCompose::parse(ok.as_bytes()).unwrap(); + } + #[test] fn only_the_docker_compose_runner_is_accepted() { let d = digest(); @@ -523,6 +617,10 @@ mod tests { format!(" {VERIFIED_FP_ENV}: ${{FP}}\n"), format!(" {VERIFIED_FP_ENV}: $FP\n"), format!(" {VERIFIED_FP_ENV}:\n"), + format!(" {VERIFIED_FP_ENV}: Null\n"), + format!(" {VERIFIED_FP_ENV}: NULL\n"), + format!(" {VERIFIED_FP_ENV}: null\n"), + format!(" {VERIFIED_FP_ENV}: ~\n"), format!(" - {VERIFIED_FP_ENV}\n"), format!(" - {VERIFIED_FP_ENV}=${{FP:-{d}}}\n"), ] {