From 99460e3c5a17df19b66c6f4d987fe8a7e9ed1fa9 Mon Sep 17 00:00:00 2001 From: Annie Ke Date: Mon, 6 Apr 2026 17:09:38 -0700 Subject: [PATCH 1/2] add oci-client for locally pulling images --- Cargo.lock | 726 ++++++++++++++++++++++++++++++++++++++----------- Cargo.toml | 7 +- tvc/Cargo.toml | 1 + 3 files changed, 574 insertions(+), 160 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5bbebf39..af88dba6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,21 +201,21 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attestation-doc-validation" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c74d251482367930e866977b88581824c76f27aff8105e808777f4f234e0b04" +checksum = "cb740cfcf6c1167edf66243e2e81681abe61d0698ac54f26ab6787990ffd7aa2" dependencies = [ "aes", "aes-gcm", "aws-nitro-enclaves-cose", - "aws-nitro-enclaves-nsm-api 0.4.0", + "aws-nitro-enclaves-nsm-api", "base64 0.21.7", "chrono", - "der 0.6.1", - "ecdsa 0.15.1", + "der 0.7.9", + "ecdsa", "hex", - "p256 0.12.0", - "p384 0.12.0", + "p256 0.13.2", + "p384", "rand 0.8.5", "serde", "serde_bytes", @@ -234,30 +234,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] -name = "aws-nitro-enclaves-cose" -version = "0.5.2" +name = "aws-lc-rs" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a94047bd9c3717c6ca3a145504c0e26b64a5e2d9eb9559b187748433fbc382" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ - "serde", - "serde_bytes", - "serde_cbor", - "serde_repr", - "serde_with 3.14.0", + "aws-lc-sys", + "zeroize", ] [[package]] -name = "aws-nitro-enclaves-nsm-api" -version = "0.3.0" +name = "aws-lc-sys" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36097332580c65ddaac1ad9686ffb58ea531bf3b2d4b3cef7ccb9b7271045d4b" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-nitro-enclaves-cose" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a94047bd9c3717c6ca3a145504c0e26b64a5e2d9eb9559b187748433fbc382" dependencies = [ - "libc", - "log", - "nix 0.26.4", "serde", "serde_bytes", "serde_cbor", + "serde_repr", + "serde_with 3.14.0", ] [[package]] @@ -333,9 +341,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -443,15 +451,22 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.17" +version = "1.2.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -579,12 +594,51 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +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_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[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 = "core-foundation" version = "0.9.4" @@ -595,6 +649,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" @@ -803,6 +867,37 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.100", +] + [[package]] name = "difflib" version = "0.4.0" @@ -839,22 +934,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "dyn-clone" -version = "1.0.20" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "ecdsa" -version = "0.15.1" +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12844141594ad74185a926d030f3b605f6a903b4e3fec351f3ea338ac5b7637e" -dependencies = [ - "der 0.6.1", - "elliptic-curve 0.12.3", - "rfc6979 0.3.1", - "signature", -] +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" @@ -865,7 +954,7 @@ dependencies = [ "der 0.7.9", "digest", "elliptic-curve 0.13.8", - "rfc6979 0.4.0", + "rfc6979", "serdect", "signature", "spki 0.7.3", @@ -891,8 +980,6 @@ dependencies = [ "generic-array 0.14.7", "group 0.12.1", "hkdf", - "pem-rfc7468 0.6.0", - "pkcs8 0.9.0", "rand_core 0.6.4", "sec1 0.3.0", "subtle", @@ -912,7 +999,8 @@ dependencies = [ "generic-array 0.14.7", "group 0.13.0", "hkdf", - "pkcs8 0.10.2", + "pem-rfc7468 0.7.0", + "pkcs8", "rand_core 0.6.4", "sec1 0.7.3", "serdect", @@ -990,6 +1078,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1038,6 +1132,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 = "2.0.0" @@ -1182,6 +1282,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "ghash" version = "0.5.1" @@ -1429,6 +1541,15 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-auth" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" +dependencies = [ + "memchr", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1748,7 +1869,7 @@ version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.0", "cfg-if", "libc", ] @@ -1796,6 +1917,50 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[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.100", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -1808,14 +1973,30 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +dependencies = [ + "base64 0.22.1", + "getrandom 0.2.15", + "js-sys", + "serde", + "serde_json", + "signature", +] + [[package]] name = "k256" version = "0.13.4" @@ -1823,7 +2004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa 0.16.9", + "ecdsa", "elliptic-curve 0.13.8", "sha2", ] @@ -1983,10 +2164,10 @@ 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", ] @@ -2010,7 +2191,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -2127,6 +2308,49 @@ dependencies = [ "memchr", ] +[[package]] +name = "oci-client" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b7f8deaffcd3b0e3baf93dddcab3d18b91d46dc37d38a8b170089b234de5bb3" +dependencies = [ + "bytes", + "chrono", + "futures-util", + "http 1.3.1", + "http-auth", + "jsonwebtoken", + "lazy_static", + "oci-spec", + "olpc-cjson", + "regex", + "reqwest 0.13.2", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.12", + "tokio", + "tracing", + "unicase", +] + +[[package]] +name = "oci-spec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8445a2631507cec628a15fdd6154b54a3ab3f20ed4fe9d73a3b8b7a4e1ba03a" +dependencies = [ + "const_format", + "derive_builder", + "getset", + "regex", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror 2.0.12", +] + [[package]] name = "oid-registry" version = "0.6.1" @@ -2136,6 +2360,17 @@ dependencies = [ "asn1-rs", ] +[[package]] +name = "olpc-cjson" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" +dependencies = [ + "serde", + "serde_json", + "unicode-normalization", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2154,7 +2389,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -2180,6 +2415,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.1+3.5.1" @@ -2211,52 +2452,28 @@ dependencies = [ "elliptic-curve 0.12.3", ] -[[package]] -name = "p256" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c124b3cbce43bcbac68c58ec181d98ed6cc7e6d0aa7c3ba97b2563410b0e55" -dependencies = [ - "ecdsa 0.15.1", - "elliptic-curve 0.12.3", - "primeorder 0.12.1", - "sha2", -] - [[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa 0.16.9", + "ecdsa", "elliptic-curve 0.13.8", - "primeorder 0.13.6", + "primeorder", "serdect", "sha2", ] -[[package]] -name = "p384" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630a4a9b2618348ececfae61a4905f564b817063bf2d66cdfc2ced523fe1d2d4" -dependencies = [ - "ecdsa 0.15.1", - "elliptic-curve 0.12.3", - "primeorder 0.12.1", - "sha2", -] - [[package]] name = "p384" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa 0.16.9", + "ecdsa", "elliptic-curve 0.13.8", - "primeorder 0.13.6", + "primeorder", "sha2", ] @@ -2329,16 +2546,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der 0.6.1", - "spki 0.6.0", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -2430,15 +2637,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "primeorder" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b54f7131b3dba65a2f414cf5bd25b66d4682e4608610668eae785750ba4c5b2" -dependencies = [ - "elliptic-curve 0.12.3", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -2482,6 +2680,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -2600,7 +2820,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ee8b50c99aa2d1d9163d8696d8c6ed4e5f9ff309410610b22a49d13fde9cc0" dependencies = [ - "aws-nitro-enclaves-nsm-api 0.4.0", + "aws-nitro-enclaves-nsm-api", "borsh", "futures", "libc", @@ -2644,9 +2864,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0227cb01964c41ba950b77b9c97aa3be12e9a5bf24991f3819dffcba68efc3b" dependencies = [ "aws-nitro-enclaves-cose", - "aws-nitro-enclaves-nsm-api 0.4.0", + "aws-nitro-enclaves-nsm-api", "borsh", - "p384 0.13.1", + "p384", "qos_hex", "serde_bytes", "sha2", @@ -2696,6 +2916,7 @@ version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.3", "lru-slab", @@ -2811,7 +3032,7 @@ version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.0", ] [[package]] @@ -2911,21 +3132,51 @@ dependencies = [ ] [[package]] -name = "resolv-conf" -version = "0.7.4" +name = "reqwest" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] [[package]] -name = "rfc6979" -version = "0.3.1" +name = "resolv-conf" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] +checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" [[package]] name = "rfc6979" @@ -2978,7 +3229,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2991,6 +3242,7 @@ version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -2999,6 +3251,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -3009,12 +3273,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +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 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3089,7 +3381,6 @@ dependencies = [ "base16ct 0.1.1", "der 0.6.1", "generic-array 0.14.7", - "pkcs8 0.9.0", "subtle", "zeroize", ] @@ -3103,7 +3394,7 @@ dependencies = [ "base16ct 0.2.0", "der 0.7.9", "generic-array 0.14.7", - "pkcs8 0.10.2", + "pkcs8", "serdect", "subtle", "zeroize", @@ -3115,8 +3406,21 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3124,9 +3428,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3333,9 +3637,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core 0.6.4", @@ -3388,7 +3692,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ - "base64ct", "der 0.6.1", ] @@ -3425,6 +3728,24 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3755,11 +4076,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.3.1", @@ -3789,6 +4110,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3845,7 +4167,7 @@ dependencies = [ "mime", "prost 0.12.6", "prost-types 0.12.6", - "reqwest", + "reqwest 0.12.22", "serde", "serde_json", "serde_with 3.14.0", @@ -3892,7 +4214,7 @@ version = "0.0.1" dependencies = [ "dotenvy", "hex", - "reqwest", + "reqwest 0.12.22", "serde", "serde_json", "tokio", @@ -3908,7 +4230,7 @@ version = "0.6.1" dependencies = [ "attestation-doc-validation", "aws-nitro-enclaves-cose", - "aws-nitro-enclaves-nsm-api 0.3.0", + "aws-nitro-enclaves-nsm-api", "base64 0.13.1", "borsh", "ciborium", @@ -3916,7 +4238,7 @@ dependencies = [ "hex", "hex-literal", "p256 0.13.2", - "p384 0.13.1", + "p384", "rand 0.8.5", "serde", "serde_bytes", @@ -3940,6 +4262,7 @@ dependencies = [ "clap", "hex", "hpke", + "oci-client", "p256 0.13.2", "predicates", "qos_core", @@ -3960,12 +4283,27 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.6" @@ -4097,48 +4435,32 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.100", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4146,31 +4468,44 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.100", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", @@ -4196,6 +4531,15 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.2" @@ -4285,6 +4629,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4312,6 +4665,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4343,6 +4711,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4355,6 +4729,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4367,6 +4747,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4385,6 +4771,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4397,6 +4789,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4409,6 +4807,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4421,6 +4825,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4482,7 +4892,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b0359bf0..74fbabed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,8 +56,8 @@ prost-types = { version = "0.12", default-features = false } prost-build = { version = "0.12.6", default-features = false } # AWS Nitro enclaves -attestation-doc-validation = { version = "0.8.0", default-features = false } -aws-nitro-enclaves-nsm-api = { version = "0.3", features = ["nix"], default-features = false } +attestation-doc-validation = { version = "0.9.0", default-features = false } +aws-nitro-enclaves-nsm-api = { version = "0.4", features = ["nix"], default-features = false } aws-nitro-enclaves-cose = { version = "0.5", default-features = false } # CBOR and COSE @@ -88,6 +88,9 @@ walkdir = { version = "2.5", default-features = false } dotenvy = { version = "0.15.0", default-features = false } clap = { version = "4.5", features = ["std", "derive", "help", "usage", "error-context"], default-features = false } +# Container operations +oci-client = { version = "0.16.1", default-features = false, features = ["rustls-tls"] } + # Development dependencies assert_cmd = { version = "2", default-features = false } predicates = { version = "3", default-features = false } diff --git a/tvc/Cargo.toml b/tvc/Cargo.toml index 4f2f1e78..f180fc66 100644 --- a/tvc/Cargo.toml +++ b/tvc/Cargo.toml @@ -23,6 +23,7 @@ serde = { workspace = true } serde_json = { workspace = true } toml = { workspace = true } tokio = { workspace = true, features = ["fs"] } +oci-client = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } From b77216449f7aae842b96ba07df92e14c39f87528 Mon Sep 17 00:00:00 2001 From: Annie Ke Date: Tue, 7 Apr 2026 18:09:56 -0700 Subject: [PATCH 2/2] feat(tvc): add pivot hash validation --- Cargo.lock | 82 ++- Cargo.toml | 2 + tvc/Cargo.toml | 6 +- tvc/README.md | 39 +- tvc/src/cli.rs | 5 + tvc/src/commands/app/mod.rs | 2 +- tvc/src/commands/app/status.rs | 9 +- tvc/src/commands/deploy/approve.rs | 80 ++- tvc/src/commands/deploy/create.rs | 84 ++- tvc/src/commands/deploy/get_status.rs | 5 +- tvc/src/commands/deploy/mod.rs | 1 + .../commands/deploy/validate_pivot_digest.rs | 56 ++ tvc/src/lib.rs | 1 + tvc/src/pivot_digest.rs | 573 ++++++++++++++++++ tvc/tests/deploy_approve.rs | 27 + 15 files changed, 928 insertions(+), 44 deletions(-) create mode 100644 tvc/src/commands/deploy/validate_pivot_digest.rs create mode 100644 tvc/src/pivot_digest.rs diff --git a/Cargo.lock b/Cargo.lock index af88dba6..dc946115 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crunchy" version = "0.2.3" @@ -1078,6 +1087,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1096,6 +1116,16 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2030,6 +2060,18 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2122,6 +2164,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2495,7 +2538,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.10", "smallvec", "windows-targets 0.52.6", ] @@ -2562,6 +2605,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "poly1305" version = "0.8.0" @@ -3035,6 +3084,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "ref-cast" version = "1.0.24" @@ -3291,7 +3349,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3645,6 +3703,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.9" @@ -3812,6 +3876,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", +] + [[package]] name = "tempfile" version = "3.19.1" @@ -4258,8 +4332,10 @@ version = "0.0.0-alpha.0" dependencies = [ "anyhow", "assert_cmd", + "base64 0.22.1", "chrono", "clap", + "flate2", "hex", "hpke", "oci-client", @@ -4269,6 +4345,8 @@ dependencies = [ "qos_p256", "serde", "serde_json", + "sha2", + "tar", "tempfile", "tokio", "toml", diff --git a/Cargo.toml b/Cargo.toml index 74fbabed..01575682 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,14 @@ qos_p256 = { version = "0.5.0", default-features = false } # Encoding and serialization base64 = { version = "0.22.0", default-features = false, features = ["std"] } bs58 = { version = "0.5.0", features = ["std", "check"], default-features = false } +flate2 = { version = "1.1.9", default-features = false, features = ["rust_backend"] } hex = { version = "0.4.3", default-features = false, features = ["std"] } serde = { version = "1.0.219", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0.140", default-features = false, features = ["std"] } serde_bytes = { version = "0.11", default-features = false } serde_cbor = { version = "0.11", default-features = false } serde_with = { version = "3.14.0", default-features = false, features = ["macros", "base64"] } +tar = { version = "0.4.45", default-features = false } # Cryptography hpke = { version = "0.10", features = ["alloc", "p256", "serde_impls"], default-features = false } diff --git a/tvc/Cargo.toml b/tvc/Cargo.toml index f180fc66..a7ad0fc6 100644 --- a/tvc/Cargo.toml +++ b/tvc/Cargo.toml @@ -15,12 +15,17 @@ turnkey_client = { workspace = true } turnkey_enclave_encrypt = { workspace = true } anyhow = { workspace = true } +base64 = { workspace = true } chrono = { workspace = true } clap = { workspace = true, features = ["env"] } +flate2 = { workspace = true } hex = { workspace = true } p256 = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +sha2 = { workspace = true } +tar = { workspace = true } +tempfile = { workspace = true } toml = { workspace = true } tokio = { workspace = true, features = ["fs"] } oci-client = { workspace = true } @@ -29,4 +34,3 @@ oci-client = { workspace = true } assert_cmd = { workspace = true } hpke = { workspace = true } predicates = { workspace = true } -tempfile = { workspace = true } diff --git a/tvc/README.md b/tvc/README.md index 5ccbb9e6..ae51bee4 100644 --- a/tvc/README.md +++ b/tvc/README.md @@ -25,12 +25,20 @@ tvc deploy init --output my-deploy.json # Edit my-deploy.json to fill in required values (appId, container images, etc.) -# Create the deployment -tvc deploy create my-deploy.json +# Optional: validate the digest of the file at pivotPath inside the image locally +tvc deploy validate-pivot-digest \ + --image-url ghcr.io/tkhq/helloworld:latest \ + --pivot-path /helloworld \ + --expected-digest -# Recommended: uses GetTvcDeployment to fetch manifest and manifest_id automatically +# Create the deployment and validate the pivot digest locally first +tvc deploy create my-deploy.json --validate-pivot-digest + +# Recommended: uses GetTvcDeployment to fetch the manifest automatically and +# validates the pivot digest against the deployment manifest before approval tvc deploy approve \ --deploy-id \ + --validate-pivot-digest \ --operator-id # Turnkey's ID for your operator (from app create response) # Alternative: provide manifest file and IDs manually @@ -38,4 +46,27 @@ tvc deploy approve \ --manifest manifest.json \ --manifest-id \ # Turnkey's ID for the manifest (from deploy create response) --operator-id -``` \ No newline at end of file +``` + +## Pivot Digest Validation + +`tvc deploy validate-pivot-digest` computes the SHA-256 digest of the file at +`pivotPath` inside a Linux container image. The command resolves the image with +the CLI's native OCI client and does not require Docker. + +For private images, pass `--pull-secret` with an unencrypted Docker-style +`config.json` containing credentials for the image registry. + +```bash +tvc deploy validate-pivot-digest \ + --image-url ghcr.io/tkhq/helloworld@sha256:f8132a6236609e4c67d9d29e5694989f18e528240844638e850897ee6319676d \ + --pivot-path /helloworld \ + --expected-digest cbe01169428f144086bfaef348bbf3db70f9217628996cafd2ecb85d5f2b47a1 +``` + +Notes: + +- Validation is Linux-only and resolves the image as `linux/amd64`. +- `tvc deploy approve --validate-pivot-digest` only works with `--deploy-id`. +- `--pull-secret` expects an unencrypted Docker-style JSON file, not the + encrypted pull secret stored in deployment config. diff --git a/tvc/src/cli.rs b/tvc/src/cli.rs index e4cff05a..17b56968 100644 --- a/tvc/src/cli.rs +++ b/tvc/src/cli.rs @@ -23,6 +23,9 @@ impl Cli { DeployCommands::Status(args) => commands::deploy::status::run(args).await, DeployCommands::Create(args) => commands::deploy::create::run(args).await, DeployCommands::Init(args) => commands::deploy::init::run(args).await, + DeployCommands::ValidatePivotDigest(args) => { + commands::deploy::validate_pivot_digest::run(args).await + } }, Commands::App { command } => match command { AppCommands::Status(args) => commands::app::status::run(args).await, @@ -63,6 +66,8 @@ enum DeployCommands { Create(commands::deploy::create::Args), /// Generate a template deployment configuration file. Init(commands::deploy::init::Args), + /// Compute or validate the pivot digest for a container image locally. + ValidatePivotDigest(commands::deploy::validate_pivot_digest::Args), } #[derive(Debug, Subcommand)] diff --git a/tvc/src/commands/app/mod.rs b/tvc/src/commands/app/mod.rs index e27817d1..cbbcd9ee 100644 --- a/tvc/src/commands/app/mod.rs +++ b/tvc/src/commands/app/mod.rs @@ -1,6 +1,6 @@ //! App commands. pub mod create; -pub mod status; pub mod init; pub mod list; +pub mod status; diff --git a/tvc/src/commands/app/status.rs b/tvc/src/commands/app/status.rs index 5ee91009..2d8cd0c6 100644 --- a/tvc/src/commands/app/status.rs +++ b/tvc/src/commands/app/status.rs @@ -30,15 +30,12 @@ pub async fn run(args: Args) -> anyhow::Result<()> { let app_status = crate::commands::app_status::sanitize_app_status( response - .app_status - .ok_or_else(|| anyhow!("no status returned for app: {}", args.app_id))?, + .app_status + .ok_or_else(|| anyhow!("no status returned for app: {}", args.app_id))?, ); println!("App ID: {}", app_status.app_id); - println!( - "Targeted Deployment: {}", - app_status.targeted_deployment_id - ); + println!("Targeted Deployment: {}", app_status.targeted_deployment_id); if app_status.deployments.is_empty() { println!(); diff --git a/tvc/src/commands/deploy/approve.rs b/tvc/src/commands/deploy/approve.rs index 8b12f995..06b5a32d 100644 --- a/tvc/src/commands/deploy/approve.rs +++ b/tvc/src/commands/deploy/approve.rs @@ -1,8 +1,10 @@ //! Approve deploy command - cryptographically approve a QOS manifest. +use super::validate_pivot_digest::print_result; use crate::config::app::KNOWN_SHARE_SET_KEYS; use crate::config::turnkey::{Config, StoredQosOperatorKey}; use crate::pair::LocalPair; +use crate::pivot_digest::{compute_pivot_digest, validate_expected_digest, PivotDigestSource}; use crate::util::{read_file_to_string, write_file}; use anyhow::{anyhow, bail, Context}; use clap::{ArgGroup, Args as ClapArgs}; @@ -19,6 +21,13 @@ use turnkey_client::generated::{ CreateTvcManifestApprovalsIntent, GetTvcDeploymentRequest, TvcManifestApproval, }; +struct DeployApprovalSource { + manifest: Manifest, + manifest_id: String, + pivot_image_url: Option, + pivot_path: Option, +} + /// Cryptographically approve a QOS manifest for a deployment with your operator's manifest set key. #[derive(Debug, ClapArgs)] #[command(about, long_about = None)] @@ -57,6 +66,14 @@ pub struct Args { #[arg(long, help_heading = "Operator signing key", value_name = "PATH")] pub operator_seed: Option, + /// Locally validate the pivot digest before approval. Only supported with `--deploy-id`. + #[arg(long)] + pub validate_pivot_digest: bool, + + /// Path to an unencrypted Docker-style pull secret JSON file. + #[arg(long, value_name = "PATH")] + pub pull_secret: Option, + /// Walk through manifest approval prompts but do not generate an approval. #[arg(long)] pub dry_run: bool, @@ -76,15 +93,48 @@ pub struct Args { /// Run the approve deploy command. pub async fn run(args: Args) -> anyhow::Result<()> { + if args.validate_pivot_digest && args.manifest.is_some() { + bail!( + "--validate-pivot-digest only works with --deploy-id. \ + Use `tvc deploy validate-pivot-digest` to validate a manifest file source locally." + ); + } + // Fetch manifest - track manifest_id if fetched from API - let (manifest, fetched_manifest_id) = match (&args.manifest, &args.deploy_id) { - (Some(path), _) => (read_manifest_from_path(path).await?, None), - (_, Some(deploy_id)) => { - let (manifest, manifest_id) = fetch_manifest_from_deploy(deploy_id).await?; - (manifest, Some(manifest_id)) - } - (None, None) => bail!("a manifest source is required"), - }; + let (manifest, fetched_manifest_id, pivot_image_url, pivot_path) = + match (&args.manifest, &args.deploy_id) { + (Some(path), _) => (read_manifest_from_path(path).await?, None, None, None), + (_, Some(deploy_id)) => { + let source = fetch_manifest_from_deploy(deploy_id).await?; + ( + source.manifest, + Some(source.manifest_id), + source.pivot_image_url, + source.pivot_path, + ) + } + (None, None) => bail!("a manifest source is required"), + }; + + if args.validate_pivot_digest { + let pivot_image_url = pivot_image_url + .ok_or_else(|| anyhow!("deployment is missing a pivot container image URL"))?; + let pivot_path = + pivot_path.ok_or_else(|| anyhow!("deployment is missing a pivot container path"))?; + + let result = compute_pivot_digest( + &PivotDigestSource { + image_url: pivot_image_url, + pivot_path, + }, + args.pull_secret.as_deref(), + ) + .await?; + validate_expected_digest(&result.digest, &hex::encode(manifest.pivot.hash))?; + print_result(&result); + println!("Pivot digest validated successfully."); + println!(); + } if !args.dangerous_skip_interactive { interactive_approve(&manifest)?; @@ -369,7 +419,7 @@ async fn read_manifest_from_path(path: &Path) -> anyhow::Result { /// Fetch manifest from Turnkey using GetTvcDeployment API. /// Returns the manifest and its Turnkey manifest_id. -async fn fetch_manifest_from_deploy(deploy_id: &str) -> anyhow::Result<(Manifest, String)> { +async fn fetch_manifest_from_deploy(deploy_id: &str) -> anyhow::Result { println!("Fetching deployment {deploy_id}..."); let auth = crate::client::build_client().await?; @@ -399,5 +449,15 @@ async fn fetch_manifest_from_deploy(deploy_id: &str) -> anyhow::Result<(Manifest println!("✓ Manifest loaded (manifest_id: {})", tvc_manifest.id); - Ok((manifest, tvc_manifest.id)) + let (pivot_image_url, pivot_path) = deployment + .pivot_container + .map(|container| (Some(container.container_url), Some(container.path))) + .unwrap_or((None, None)); + + Ok(DeployApprovalSource { + manifest, + manifest_id: tvc_manifest.id, + pivot_image_url, + pivot_path, + }) } diff --git a/tvc/src/commands/deploy/create.rs b/tvc/src/commands/deploy/create.rs index bb7d38ed..3c5c6c87 100644 --- a/tvc/src/commands/deploy/create.rs +++ b/tvc/src/commands/deploy/create.rs @@ -1,7 +1,11 @@ //! Deploy create command - creates a deployment from a config file. use crate::client::build_client; +use crate::commands::deploy::validate_pivot_digest::print_result; use crate::config::deploy::DeployConfig; +use crate::pivot_digest::{ + compute_pivot_digest, resolve_pinned_image_url, validate_expected_digest, PivotDigestSource, +}; use crate::pull_secret::encrypt_pivot_pull_secret; use anyhow::{Context, Result}; use clap::Args as ClapArgs; @@ -16,12 +20,37 @@ pub struct Args { /// Path to the deployment configuration file (JSON). pub config_file: PathBuf, - /// Path to an unencrypted pivot container pull secret file. + /// Path to an unencrypted Docker-style pull secret JSON file. /// /// The content will be encrypted based on the active org's API environment and /// override `pivotContainerEncryptedPullSecret` from the config file. - #[arg(long, alias = "pull-secret", value_name = "PATH")] + #[arg(long = "pull-secret", alias = "pivot-pull-secret", value_name = "PATH")] pub pivot_pull_secret: Option, + + /// Locally validate the digest of the file at `pivot_path` inside the pinned pivot image. + #[arg(long)] + pub validate_pivot_digest: bool, +} + +fn build_create_intent( + deploy_config: &DeployConfig, + pivot_container_image_url: String, + pivot_container_encrypted_pull_secret: Option, +) -> CreateTvcDeploymentIntent { + CreateTvcDeploymentIntent { + app_id: deploy_config.app_id.clone(), + qos_version: deploy_config.qos_version.clone(), + pivot_container_image_url, + pivot_path: deploy_config.pivot_path.clone(), + pivot_args: deploy_config.pivot_args.clone(), + expected_pivot_digest: deploy_config.expected_pivot_digest.clone(), + pivot_container_encrypted_pull_secret, + debug_mode: deploy_config.debug_mode, + nonce: None, + health_check_type: deploy_config.health_check_type, + health_check_port: deploy_config.health_check_port as u32, + public_ingress_port: deploy_config.public_ingress_port as u32, + } } /// Run the deploy create command. @@ -69,22 +98,43 @@ pub async fn run(args: Args) -> Result<()> { None => deploy_config.pivot_container_encrypted_pull_secret.clone(), }; - // Convert config to API intent - let intent = CreateTvcDeploymentIntent { - app_id: deploy_config.app_id.clone(), - qos_version: deploy_config.qos_version.clone(), - pivot_container_image_url: deploy_config.pivot_container_image_url.clone(), - pivot_path: deploy_config.pivot_path.clone(), - pivot_args: deploy_config.pivot_args.clone(), - expected_pivot_digest: deploy_config.expected_pivot_digest.clone(), - pivot_container_encrypted_pull_secret, - debug_mode: deploy_config.debug_mode, - nonce: None, - health_check_type: deploy_config.health_check_type, - health_check_port: deploy_config.health_check_port as u32, - public_ingress_port: deploy_config.public_ingress_port as u32, + let deployment_image_url = if args.validate_pivot_digest { + let pinned_image_url = resolve_pinned_image_url( + &deploy_config.pivot_container_image_url, + args.pivot_pull_secret.as_deref(), + ) + .await?; + + if pinned_image_url != deploy_config.pivot_container_image_url { + println!("Using pinned image reference for deployment request: {pinned_image_url}"); + } + + pinned_image_url + } else { + deploy_config.pivot_container_image_url.clone() }; + if args.validate_pivot_digest { + let result = compute_pivot_digest( + &PivotDigestSource { + image_url: deployment_image_url.clone(), + pivot_path: deploy_config.pivot_path.clone(), + }, + args.pivot_pull_secret.as_deref(), + ) + .await?; + validate_expected_digest(&result.digest, &deploy_config.expected_pivot_digest)?; + print_result(&result); + println!("Pivot digest validated successfully."); + println!(); + } + + let intent = build_create_intent( + &deploy_config, + deployment_image_url, + pivot_container_encrypted_pull_secret, + ); + // Get timestamp let timestamp_ms = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -111,7 +161,7 @@ pub async fn run(args: Args) -> Result<()> { result.result.deployment_id ); println!( - " - Run `tvc deploy approve --deploy-id {}` to approve the manifest", + " - Run `tvc deploy approve --deploy-id {} --validate-pivot-digest` to validate and approve the manifest", result.result.deployment_id ); diff --git a/tvc/src/commands/deploy/get_status.rs b/tvc/src/commands/deploy/get_status.rs index 9eeaa7e9..6897a026 100644 --- a/tvc/src/commands/deploy/get_status.rs +++ b/tvc/src/commands/deploy/get_status.rs @@ -46,8 +46,8 @@ pub async fn run(args: Args) -> anyhow::Result<()> { let app_status = crate::commands::app_status::sanitize_app_status( app_response - .app_status - .ok_or_else(|| anyhow!("no status returned for app: {}", deployment.app_id))?, + .app_status + .ok_or_else(|| anyhow!("no status returned for app: {}", deployment.app_id))?, ); println!("Deployment: {}", deployment.id); @@ -110,5 +110,4 @@ mod tests { assert!(deployment_status.is_some()); } - } diff --git a/tvc/src/commands/deploy/mod.rs b/tvc/src/commands/deploy/mod.rs index d83fb1a5..940048f7 100644 --- a/tvc/src/commands/deploy/mod.rs +++ b/tvc/src/commands/deploy/mod.rs @@ -5,3 +5,4 @@ pub mod create; pub mod get_status; pub mod init; pub mod status; +pub mod validate_pivot_digest; diff --git a/tvc/src/commands/deploy/validate_pivot_digest.rs b/tvc/src/commands/deploy/validate_pivot_digest.rs new file mode 100644 index 00000000..a07f8d67 --- /dev/null +++ b/tvc/src/commands/deploy/validate_pivot_digest.rs @@ -0,0 +1,56 @@ +//! Validate pivot digest command - compute or validate a pivot digest locally. + +use crate::pivot_digest::{ + compute_pivot_digest, validate_expected_digest, PivotDigestResult, PivotDigestSource, +}; +use anyhow::Result; +use clap::Args as ClapArgs; +use std::path::PathBuf; + +/// Compute or validate the digest of the file at `pivot_path` inside a container image. +#[derive(Debug, ClapArgs)] +#[command(about, long_about = None)] +pub struct Args { + /// Container image reference to inspect locally. + #[arg(long, value_name = "REF")] + pub image_url: String, + + /// Path to the pivot file inside the container image. + #[arg(long, value_name = "PATH")] + pub pivot_path: String, + + /// Expected pivot digest to compare against. + #[arg(long, value_name = "HEX")] + pub expected_digest: Option, + + /// Path to an unencrypted Docker-style pull secret JSON file. + #[arg(long, value_name = "PATH")] + pub pull_secret: Option, +} + +/// Run the validate pivot digest command. +pub async fn run(args: Args) -> Result<()> { + let result = compute_pivot_digest( + &PivotDigestSource { + image_url: args.image_url, + pivot_path: args.pivot_path, + }, + args.pull_secret.as_deref(), + ) + .await?; + + print_result(&result); + + if let Some(expected_digest) = args.expected_digest.as_deref() { + validate_expected_digest(&result.digest, expected_digest)?; + println!("Pivot digest validated successfully."); + } + + Ok(()) +} + +pub fn print_result(result: &PivotDigestResult) { + println!("Image: {}", result.image_url); + println!("Pivot Path: {}", result.pivot_path); + println!("Pivot Digest: {}", result.digest); +} diff --git a/tvc/src/lib.rs b/tvc/src/lib.rs index 97da9584..004e72b8 100644 --- a/tvc/src/lib.rs +++ b/tvc/src/lib.rs @@ -4,5 +4,6 @@ pub mod client; pub mod commands; pub mod config; pub mod pair; +pub(crate) mod pivot_digest; pub mod pull_secret; pub mod util; diff --git a/tvc/src/pivot_digest.rs b/tvc/src/pivot_digest.rs new file mode 100644 index 00000000..1ef85ee3 --- /dev/null +++ b/tvc/src/pivot_digest.rs @@ -0,0 +1,573 @@ +use anyhow::{anyhow, bail, Context, Result}; +use oci_client::client::{linux_amd64_resolver, ClientConfig}; +use oci_client::config::ConfigFile; +use oci_client::manifest::{ + OciDescriptor, IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE, IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE, + IMAGE_LAYER_GZIP_MEDIA_TYPE, IMAGE_LAYER_MEDIA_TYPE, +}; +use oci_client::secrets::RegistryAuth; +use oci_client::{Client, Reference}; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; +use std::path::{Component, Path, PathBuf}; +use tar::Archive; +use tempfile::TempDir; + +const DEBUG_ENV: &str = "TVC_DEBUG_PIVOT_DIGEST"; +const MAX_CANDIDATE_PATHS: usize = 8; +const MAX_SAMPLE_PATHS: usize = 6; + +#[derive(Debug, Clone)] +pub struct PivotDigestSource { + pub image_url: String, + pub pivot_path: String, +} + +#[derive(Debug, Clone)] +pub struct PivotDigestResult { + pub image_url: String, + pub pivot_path: String, + pub digest: String, +} + +#[derive(Debug, Deserialize)] +struct DockerConfig { + #[serde(default)] + auths: HashMap, +} + +#[derive(Debug, Default, Deserialize)] +struct DockerAuthEntry { + #[serde(default)] + auth: Option, + #[serde(default)] + username: Option, + #[serde(default)] + password: Option, + #[serde(default, rename = "identitytoken")] + identity_token: Option, + #[serde(default, rename = "registrytoken")] + registry_token: Option, +} + +pub async fn compute_pivot_digest( + source: &PivotDigestSource, + pull_secret_path: Option<&Path>, +) -> Result { + let reference: Reference = source + .image_url + .parse() + .with_context(|| format!("invalid image reference: {}", source.image_url))?; + let target = normalize_pivot_path(&source.pivot_path)?; + debug_log(format!( + "computing pivot digest for image '{}' and target '{}'", + source.image_url, + target.display() + )); + let auth = registry_auth_for_reference(&reference, pull_secret_path)?; + let client = build_client(); + let (manifest, manifest_digest, config_json) = client + .pull_manifest_and_config(&reference, &auth) + .await + .with_context(|| { + format!( + "failed to pull image manifest and config for {}", + source.image_url + ) + })?; + debug_log(format!( + "resolved manifest {} with {} layer(s)", + manifest_digest, + manifest.layers.len() + )); + log_image_config(&config_json); + + let temp_dir = TempDir::new().context("failed to create temporary directory for layers")?; + let mut current_contents: Option> = None; + let mut candidate_paths = Vec::new(); + + for (index, layer) in manifest.layers.iter().enumerate() { + debug_log(format!( + "inspecting layer {} digest={} media_type={}", + index, layer.digest, layer.media_type + )); + let layer_path = temp_dir.path().join(format!("layer-{index}.blob")); + let layer_file = tokio::fs::File::create(&layer_path) + .await + .with_context(|| { + format!("failed to create temp layer file: {}", layer_path.display()) + })?; + + client + .pull_blob(&reference, layer, layer_file) + .await + .with_context(|| { + format!( + "failed to pull layer {} for {}", + layer.digest, source.image_url + ) + })?; + + let inspection = apply_layer(&layer_path, layer, &target, &mut current_contents) + .with_context(|| { + format!( + "failed to inspect pivot path '{}' in layer {}", + source.pivot_path, layer.digest + ) + })?; + + if inspection.found_exact_match { + debug_log(format!( + "layer {} contained the pivot path '{}'", + index, + target.display() + )); + } + + if inspection.cleared_target { + debug_log(format!( + "layer {} removed or shadowed the target path '{}'", + index, + target.display() + )); + } + + if !inspection.sample_paths.is_empty() { + debug_log(format!( + "layer {} sample paths: {}", + index, + inspection.sample_paths.join(", ") + )); + } + + for candidate in inspection.candidate_paths { + if candidate_paths.len() < MAX_CANDIDATE_PATHS && !candidate_paths.contains(&candidate) + { + candidate_paths.push(candidate); + } + } + } + + let contents = current_contents.ok_or_else(|| { + let hint = if candidate_paths.is_empty() { + format!( + "pivot path '{}' was not found in image {}. Set {}=1 for per-layer debug logs.", + source.pivot_path, source.image_url, DEBUG_ENV + ) + } else { + format!( + "pivot path '{}' was not found in image {}. Candidate paths seen while inspecting layers: {}. Set {}=1 for per-layer debug logs.", + source.pivot_path, + source.image_url, + candidate_paths.join(", "), + DEBUG_ENV + ) + }; + anyhow!(hint) + })?; + + Ok(PivotDigestResult { + image_url: source.image_url.clone(), + pivot_path: source.pivot_path.clone(), + digest: format!("{:x}", Sha256::digest(&contents)), + }) +} + +pub async fn resolve_pinned_image_url( + image_url: &str, + pull_secret_path: Option<&Path>, +) -> Result { + if image_url.contains('@') { + return Ok(image_url.to_string()); + } + + let reference: Reference = image_url + .parse() + .with_context(|| format!("invalid image reference: {image_url}"))?; + let auth = registry_auth_for_reference(&reference, pull_secret_path)?; + let client = build_client(); + let (_, digest) = client + .pull_image_manifest(&reference, &auth) + .await + .with_context(|| format!("failed to resolve image digest for {image_url}"))?; + debug_log(format!( + "resolved pinned digest {digest} for image '{image_url}'" + )); + + Ok(format!("{image_url}@{digest}")) +} + +pub fn validate_expected_digest(actual_digest: &str, expected_digest: &str) -> Result<()> { + let expected = normalize_expected_digest(expected_digest)?; + let actual = normalize_expected_digest(actual_digest)?; + + if actual != expected { + bail!("pivot digest mismatch: expected {expected}, got {actual}"); + } + + Ok(()) +} + +fn registry_auth_for_reference( + reference: &Reference, + pull_secret_path: Option<&Path>, +) -> Result { + let Some(path) = pull_secret_path else { + debug_log("using anonymous registry auth"); + return Ok(RegistryAuth::Anonymous); + }; + + let content = std::fs::read_to_string(path) + .with_context(|| format!("failed to read pull secret file: {}", path.display()))?; + + if content.trim().is_empty() { + bail!( + "pull secret file is empty after trimming whitespace: {}", + path.display() + ); + } + + let config: DockerConfig = serde_json::from_str(&content) + .with_context(|| format!("failed to parse pull secret JSON: {}", path.display()))?; + let registry = normalize_registry(reference.resolve_registry()); + debug_log(format!( + "resolving registry credentials for '{}' from '{}'", + registry, + path.display() + )); + + let entry = config + .auths + .into_iter() + .find(|(key, _)| normalize_registry(key) == registry) + .map(|(_, entry)| entry) + .ok_or_else(|| { + anyhow!( + "no credentials found for registry '{}' in pull secret file {}", + registry, + path.display() + ) + })?; + + if let Some(token) = entry + .identity_token + .filter(|value| !value.trim().is_empty()) + .or(entry + .registry_token + .filter(|value| !value.trim().is_empty())) + { + debug_log(format!("using bearer auth for registry '{registry}'")); + return Ok(RegistryAuth::Bearer(token)); + } + + if let Some(auth) = entry.auth.filter(|value| !value.trim().is_empty()) { + use base64::Engine; + + let decoded = base64::engine::general_purpose::STANDARD + .decode(auth) + .context("failed to decode base64 auth entry in pull secret")?; + let decoded = String::from_utf8(decoded) + .context("decoded pull secret auth entry is not valid UTF-8")?; + let (username, password) = decoded + .split_once(':') + .ok_or_else(|| anyhow!("pull secret auth entry must decode to 'username:password'"))?; + debug_log(format!( + "using basic auth from encoded credentials for registry '{registry}'" + )); + return Ok(RegistryAuth::Basic( + username.to_string(), + password.to_string(), + )); + } + + match (entry.username, entry.password) { + (Some(username), Some(password)) if !username.trim().is_empty() => { + debug_log(format!( + "using basic auth from explicit username/password for registry '{registry}'" + )); + Ok(RegistryAuth::Basic(username, password)) + } + _ => bail!( + "pull secret entry for registry '{}' does not contain usable credentials", + registry + ), + } +} + +fn build_client() -> Client { + Client::new(ClientConfig { + platform_resolver: Some(Box::new(linux_amd64_resolver)), + ..Default::default() + }) +} + +#[derive(Default)] +struct LayerInspection { + found_exact_match: bool, + cleared_target: bool, + candidate_paths: Vec, + sample_paths: Vec, +} + +fn normalize_registry(registry: &str) -> String { + let registry = registry + .trim() + .trim_start_matches("https://") + .trim_start_matches("http://") + .trim_end_matches('/') + .trim_end_matches("/v1") + .trim_end_matches("/v2"); + + match registry { + "docker.io" | "registry-1.docker.io" => "index.docker.io".to_string(), + other => other.to_string(), + } +} + +fn normalize_pivot_path(path: &str) -> Result { + if path.trim().is_empty() { + bail!("pivot path cannot be empty"); + } + + let mut normalized = PathBuf::new(); + + for component in Path::new(path).components() { + match component { + Component::Prefix(_) | Component::ParentDir => { + bail!("pivot path must not contain parent-directory components: {path}"); + } + Component::RootDir | Component::CurDir => {} + Component::Normal(segment) => normalized.push(segment), + } + } + + if normalized.as_os_str().is_empty() { + bail!("pivot path must resolve to a file inside the image: {path}"); + } + + Ok(normalized) +} + +fn normalize_archive_path(path: &Path) -> Result { + let mut normalized = PathBuf::new(); + + for component in path.components() { + match component { + Component::Prefix(_) | Component::ParentDir => { + bail!( + "archive entry path contains unsupported traversal: {}", + path.display() + ); + } + Component::RootDir | Component::CurDir => {} + Component::Normal(segment) => normalized.push(segment), + } + } + + Ok(normalized) +} + +fn apply_layer( + layer_path: &Path, + layer: &OciDescriptor, + target: &Path, + current_contents: &mut Option>, +) -> Result { + let media_type = layer.media_type.as_str(); + match media_type { + IMAGE_LAYER_MEDIA_TYPE | IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE => { + let file = File::open(layer_path) + .with_context(|| format!("failed to open layer blob: {}", layer_path.display()))?; + apply_archive(Archive::new(file), target, current_contents) + } + IMAGE_LAYER_GZIP_MEDIA_TYPE | IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE => { + let file = File::open(layer_path) + .with_context(|| format!("failed to open layer blob: {}", layer_path.display()))?; + let reader = flate2::read::GzDecoder::new(file); + apply_archive(Archive::new(reader), target, current_contents) + } + unsupported => bail!("unsupported layer media type: {unsupported}"), + } +} + +fn apply_archive( + mut archive: Archive, + target: &Path, + current_contents: &mut Option>, +) -> Result { + let mut inspection = LayerInspection::default(); + + for entry in archive + .entries() + .context("failed to read tar archive entries")? + { + let mut entry = entry.context("failed to read tar archive entry")?; + let path = normalize_archive_path( + &entry + .path() + .context("failed to read tar archive entry path")?, + )?; + + let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else { + continue; + }; + + if file_name == ".wh..wh..opq" { + let opaque_dir = path.parent().unwrap_or_else(|| Path::new("")); + if target.starts_with(opaque_dir) { + *current_contents = None; + inspection.cleared_target = true; + debug_log(format!( + "opaque whiteout removed target via directory '{}'", + opaque_dir.display() + )); + } + continue; + } + + if let Some(whiteout_target) = file_name.strip_prefix(".wh.") { + let deleted_path = path + .parent() + .unwrap_or_else(|| Path::new("")) + .join(whiteout_target); + if target == deleted_path || target.starts_with(&deleted_path) { + *current_contents = None; + inspection.cleared_target = true; + debug_log(format!( + "whiteout removed target via path '{}'", + deleted_path.display() + )); + } + continue; + } + + if looks_like_candidate(&path, target) + && inspection.candidate_paths.len() < MAX_CANDIDATE_PATHS + { + inspection.candidate_paths.push(path.display().to_string()); + } + + if inspection.sample_paths.len() < MAX_SAMPLE_PATHS { + inspection.sample_paths.push(path.display().to_string()); + } + + if path != target { + continue; + } + + if !entry.header().entry_type().is_file() { + bail!( + "pivot path '{}' resolved to a non-regular file in the image layer", + target.display() + ); + } + + let mut contents = Vec::new(); + entry + .read_to_end(&mut contents) + .context("failed to read pivot file contents from image layer")?; + *current_contents = Some(contents); + inspection.found_exact_match = true; + } + + Ok(inspection) +} + +fn looks_like_candidate(path: &Path, target: &Path) -> bool { + let target_name = target.file_name(); + let path_name = path.file_name(); + + path_name.is_some() + && (target_name == path_name + || path + .display() + .to_string() + .contains(target.to_string_lossy().as_ref())) +} + +fn debug_log(message: impl AsRef) { + if std::env::var_os(DEBUG_ENV).is_some() { + eprintln!("[pivot-digest] {}", message.as_ref()); + } +} + +fn log_image_config(config_json: &str) { + let Ok(config) = serde_json::from_str::(config_json) else { + debug_log("failed to parse image config JSON for debug output"); + return; + }; + + debug_log(format!( + "image platform from config: os={} arch={}", + config.os, config.architecture + )); + + if let Some(runtime) = config.config { + if let Some(entrypoint) = runtime.entrypoint.filter(|value| !value.is_empty()) { + debug_log(format!("image entrypoint: {}", entrypoint.join(" "))); + } + + if let Some(cmd) = runtime.cmd.filter(|value| !value.is_empty()) { + debug_log(format!("image cmd: {}", cmd.join(" "))); + } + + if let Some(working_dir) = runtime.working_dir.filter(|value| !value.is_empty()) { + debug_log(format!("image working dir: {working_dir}")); + } + } +} + +fn normalize_expected_digest(value: &str) -> Result { + let digest = value + .trim() + .strip_prefix("sha256:") + .unwrap_or(value.trim()) + .to_ascii_lowercase(); + + if digest.len() != 64 || !digest.chars().all(|ch| ch.is_ascii_hexdigit()) { + bail!("pivot digest must be 64 hexadecimal characters, optionally prefixed with 'sha256:'"); + } + + Ok(digest) +} + +#[cfg(test)] +mod tests { + use super::normalize_expected_digest; + + #[test] + fn normalize_expected_digest_accepts_prefixed_value() { + let digest = normalize_expected_digest( + "sha256:cbe01169428f144086bfaef348bbf3db70f9217628996cafd2ecb85d5f2b47a1", + ) + .unwrap(); + + assert_eq!( + digest, + "cbe01169428f144086bfaef348bbf3db70f9217628996cafd2ecb85d5f2b47a1" + ); + } + + #[test] + fn normalize_expected_digest_accepts_raw_value() { + let digest = normalize_expected_digest( + "CBE01169428F144086BFAEF348BBF3DB70F9217628996CAFD2ECB85D5F2B47A1", + ) + .unwrap(); + + assert_eq!( + digest, + "cbe01169428f144086bfaef348bbf3db70f9217628996cafd2ecb85d5f2b47a1" + ); + } + + #[test] + fn normalize_expected_digest_rejects_invalid_length() { + let error = normalize_expected_digest("abc").unwrap_err().to_string(); + + assert!(error.contains("64 hexadecimal characters")); + } +} diff --git a/tvc/tests/deploy_approve.rs b/tvc/tests/deploy_approve.rs index fc5ffc9f..e926fa9b 100644 --- a/tvc/tests/deploy_approve.rs +++ b/tvc/tests/deploy_approve.rs @@ -1,6 +1,18 @@ use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; +#[test] +fn approve_help_mentions_validate_pivot_digest() { + cargo_bin_cmd!("tvc") + .arg("deploy") + .arg("approve") + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains("--validate-pivot-digest")) + .stdout(predicate::str::contains("--pull-secret")); +} + #[test] fn approve_requires_source() { cargo_bin_cmd!("tvc") @@ -109,3 +121,18 @@ fn approve_requires_manifest_id_or_skip_post() { "--manifest-id is required to post approval to API", )); } + +#[test] +fn approve_validate_requires_deploy_id() { + cargo_bin_cmd!("tvc") + .arg("deploy") + .arg("approve") + .arg("--manifest") + .arg("fixtures/manifest.json") + .arg("--validate-pivot-digest") + .assert() + .failure() + .stderr(predicate::str::contains( + "--validate-pivot-digest only works with --deploy-id", + )); +}