diff --git a/tvc-cosign/.dockerignore b/tvc-cosign/.dockerignore new file mode 100644 index 0000000..ed8e9a3 --- /dev/null +++ b/tvc-cosign/.dockerignore @@ -0,0 +1,11 @@ +# Keep the build context minimal and deterministic: only Cargo manifests, src, +# and the baked ruleset are COPYed in the Dockerfile. Excluding everything else +# ensures the image digest is a pure function of the committed source. +/target +/notes +/.git +/.claude +*.local.toml +.env +README.md +rules.example.toml diff --git a/tvc-cosign/.gitignore b/tvc-cosign/.gitignore new file mode 100644 index 0000000..454d10a --- /dev/null +++ b/tvc-cosign/.gitignore @@ -0,0 +1,17 @@ +# Rust build artifacts +/target + +# Per-deployment ruleset: compiled into the binary via include_str!, but it holds +# each deployer's own allowlists. Create it from rules.example.toml before building +# (see README Step 1); this demo does not commit its rules, but you might want to do so if it fits your use case. +/rules.toml + +# Local secrets / ruleset overrides (never commit real ones) +*.local.toml +.env + +# Extracted pivot binaries / digest-check artifacts +*.bin + +# Notes +/notes \ No newline at end of file diff --git a/tvc-cosign/Cargo.lock b/tvc-cosign/Cargo.lock new file mode 100644 index 0000000..a647184 --- /dev/null +++ b/tvc-cosign/Cargo.lock @@ -0,0 +1,3278 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "alloy-consensus" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ff0c4adba2abdcd9fb5829ae5f4394c06f8585ed283a9ba79aa33763c802e1" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "k256", + "once_cell", + "secp256k1 0.30.0", + "serde", + "serde_json", + "serde_with", + "thiserror", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", + "thiserror", +] + +[[package]] +name = "alloy-eip7928" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b12337f74cbfa451cb04dac173974814a6ff463079e1793aa09600ba8813ab" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "once_cell", + "serde", + "thiserror", +] + +[[package]] +name = "alloy-eips" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4c0453065b9206acc0f869a258dc8dcbbd595e144b4446f2c493a24a814d1f" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-eip7928", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "serde", + "serde_with", + "sha2 0.10.9", +] + +[[package]] +name = "alloy-primitives" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4885c1409b6936c4898e646ef58baf6ec54edaf6d8179f79df805a7b85b7cf3e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "foldhash", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.4", + "rapidhash", + "ruint", + "rustc-hash", + "secp256k1 0.31.1", + "serde", + "sha3", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4311c03125e8a18296504560b9de3d75ecbd0dcda7f71e6cf2a196d57e6fba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "alloy-serde" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e97b3e0b9f816b25083045dcfa69431bd059a078e828e4d82d296d1949b96c" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-trie" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more", + "nybbles", + "serde", + "smallvec", + "thiserror", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406bc1183f6843e0aba09f7b3365e828b597213d60793ba5cb41befc863e3a78" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" +dependencies = [ + "ark-ff-asm 0.6.0", + "ark-ff-macros 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "educe", + "num-bigint", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-ff-asm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-ff-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.6.0", + "digest 0.10.7", + "num-bigint", + "serde_with", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[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.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +dependencies = [ + "hex-conservative 0.3.2", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[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", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "c-kzg" +version = "2.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + +[[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 = "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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "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", + "getrandom 0.4.3", + "hybrid-array", + "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", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[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", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[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 0.4.1", + "syn 2.0.118", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[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.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 = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[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.0", + "digest 0.11.3", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "spki 0.8.0", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[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", + "group 0.13.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "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", + "pem-rfc7468", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "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 = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.6", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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 = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[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 = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[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 = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", +] + +[[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 = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +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 = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[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.118", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "once_cell", + "sha2 0.10.9", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "keccak-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5dc2c0d691cbf7595cde551ced329cca99c2387c2cbc97754c5d0cd045d3ee" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[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 = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[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", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "nybbles" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" +dependencies = [ + "alloy-rlp", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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", + "sha2 0.11.0", +] + +[[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.118", +] + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[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 = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve 0.14.1", + "once_cell", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[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-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", +] + +[[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_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[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.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[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 = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "ruint" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45caf26f647c19115bf9c453c70ffe4a4a3a6390dceebd942610584f99b8ddce" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "ark-ff 0.6.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.6", + "rand 0.9.4", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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", + "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.0", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "secp256k1-sys 0.10.1", +] + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.4", + "secp256k1-sys 0.11.0", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + +[[package]] +name = "sha3-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6287fd675f713484342a89cbf0a386abef5f15919cfad607e5e1f19e1e15331" +dependencies = [ + "cc", + "cfg-if", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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 = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +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" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[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", + "tracing", +] + +[[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 = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[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 = "tvc-cosign" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "axum", + "base64", + "hex", + "hkdf", + "p256", + "serde", + "serde_json", + "sha2 0.11.0", + "thiserror", + "tokio", + "toml", + "zeroize", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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 = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +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.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tvc-cosign/Cargo.toml b/tvc-cosign/Cargo.toml new file mode 100644 index 0000000..1859ba1 --- /dev/null +++ b/tvc-cosign/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "tvc-cosign" +version = "0.1.0" +edition = "2024" + +[dependencies] +alloy-consensus = "2.1.1" +alloy-eips = "2.1.1" +alloy-primitives = { version = "1.6.0", features = ["serde"] } +alloy-rlp = "0.3.16" +axum = "0.8.9" +base64 = "0.22.1" +hex = "0.4.3" +hkdf = "0.13.0" +p256 = { version = "0.14.0", features = ["ecdsa"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +sha2 = "0.11.0" +thiserror = "2.0.18" +tokio = { version = "1.52.3", features = ["full"] } +toml = "1.1.2" +zeroize = { version = "1.9.0", features = ["alloc"] } + +# Release profile tuned for a small, deterministic enclave binary. `strip` +# removes symbols/debug info (which carry build-machine paths), so the pivot +# digest is reproducible across build hosts. +[profile.release] +strip = true +lto = true +codegen-units = 1 +panic = "abort" diff --git a/tvc-cosign/Dockerfile b/tvc-cosign/Dockerfile new file mode 100644 index 0000000..42c9f1d --- /dev/null +++ b/tvc-cosign/Dockerfile @@ -0,0 +1,52 @@ +# syntax=docker/dockerfile:1 +# ── Build stage ────────────────────────────────────────────────────────────── +# Reproducible builds require pinning every input by digest. The builder image +# below is already pinned by @sha256:. To re-pin it (e.g. after bumping the Rust +# version): +# docker pull rust:1.94-alpine +# docker inspect rust:1.94-alpine --format='{{index .RepoDigests 0}}' +# then replace the @sha256:... on the FROM line below. Keep the Rust version +# aligned with rust-toolchain / your local toolchain for output parity. +FROM rust:1.94-alpine@sha256:77237dd363a0b127bb5ef532c2d64c0deb380b738e43a9c4bdac73398d6d0a08 AS builder + +# musl-dev supplies the headers + static C runtime the linker needs. Alpine's +# default target is x86_64-unknown-linux-musl, so cargo produces a fully static +# binary with no libc dependency. +RUN apk add --no-cache musl-dev + +WORKDIR /app +# Copy only what the build needs (see .dockerignore) so the image can't pick up +# stray host files and the digest stays a function of the committed source. +COPY Cargo.toml Cargo.lock ./ +COPY src ./src +# rules.toml is compiled into the binary via include_str! (see src/rules.rs), so it +# must be present in the build context. +COPY rules.toml ./rules.toml + +# --locked builds against the committed Cargo.lock exactly (no dependency drift). +RUN cargo build --release --locked + +# ── Runtime stage ──────────────────────────────────────────────────────────── +# StageX busybox is itself reproducibly built and provides the /bin/sh that TVC's +# containerd requires to start the task. There are deliberately NO ca-certificates +# and no libc: the binary is static and makes ZERO network egress — it only stamps +# requests, so it has nothing to reach out to. That is a security property, not an +# omission (see README "No egress"). +FROM stagex/core-busybox:1.36.1@sha256:cac5d773db1c69b832d022c469ccf5f52daf223b91166e6866d42d6983a3b374 + +COPY --from=builder /app/target/release/tvc-cosign /tvc-cosign + +# The ruleset is compiled into the binary, so it is covered by expectedPivotDigest. +# There is deliberately no rules.toml file in this image. + +# Print the pivot binary digest at build time. This is the `expectedPivotDigest` +# you record in the TVC deployment manifest. +RUN sha256sum /tvc-cosign | awk '{print "expectedPivotDigest=sha256:" $1}' + +EXPOSE 3000 + +# For local `docker run`. In a TVC deployment these are overridden by the +# manifest's pivotPath (/tvc-cosign) + pivotArgs, where you also add +# --organization-id (see README). +ENTRYPOINT ["/tvc-cosign"] +CMD ["--port", "3000"] diff --git a/tvc-cosign/README.md b/tvc-cosign/README.md new file mode 100644 index 0000000..a21001f --- /dev/null +++ b/tvc-cosign/README.md @@ -0,0 +1,546 @@ +# tvc-cosign + +A [Turnkey Verifiable Cloud (TVC)](https://docs.turnkey.com/features/verifiable-cloud/overview) +pivot binary that runs inside an AWS Nitro Enclave and exposes a single +`POST /cosign` endpoint. It parses an unsigned EVM transaction, classifies it +against a baked-in ruleset, and returns a **stamped** Turnkey `SIGN_TRANSACTION` +activity request signed by one of two keys deterministically derived from the +enclave's quorum key: `programmatic` or `admin`. The customer submits that +request to Turnkey, where policies either auto-complete it (programmatic) or hold +it for human approval (admin). + +The app **never holds Turnkey credentials and makes zero network egress**, it +only stamps. All trust flows from the quorum-key-derived API keys plus Turnkey +policies, and every decision is accompanied by a verifiable **App Proof**. + +```mermaid +%%{init: {'themeVariables': {'actorBorder':'#4C48FF','actorLineColor':'#4C48FF','noteBorderColor':'#4C48FF', 'labelBoxBorderColor': '#4C48FF'}}}%% +sequenceDiagram + participant C as Customer + participant E as TVC app (Nitro Enclave) + participant T as Turnkey API + + C->>E: POST /cosign (unsignedTransaction, signerAddress) + Note over E: parse → classify → build SIGN_TRANSACTION_V2
→ stamp (prog OR admin key) → App-Proof + + alt PROGRAMMATIC + E-->>C: 200 (activityBody, xStamp, appProof, bootEphemeralKey) + C->>T: submit activityBody + xStamp + T-->>C: policy ALLOW → COMPLETED + else ADMIN + E-->>C: 200 (activityBody, xStamp, appProof, bootEphemeralKey) + C->>T: submit activityBody + xStamp + T-->>C: human consensus → CONSENSUS_NEEDED + else REJECT + E-->>C: 400 (no stamp) + end +``` + +--- + +## Contents + +- [How it works](#how-it-works) +- [Endpoints](#endpoints) +- [Local development](#local-development) +- [Reproducible build](#reproducible-build) +- [Deploy to TVC](#deploy-to-tvc) +- [Turnkey org setup: users + policies](#turnkey-org-setup-users--policies) +- [Integration example](#integration-example) +- [Verifying the proofs](#verifying-the-proofs) +- [Configuration](#configuration) +- [Limitations & operational considerations](#limitations--operational-considerations) + +--- + +## How it works + +The quorum key is **stable across deployments** and exposed inside the enclave at +`/qos.quorum.key`. Two independent API keys are HKDF-derived from it: + +``` +prog_key = P256(HKDF-SHA512(salt="tvc-cosign-programmatic-v1", ikm=quorum_seed)) +admin_key = P256(HKDF-SHA512(salt="tvc-cosign-admin-v1", ikm=quorum_seed)) +``` + +Because the seed is stable, these public keys are stable and are registered as +Turnkey API users **once** (read them from `GET /pubkeys`). Classification: + +- **PROGRAMMATIC**: an ERC-20 `transfer` whose token, recipient, and amount all + pass the ruleset → stamped with `prog_key`. +- **ADMIN**: a privileged selector on the admin allowlist → stamped with + `admin_key`. +- **REJECT**: anything else, or a signer not on the allowlist → `400`, no stamp. + +Separately, each enclave has a per-boot **ephemeral key** (`/qos.ephemeral.key`, +one per replica) whose public half is attested in the enclave's **Boot Proof**. +Every `/cosign` response carries an **App Proof** signed by that key, committing +to the exact decision. See [Verifying the proofs](#verifying-the-proofs). + +## Endpoints + +| Endpoint | Purpose | +|---|---| +| `GET /health` | Liveness probe → `200 {"status":"ok"}` (required by TVC). | +| `GET /pubkeys` | `{ programmatic, admin }`: the two stamping public keys to register as API users. Stable across replicas; fetch once. | +| `POST /cosign` | In: `{ unsignedTransaction, signerAddress }`. Out: `{ activityBody, xStamp, classification, appProof, bootEphemeralKey }`. | + +--- + +## Local development + +```bash +# Build + run the full test suite (crypto is pinned by known-answer tests). +cargo test + +# Create a ruleset from the example and run locally. +cp rules.example.toml rules.toml +cargo run -- --organization-id "$YOUR_ORG_ID" --rules-path rules.example.toml + +# In another shell: +curl -s localhost:3000/health +curl -s localhost:3000/pubkeys | jq + +# Cosign an ERC-20 transfer (matches rules.example.toml). +TX=02f862018080808094111111111111111111111111111111111111111180b844a9059cbb00000000000000000000000000000000000000000000000000000000000000ff00000000000000000000000000000000000000000000000000000000000001f4c0 +curl -s -X POST localhost:3000/cosign -H 'content-type: application/json' \ + -d "{\"unsignedTransaction\":\"$TX\",\"signerAddress\":\"0x00000000000000000000000000000000000000a1\"}" | jq +``` + +> Outside an enclave, `/qos.quorum.key` and `/qos.ephemeral.key` are absent, so +> the app falls back to **insecure dev seeds** and warns loudly. The real keys +> (and therefore the registered pubkeys) only appear once deployed, read them +> from `GET /pubkeys` on the live enclave. + +--- + +## Reproducible build + +A TVC app boots by extracting and running **only the pivot binary** from the +container image (measured as `expectedPivotDigest`); the rest of the image +filesystem is never mounted in the enclave. The QOS manifest pins that binary's +digest, so if the binary changes between builds, even from identical source, +attestation fails. Because the container filesystem is not available at runtime, +the **ruleset is compiled into the binary** (`include_str!("rules.toml")`, see +`src/rules.rs`) rather than shipped as a file, which also means it is covered by +the attested `expectedPivotDigest`. Every build input is pinned: + +| Input | How it's pinned | +|---|---| +| Rust toolchain | `rust:1.94-alpine`, pinned by `@sha256:` in `Dockerfile` (re-pin if you bump the version; see the comment in the file) | +| Runtime base | `stagex/core-busybox:1.36.1@sha256:cac5d773…` (StageX is itself reproducible) | +| Rust dependencies | `Cargo.lock`, committed; built with `--locked` | +| Ruleset | `rules.toml` compiled into the binary via `include_str!`, so it is covered by `expectedPivotDigest` | +| Symbols / paths | `[profile.release] strip = true` removes build-machine paths from the binary | + +The runtime image ships **no ca-certificates and no libc**, the binary is static +and makes no egress (see [Limitations](#limitations--operational-considerations)). + +### Step 1 — Set your ruleset (compiled into the binary) + +`rules.toml` at the crate root is compiled into the binary at build time +(`include_str!`), so it becomes part of the attested `expectedPivotDigest` and is +present in the enclave (a file baked into the image would not be — TVC runs only +the pivot binary). Copy the example, then open `rules.toml` in your editor and +fill in your real allowlists before building: + +```bash +cp rules.example.toml rules.toml +# then edit rules.toml +``` + +> `rules.toml` must exist at the crate root for the build to compile. Changing it +> changes the binary, and therefore `expectedPivotDigest`, which is exactly what +> makes the ruleset attestable. + +The builder toolchain is pinned by digest in the `Dockerfile` +(`FROM rust:1.94-alpine@sha256:…`). That pin is what makes the build bit-for-bit +reproducible, so anyone can rebuild from this source and confirm the exact +`expectedPivotDigest`. If you bump the Rust version, re-pin it (see the comment at +the top of the `Dockerfile`). + +### Step 2 — Build and push to a container registry + +GHCR is used here only as an example. Any OCI-compliant registry works +(Docker Hub, Amazon ECR, GCP Artifact Registry, a self-hosted registry) as long +as TVC can pull the image **by digest** and it's a standard `linux/amd64` OCI +image. Substitute your registry host/namespace for `ghcr.io/YOUR_GITHUB_USERNAME` +throughout (and use that registry's login instead of `docker login ghcr.io`). + +```bash +# Create a GitHub Personal Access Token with the `write:packages` scope: +# https://github.com/settings/tokens/new?scopes=write:packages +# Read it without it landing in shell history (-s silences echo; `read` is a +# builtin so the value is never a command argument). +read -s GITHUB_TOKEN && export GITHUB_TOKEN +echo "$GITHUB_TOKEN" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin + +# Build for linux/amd64 (required by Nitro Enclaves) and push. +# --provenance=false --sbom=false keeps the push a single image manifest instead of +# wrapping it in a multi-arch index, so there is exactly one digest to pin. +docker buildx build --platform linux/amd64 --provenance=false --sbom=false \ + -t ghcr.io/YOUR_GITHUB_USERNAME/tvc-cosign:latest --push . +``` + +Make the package public so the enclave can pull it without a pull secret: +GitHub → Packages → `tvc-cosign` → Package settings → Change visibility → Public. +(If you keep it private, you must set `pivotContainerEncryptedPullSecret` in the +deploy config instead.) + +### Step 3 — Capture the two digests + +TVC pins **both** the container image and the pivot binary inside it. + +```bash +# (a) Container image digest -> pivotContainerImageUrl. With the single-manifest +# build above, this is just the top-level `Digest:` (MediaType +# ...manifest.v2+json). If you built without --provenance=false, the output is an +# index instead and you pick the child whose line says `Platform: linux/amd64`. +docker buildx imagetools inspect ghcr.io/YOUR_GITHUB_USERNAME/tvc-cosign:latest +# This prints a `Digest: sha256:...` line (not the full URL). Assemble +# pivotContainerImageUrl by joining the image name (`Name: ghcr.io/YOUR_GITHUB_USERNAME/tvc-cosign:latest`) to that digest, placing the `:latest` at the end with `@` + the digest value: +# ghcr.io/YOUR_GITHUB_USERNAME/tvc-cosign@sha256: + +# (b) Pivot binary digest -> expectedPivotDigest. The build already prints it as +# `expectedPivotDigest=sha256:...`; to recompute from the image: +docker create --platform linux/amd64 --name tvc-extract \ + ghcr.io/YOUR_GITHUB_USERNAME/tvc-cosign:latest /bin/true \ + && docker cp tvc-extract:/tvc-cosign ./tvc-cosign.bin && docker rm tvc-extract +sha256sum ./tvc-cosign.bin +``` + +> **Source provenance.** Build from a clean, committed tree and record the git +> commit (ideally an annotated tag) the image was built from, and publish it +> alongside the deployment, since nothing in the registry infers it. To verify: +> `git checkout `, rebuild Steps 1–3, and confirm the pivot binary +> `sha256` equals the `expectedPivotDigest` in the enclave's attested QOS manifest +> ([Verifying the proofs](#verifying-the-proofs)). With the builder pinned by +> digest + `Cargo.lock` + `--locked`, that binary digest is deterministic from the +> source. Note `rules.toml` is compiled into the binary (and is **not** committed, +> it is per-deployment), so it also determines the digest: to reproduce a specific +> deployment, a verifier needs both that commit **and** that deployment's exact +> `rules.toml`. Publish your `rules.toml` alongside the deployment if you want +> third parties to reproduce your `expectedPivotDigest`. + +--- + +## Deploy to TVC + +Requires the [`tvc` CLI](https://docs.turnkey.com/getting-started/verifiable-cloud-quickstart) +(`cargo install tvc`) and TVC access enabled for your org. The steps below are the +CLI flow; some parts (creating the app, creating the deployment) can also be done +from the Turnkey dashboard. See the +[Verifiable Cloud quickstart](https://docs.turnkey.com/features/verifiable-cloud/quickstart#create-your-first-verifiable-app) +for the dashboard walkthrough. Approval is always done via the `tvc` CLI. + +### Step 4 — Create the TVC app + +```bash +tvc login # generates an operator keypair +tvc app init --output app.json # set "name": "tvc-cosign" +tvc app create --config-file app.json +``` + +### Step 5 — Create the deployment + +```bash +tvc deploy init # writes deploy-.json +``` + +Edit the generated `deploy-.json`: + +```jsonc +{ + "qosVersion": "0.12.0", // LatestQosReleaseVersion + "pivotContainerImageUrl": "ghcr.io/YOUR_GITHUB_USERNAME/tvc-cosign@sha256:", + "pivotPath": "/tvc-cosign", + "pivotArgs": ["--organization-id", "", "--port", "3000"], + "expectedPivotDigest": "", + "healthCheckType": "TVC_HEALTH_CHECK_TYPE_HTTP", + "healthCheckPort": 3000, + "publicIngressPort": 3000, + "dangerousDeployDebugMode": false + // remove pivotContainerEncryptedPullSecret (image is public, see below) +} +``` + +**On `pivotContainerEncryptedPullSecret`:** `tvc deploy init` always generates this +line with the placeholder `""`. It is +only needed to pull the image from a **private** registry. If your image is public, +delete the line entirely, otherwise `tvc deploy create` rejects the config with a +placeholder error. (If you keep the image private, supply the secret with +`--pivot-pull-secret ` instead.) In this demo we made the ghcr image public +in Step 2, so **remove the line**. + +Also keep `dangerousDeployDebugMode: false` for any real deployment: debug mode +disables normal attestation enforcement (PCRs come back zeroed), which invalidates +the boot and app proofs. + +```bash +tvc deploy create --config-file deploy-.json +# → prints Deployment ID and App ID; copy the Deployment ID. +``` + +`--organization-id` rides in `pivotArgs`, so it is recorded in the QOS manifest +and **attested**, so the deployment provably stamps only for that org. The ruleset +is compiled into the binary, covered by `expectedPivotDigest`. One deployment = one +org + one ruleset. + +### Step 6 — Approve the manifest + +Passing `--deploy-id` is enough: `tvc deploy approve` fetches the manifest for that +deployment (so it resolves the manifest ID itself) and resolves the operator ID and +operator seed from your logged-in tvc profile (`~/.config/turnkey`, where +`tvc app create` cached them). It then walks you through the interactive approval +and posts it. + +```bash +tvc deploy approve --deploy-id +``` + +You only need the extra flags in specific cases: + +- `--operator-id ` if your profile has **more than one** saved operator + (otherwise it auto-selects the single one, or prompts interactively). The operator + ID is printed by `tvc app create` as "Manifest Set Operator IDs" and stored under + `last_operator_ids` in `~/.config/turnkey`; it is **not** shown by `deploy status`. +- `--manifest-id ` only if you approve from a manifest file + (`--manifest `) instead of `--deploy-id`. When needed, the manifest ID *is* + shown by `tvc deploy status --deploy-id `. +- `--dangerous-skip-interactive` if you run without a TTY (CI); otherwise the + interactive approval prompts require a terminal. + +### Step 7 — Go live + +The deployment reaches **LIVE** a few minutes after approval: + +```bash +tvc deploy status --deploy-id # wait for LIVE +curl https://app-.turnkey.cloud/health # → {"status":"ok"} +curl https://app-.turnkey.cloud/pubkeys # the REAL stamping keys +``` + +The `/pubkeys` values are the quorum-derived keys you register as API users in +[Turnkey org setup](#turnkey-org-setup-users--policies). + +--- + +## Turnkey org setup: users + policies + +Do this once, against the org you passed as `--organization-id` (the org that owns +the signing wallets, the two API users, and the policies). + +**1. A wallet** whose account address is your `signerAddress` / `signWith` +target, and which appears in `allowed_signers` in `rules.toml`. + +**2. Two API-only users**, with API public keys taken verbatim from +`GET /pubkeys` on the live enclave (curve `API_KEY_CURVE_P256`): + +| User | API public key | +|---|---| +| `TVC programmatic` | `pubkeys.programmatic` | +| `TVC admin` | `pubkeys.admin` | + +**3. Two policies.** The engine is default-deny, so each policy only grants a +specific ALLOW. (Field syntax: [policy language](https://docs.turnkey.com/features/policies/language).) + +Programmatic → auto-complete (self-consensus by the programmatic user): + +```json +{ + "policyName": "TVC programmatic: allow signing", + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(user, user.id == '')", + "condition": "activity.action == 'SIGN'" +} +``` + +Admin → require human consensus (holds at `CONSENSUS_NEEDED` until the named +humans approve; they get implicit approve permission by being in `consensus`): + +```json +{ + "policyName": "TVC admin: require 2 human approvers", + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(user, user.id == '') && approvers.any(user, user.id == '') && approvers.any(user, user.id == '')", + "condition": "activity.action == 'SIGN'" +} +``` + +The two policies are disjoint by initiator: a programmatic-stamped activity never +matches the admin policy and vice-versa. To tighten further (belt-and-suspenders +with the enclave-side signer allowlist), extend `condition`, e.g. +`activity.action == 'SIGN' && wallet.id == ''`, or scope by +`eth.tx.to`. For a larger human quorum, tag your approvers and use +`approvers.filter(user, user.tags.contains('')).count() >= N`. + +--- + +## Integration example + +The customer calls `/cosign`, then forwards the result to Turnkey. **Send +`activityBody` verbatim**, the stamp covers those exact bytes, so any +re-serialization breaks it. `classification` is informational (client-side only). + +```bash +OUT=$(curl -s -X POST "$TVC_URL/cosign" -H 'content-type: application/json' \ + -d "{\"unsignedTransaction\":\"$TX\",\"signerAddress\":\"$SIGNER\"}") + +curl -s https://api.turnkey.com/public/v1/submit/sign_transaction \ + -H 'Content-Type: application/json' \ + -H "X-Stamp: $(printf '%s' "$OUT" | jq -r .xStamp)" \ + --data "$(printf '%s' "$OUT" | jq -r .activityBody)" # verbatim body +``` + +```javascript +const cosign = await fetch(`${TVC_URL}/cosign`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ unsignedTransaction, signerAddress }), +}).then((r) => r.json()); + +// Forward to Turnkey. activityBody is sent as-is; xStamp goes in the header. +const res = await fetch( + "https://api.turnkey.com/public/v1/submit/sign_transaction", + { + method: "POST", + headers: { "Content-Type": "application/json", "X-Stamp": cosign.xStamp }, + body: cosign.activityBody, // do NOT JSON.parse/stringify; send verbatim + }, +).then((r) => r.json()); + +switch (res.activity.status) { + case "ACTIVITY_STATUS_COMPLETED": { + const signed = res.activity.result.signTransactionResult.signedTransaction; + // broadcast `signed` + break; + } + case "ACTIVITY_STATUS_CONSENSUS_NEEDED": { + // admin path: notify human approvers; poll get_activity or use webhooks. + const activityId = res.activity.id; + break; + } +} +``` + +--- + +## Verifying the proofs + +Every `/cosign` response includes an `appProof` and a `bootEphemeralKey`. Together +with the enclave's Boot Proof they prove that **this attested code classified this +transaction this way**, independent of Turnkey and of this app's operator. + +`appProof` is the standard Turnkey App Proof envelope: + +```json +{ + "scheme": "SIGNATURE_SCHEME_EPHEMERAL_KEY_P256", + "publicKey": "04…", // uncompressed SEC1 sign key (65 bytes) + "proofPayload": "{\"type\":\"APP_PROOF_TYPE_COSIGN_DECISION\", …}", + "signature": "30…" // P-256 / SHA-256 / DER over proofPayload +} +``` + +The `proofPayload` commits to `organizationId`, `signerAddress`, +`unsignedTransaction`, `classification`, `stampedWith` (which API key stamped), +and `activityBodySha256` (SHA-256 of the exact submitted body). + +To verify: + +1. **App Proof signature**: verify `signature` over the raw `proofPayload` + string bytes (ECDSA P-256, SHA-256) against `publicKey`. If it checks out, the + decision is intact and was produced by the holder of that ephemeral key. +2. **Boot Proof**: fetch it for the replica that answered, using the response's + `bootEphemeralKey` (per-replica; **use the value from the same response**): + + ``` + POST https://api.turnkey.com/public/v1/query/get_boot_proof + { "organizationId": "", "ephemeralKey": "" } + ``` +3. **Link them**: `bootProof.ephemeralPublicKeyHex` ends with + `appProof.publicKey` (the boot key is `encryptPub ‖ signPub`; the sign half is + the App Proof key). +4. **Confirm the code**: `bootProof.awsAttestationDocB64` is an AWS-signed COSE + document with the enclave's PCRs, and `bootProof.qosManifestB64` contains the + pivot binary hash, compare it to your `expectedPivotDigest` from the build. + +Turnkey publishes verification tooling so you don't hand-roll steps 1–4: +[`turnkey_proofs`](https://crates.io/crates/turnkey_proofs) (Rust), +[`@turnkey/crypto` `proof.ts`](https://github.com/tkhq/sdk/blob/main/packages/crypto/src/proof.ts) +(JS), and the [Go SDK](https://github.com/tkhq/go-sdk/tree/main/pkg/proofs). + +> **Replicas.** A production TVC runs multiple replicas, each with its own +> ephemeral key, and requests are load-balanced across them. Always verify using +> the `appProof.publicKey` / `bootEphemeralKey` returned **in that same response** +> and never a cached one from a different call. + +--- + +## Configuration + +All runtime config is passed as CLI arguments (`pivotArgs` in a deployment). None +of it is secret; the only secrets are the enclave-provided key files. + +| Argument | Default | Meaning | +|---|---|---| +| `--organization-id ` | none (warns, empty) | The org placed in every `SIGN_TRANSACTION_V2` body (owns the wallets + API users + policies). Attested via `pivotArgs`. | +| `--rules-path ` | embedded ruleset | **Local-dev override only.** Loads a ruleset TOML from disk instead of the one compiled into the binary; a deployment does not use this (the file would not exist in the enclave). If the path fails to load, the app falls back to the embedded ruleset, never to deny-all. | +| `--port ` | `3000` | Listen port (binds `0.0.0.0`). | + +The ruleset a deployment enforces is the `rules.toml` **compiled into the binary** +(`include_str!`), covered by `expectedPivotDigest`. For local dev, `--rules-path` / +`TVC_RULES_PATH` can point at a different file, and `TVC_ORGANIZATION_ID` is honored +(a TVC deployment cannot inject env vars). See `rules.example.toml` for the ruleset +format: `allowed_signers`, and a `[programmatic]` block (`allowed_tokens`, +`allowed_recipients`, `max_amount`) plus `[admin] selectors`. + +--- + +## Limitations & operational considerations + +This is a POC. Known scope limits, all deliberate: + +- **Quorum-key provisioning: the stamping keys are not yet secret.** TVC currently + provisions every app with a **static, well-known quorum key** (custom + provisioning is "coming soon"). The programmatic and admin stamping keys are + HKDF-derived from that quorum key with public salts, so today anyone who knows + the well-known quorum key can re-derive both private keys. The programmatic + path's safety therefore does not rest on key secrecy: a party who derives the + programmatic key could stamp an arbitrary `SIGN_TRANSACTION` that the + programmatic policy (`activity.action == 'SIGN'`) allows, bypassing the enclave + ruleset (the ruleset only binds when the enclave itself stamps). Separately, a + quorum-key signature is not enclave-exclusive by design (the quorum key can be + provisioned into any conforming enclave), which is why enclave-exclusivity comes + from the **App Proof** (signed by the per-boot Ephemeral Key), not from the + stamp. Treat this deployment as **testnet / demo only** until custom (secret) + quorum-key provisioning is available; only then do the derived keys become + secret and the "only the attested enclave can stamp" property hold. Interim + mitigations: tighten the Turnkey programmatic policy (constrain `eth.tx.to` / + wallet / chain) so a leaked key can sign less, and verify the App Proof + out-of-band before acting on a stamp. +- **No caller authentication on `/cosign`.** Access is network-perimeter only. + Anyone who can reach the endpoint can request a stamp; safety comes from the + ruleset + the Turnkey-side policies, not from authenticating the caller. Put it + behind your own authenticated ingress. +- **`max_amount` is a single global cap**, interpreted in the token's base units, + it ignores per-token decimals and USD value. Easy extension: per-token caps in + `rules.toml`. +- **No price/velocity/cumulative limits.** Each `/cosign` is stateless, so a + per-transaction cap is not a spending limit. Rate/velocity limits would need + state (in tension with the stateless, attestable design) or a Turnkey-side + policy. +- **ERC-20 `transfer` only** for the programmatic path; native-ETH transfers and + other selectors are REJECT (admin selectors excepted). Contract-creation is + out of scope. +- **One deployment = one org + one ruleset.** New contracts/rules mean a new + deployment (and a new attested image), not new individual policies. +- **No network egress, by design.** The app only stamps, so it needs nothing + external; the image ships without ca-certificates. Egress would only be + justified to add on-chain / sanctions / price-oracle rules, and TVC external + connectivity is a separate feature. +- **Verification status.** The stamp construction is validated live against + Turnkey, and the ephemeral-key / App-Proof construction is validated + byte-for-byte against Turnkey's production reference. The full live round-trip + (submit → policy outcome → Boot-Proof verification) is exercised once deployed + to a real enclave. diff --git a/tvc-cosign/rules.example.toml b/tvc-cosign/rules.example.toml new file mode 100644 index 0000000..9fc8886 --- /dev/null +++ b/tvc-cosign/rules.example.toml @@ -0,0 +1,39 @@ +# tvc-cosign ruleset — example. +# +# Copy to `rules.toml` (or point TVC_RULES_PATH at it) and fill in real values. +# In production this file is baked into the reproducible enclave image, so the +# rules are covered by the image measurement (i.e. attestable). +# +# Classification: +# PROGRAMMATIC -> stamped with the programmatic key (policy auto-completes) +# ADMIN -> stamped with the admin key (policy requires human consensus) +# REJECT -> not stamped; /cosign returns 400 + +# Wallets this deployment may sign for (the `signerAddress` / `signWith` target). +# A global gate: any transaction whose signer is not listed here is REJECTed, +# regardless of the rules below. +allowed_signers = [ + "0x00000000000000000000000000000000000000a1", # e.g. the company treasury wallet +] + +[programmatic] +# An ERC-20 transfer(address,uint256) is PROGRAMMATIC only if ALL hold: +# - the token contract (the tx `to`) is in allowed_tokens +# - the recipient (first arg) is in allowed_recipients +# - the amount (second arg) is <= max_amount +allowed_tokens = [ + "0x1111111111111111111111111111111111111111", # e.g. USDC +] +allowed_recipients = [ + "0x00000000000000000000000000000000000000ff", # e.g. treasury / settlement address +] +# Max transfer amount in the token's base units, as a string (may exceed i64). +# Example below is 1000 * 10^6 for a 6-decimal token like USDC. +max_amount = "1000000000" + +[admin] +# Any transaction whose 4-byte selector is listed here -> ADMIN (human consensus). +# Put privileged operations here (e.g. an "add ticket" / config selector). +selectors = [ + "0x12345678", +] diff --git a/tvc-cosign/src/activity.rs b/tvc-cosign/src/activity.rs new file mode 100644 index 0000000..1665f19 --- /dev/null +++ b/tvc-cosign/src/activity.rs @@ -0,0 +1,156 @@ +//! Build a Turnkey `SIGN_TRANSACTION_V2` activity request body. +//! +//! This JSON is what we stamp and the customer submits to Turnkey. The shape and +//! field names are verified against Turnkey's API reference +//! (`POST /public/v1/submit/sign_transaction`): +//! +//! { type, timestampMs, organizationId, +//! parameters: { signWith, unsignedTransaction, type } } +//! +//! `unsignedTransaction` is the raw Ethereum transaction as hex WITHOUT a `0x` +//! prefix (matching Turnkey's SDKs). We accept either form on input and +//! normalize to no-prefix lowercase so the stamped bytes are deterministic. + +use serde::Serialize; + +const ACTIVITY_TYPE_SIGN_TRANSACTION: &str = "ACTIVITY_TYPE_SIGN_TRANSACTION_V2"; +const TRANSACTION_TYPE_ETHEREUM: &str = "TRANSACTION_TYPE_ETHEREUM"; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Parameters { + sign_with: String, + unsigned_transaction: String, + #[serde(rename = "type")] + tx_type: &'static str, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct SignTransactionActivity { + #[serde(rename = "type")] + activity_type: &'static str, + timestamp_ms: String, + organization_id: String, + parameters: Parameters, +} + +/// Inputs needed to build a sign-transaction request. +pub struct SignTransaction<'a> { + /// The customer's (sub-)organization id. + pub organization_id: &'a str, + /// Wallet account address / private key address / id to sign with. + pub sign_with: &'a str, + /// Raw unsigned Ethereum transaction, hex (with or without `0x`). + pub unsigned_transaction: &'a str, + /// Request timestamp in milliseconds (Turnkey uses it for liveness). + pub timestamp_ms: u64, +} + +/// Serialize a `SIGN_TRANSACTION_V2` activity to its exact JSON body. +/// +/// The returned string is both stamped and sent — Turnkey re-hashes these exact +/// bytes, so it must be produced once and used for both. +pub fn build_sign_transaction(req: &SignTransaction) -> String { + let activity = SignTransactionActivity { + activity_type: ACTIVITY_TYPE_SIGN_TRANSACTION, + timestamp_ms: req.timestamp_ms.to_string(), + organization_id: req.organization_id.to_string(), + parameters: Parameters { + sign_with: req.sign_with.to_string(), + unsigned_transaction: strip_0x(req.unsigned_transaction).to_ascii_lowercase(), + tx_type: TRANSACTION_TYPE_ETHEREUM, + }, + }; + serde_json::to_string(&activity).expect("activity serializes") +} + +/// Strip a leading `0x`/`0X` if present. +fn strip_0x(s: &str) -> &str { + s.strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::Value; + + fn sample() -> SignTransaction<'static> { + SignTransaction { + organization_id: "org-123", + sign_with: "0xabc0000000000000000000000000000000000001", + unsigned_transaction: "0xDEADBEEF", + timestamp_ms: 1_700_000_000_000, + } + } + + #[test] + fn body_matches_turnkey_shape() { + let body = build_sign_transaction(&sample()); + let v: Value = serde_json::from_str(&body).unwrap(); + + assert_eq!(v["type"], "ACTIVITY_TYPE_SIGN_TRANSACTION_V2"); + assert_eq!(v["timestampMs"], "1700000000000"); // string, not number + assert_eq!(v["organizationId"], "org-123"); + assert_eq!(v["parameters"]["signWith"], sample().sign_with); + assert_eq!(v["parameters"]["type"], "TRANSACTION_TYPE_ETHEREUM"); + } + + #[test] + fn unsigned_transaction_is_normalized() { + let body = build_sign_transaction(&sample()); + let v: Value = serde_json::from_str(&body).unwrap(); + // 0x stripped, lowercased. + assert_eq!(v["parameters"]["unsignedTransaction"], "deadbeef"); + } + + #[test] + fn accepts_input_without_0x() { + let mut req = sample(); + req.unsigned_transaction = "deadbeef"; + let body = build_sign_transaction(&req); + let v: Value = serde_json::from_str(&body).unwrap(); + assert_eq!(v["parameters"]["unsignedTransaction"], "deadbeef"); + } + + #[test] + fn build_is_deterministic() { + assert_eq!( + build_sign_transaction(&sample()), + build_sign_transaction(&sample()) + ); + } + + /// The whole point: an activity body can be stamped and the stamp verifies. + #[test] + fn activity_body_can_be_stamped_and_verified() { + use crate::keys::KeySet; + use crate::stamp::stamp; + use base64::Engine; + use p256::ecdsa::signature::Verifier; + use p256::ecdsa::{Signature, VerifyingKey}; + + let body = build_sign_transaction(&sample()); + let keys = KeySet::derive(&[7u8; 32]); + let stamped = stamp(&keys.programmatic, &body); + + let env: Value = serde_json::from_slice( + &base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(&stamped.x_stamp) + .unwrap(), + ) + .unwrap(); + + let vk = VerifyingKey::from_sec1_bytes( + &hex::decode(env["publicKey"].as_str().unwrap()).unwrap(), + ) + .unwrap(); + let sig = + Signature::from_der(&hex::decode(env["signature"].as_str().unwrap()).unwrap()).unwrap(); + + vk.verify(stamped.body.as_bytes(), &sig) + .expect("stamp over the activity body verifies"); + } +} diff --git a/tvc-cosign/src/config.rs b/tvc-cosign/src/config.rs new file mode 100644 index 0000000..84c79e7 --- /dev/null +++ b/tvc-cosign/src/config.rs @@ -0,0 +1,66 @@ +//! Runtime configuration (non-secret), supplied at deploy time. +//! +//! The only secret this app uses is the quorum seed, which is read from +//! `/qos.quorum.key` — never from here. Everything in `Config` is safe to pass +//! as a plain deploy-time argument or as a file baked into the image, and is not +//! secret. +//! +//! In a TVC deployment there is no way to inject environment variables: runtime +//! config arrives through the pivot binary's CLI arguments (`pivotArgs`), which are +//! recorded in the QOS manifest and therefore attested. The organization id is +//! passed as `--organization-id`; the ruleset is compiled into the binary (see +//! [`crate::rules::Ruleset::embedded`]). `--rules-path` / `TVC_RULES_PATH` remain a +//! local-dev override only. + +use crate::rules::Ruleset; + +/// Environment variable holding the customer's (sub-)organization id (dev only; +/// in a deployment this comes from `--organization-id`). +const ORGANIZATION_ID_ENV: &str = "TVC_ORGANIZATION_ID"; +/// Environment variable pointing at a ruleset TOML file (dev-only override). +const RULES_PATH_ENV: &str = "TVC_RULES_PATH"; + +/// Non-secret configuration loaded once at startup. +pub struct Config { + /// The `organizationId` placed in every `SIGN_TRANSACTION_V2` body. + pub organization_id: String, + /// The active classification ruleset. + pub ruleset: Ruleset, +} + +impl Config { + /// Resolve config. The `organizationId` comes from `--organization-id` (or env + /// var for dev); if unset it falls back to an empty placeholder and warns. + /// + /// The ruleset is the one compiled into the binary ([`Ruleset::embedded`]). A + /// `--rules-path` / `TVC_RULES_PATH` override is honored for local dev only, and + /// if it fails to load falls back to the embedded ruleset (never deny-all). + pub fn load(cli_organization_id: Option, cli_rules_path: Option) -> Self { + let organization_id = cli_organization_id + .or_else(|| std::env::var(ORGANIZATION_ID_ENV).ok()) + .unwrap_or_else(|| { + eprintln!( + "config: WARNING organization id not set (--organization-id / \ + {ORGANIZATION_ID_ENV}) — using empty placeholder; set it to submit real \ + requests" + ); + String::new() + }); + + let ruleset = match cli_rules_path.or_else(|| std::env::var(RULES_PATH_ENV).ok()) { + Some(path) => Ruleset::load(&path).unwrap_or_else(|e| { + eprintln!( + "config: WARNING could not load --rules-path {path} ({e}); using the \ + embedded ruleset" + ); + Ruleset::embedded().expect("embedded ruleset (rules.toml) is valid") + }), + None => Ruleset::embedded().expect("embedded ruleset (rules.toml) is valid"), + }; + + Config { + organization_id, + ruleset, + } + } +} diff --git a/tvc-cosign/src/keys.rs b/tvc-cosign/src/keys.rs new file mode 100644 index 0000000..7b76e03 --- /dev/null +++ b/tvc-cosign/src/keys.rs @@ -0,0 +1,323 @@ +//! Key derivation — the trust crux. +//! +//! Inside the enclave, QOS exposes the quorum master seed (stable across +//! deploys) at [`QUORUM_KEY_PATH`]. We HKDF-derive two independent P-256 API +//! keys from it: `programmatic` and `admin`. Because the seed is stable, the +//! derived public keys are stable too, so they can be registered as Turnkey +//! API users exactly once (via `GET /pubkeys`). +//! +//! Format requirements Turnkey enforces (pinned by the tests below): +//! - public key: compressed SEC1 P-256, 33 bytes, `02`/`03` prefix, hex. +//! - (signing format lives in `stamp.rs` — ECDSA-P256 / SHA-256 / DER.) + +use std::path::Path; + +use hkdf::Hkdf; +use p256::SecretKey; +use p256::ecdsa::SigningKey; +use sha2::Sha512; +use zeroize::Zeroizing; + +/// Path inside the enclave where QOS writes the hex-encoded 32-byte quorum seed. +pub const QUORUM_KEY_PATH: &str = "/qos.quorum.key"; + +/// Path inside the enclave where QOS writes the hex-encoded ephemeral key. Like +/// the quorum key this is a 32-byte *master seed* (unique per boot, one per +/// replica), from which the sign/encrypt keys are sub-derived via the QOS KeySet +/// paths below. QOS attests the resulting public keys in the enclave's Boot +/// Proof, so [`EphemeralKey`] must reconstruct them exactly. +pub const EPHEMERAL_KEY_PATH: &str = "/qos.ephemeral.key"; + +/// HKDF salts for our two Turnkey API keys. These domain-separate the keys and +/// are versioned so the derivation can be rotated. Unlike the ephemeral key, we +/// *register* these public keys ourselves (they are not attested), so the salts +/// are ours to choose — validated live against Turnkey. +const PROG_SALT: &[u8] = b"tvc-cosign-programmatic-v1"; +const ADMIN_SALT: &[u8] = b"tvc-cosign-admin-v1"; + +/// QOS KeySet sub-derivation paths — used as the HKDF-SHA512 salt over a master +/// seed. These MUST match `qos_p256/src/lib.rs`, because QOS attests the public +/// keys it derives this way; the ephemeral key reconstruction depends on it. +const QOS_SIGN_PATH: &[u8] = b"qos_p256_sign"; +const QOS_ENCRYPT_PATH: &[u8] = b"qos_p256_encrypt"; + +/// Insecure fixed seed used only when [`QUORUM_KEY_PATH`] is absent (i.e. running +/// outside an enclave, in dev/CI). Keys derived from it are NOT secret. +const DEV_SEED: [u8; 32] = [0x11; 32]; + +/// Insecure fixed ephemeral master seed used only when [`EPHEMERAL_KEY_PATH`] is +/// absent. App Proofs signed with it are NOT attestable — dev/CI only. +const DEV_EPHEMERAL_SEED: [u8; 32] = [0x22; 32]; + +/// A single derived Turnkey API key. +pub struct ApiKey { + signing_key: SigningKey, +} + +impl ApiKey { + /// The P-256 signing key, used to produce Turnkey stamps. + pub fn signing_key(&self) -> &SigningKey { + &self.signing_key + } + + /// Compressed SEC1 public key as lowercase hex — the exact form Turnkey + /// registers as an `API_KEY_CURVE_P256` user public key. + pub fn public_key_hex(&self) -> String { + let point = self.signing_key.verifying_key().to_sec1_point(true); + hex::encode(point.as_bytes()) + } +} + +/// The enclave's per-boot ephemeral key, reconstructed from the QOS master seed +/// at [`EPHEMERAL_KEY_PATH`]. It signs App Proofs (see `proof.rs`); its public +/// half is attested in the enclave's Boot Proof, so the sign/encrypt keys are +/// sub-derived with the exact [`QOS_SIGN_PATH`]/[`QOS_ENCRYPT_PATH`] construction +/// QOS uses. Each of a TVC's replicas boots its own ephemeral key. +pub struct EphemeralKey { + signing_key: SigningKey, + /// Uncompressed SEC1 (`04‖X‖Y`) hex of the sign public key — the form that + /// appears in the App Proof `publicKey` field. + public_key_hex: String, + /// The QOS KeySet hex used to look up this replica's Boot Proof: uncompressed + /// encrypt pubkey ‖ uncompressed sign pubkey (130 bytes). + boot_ephemeral_key_hex: String, +} + +impl EphemeralKey { + /// Reconstruct the ephemeral key from a 32-byte QOS master seed. + pub fn derive(master_seed: &[u8; 32]) -> Self { + let sign_key = derive_signing_key(master_seed, QOS_SIGN_PATH); + let encrypt_key = derive_signing_key(master_seed, QOS_ENCRYPT_PATH); + + let sign_pub = uncompressed_sec1(&sign_key); + let encrypt_pub = uncompressed_sec1(&encrypt_key); + + // Boot Proof lookup key = encryptPub ‖ signPub (matches qos_p256 KeySet). + let mut keyset = encrypt_pub; + keyset.extend_from_slice(&sign_pub); + + EphemeralKey { + signing_key: sign_key, + public_key_hex: hex::encode(&sign_pub), + boot_ephemeral_key_hex: hex::encode(&keyset), + } + } + + /// Load the master seed from the enclave and reconstruct the key. Outside an + /// enclave the file is absent, so we fall back to [`DEV_EPHEMERAL_SEED`] and + /// warn — proofs then carry no attestation value. + pub fn load() -> Self { + match read_hex_seed(Path::new(EPHEMERAL_KEY_PATH)) { + Some(seed) => { + println!("keys: reconstructed ephemeral key from {EPHEMERAL_KEY_PATH}"); + Self::derive(&seed) + } + None => { + eprintln!( + "keys: WARNING {EPHEMERAL_KEY_PATH} not found — using INSECURE dev ephemeral \ + key (not running in an enclave)" + ); + Self::derive(&DEV_EPHEMERAL_SEED) + } + } + } + + /// The P-256 signing key used to sign App Proofs. + pub fn signing_key(&self) -> &SigningKey { + &self.signing_key + } + + /// Uncompressed SEC1 hex of the sign public key (App Proof `publicKey`). + pub fn public_key_hex(&self) -> &str { + &self.public_key_hex + } + + /// The QOS KeySet hex for Boot Proof lookup (`get_boot_proof`'s `ephemeralKey`). + pub fn boot_ephemeral_key_hex(&self) -> &str { + &self.boot_ephemeral_key_hex + } +} + +/// Uncompressed SEC1 encoding (`04‖X‖Y`, 65 bytes) of a signing key's public key. +fn uncompressed_sec1(key: &SigningKey) -> Vec { + key.verifying_key().to_sec1_point(false).as_bytes().to_vec() +} + +/// The pair of keys derived for this enclave. +pub struct KeySet { + pub programmatic: ApiKey, + pub admin: ApiKey, +} + +impl KeySet { + /// Derive both keys from a 32-byte quorum seed. + pub fn derive(quorum_seed: &[u8; 32]) -> Self { + KeySet { + programmatic: derive_key(quorum_seed, PROG_SALT), + admin: derive_key(quorum_seed, ADMIN_SALT), + } + } + + /// Load the quorum seed from the enclave and derive both keys. Outside an + /// enclave the seed file is absent, so we fall back to [`DEV_SEED`] and warn. + pub fn load() -> Self { + match read_hex_seed(Path::new(QUORUM_KEY_PATH)) { + Some(seed) => { + println!("keys: derived from quorum seed at {QUORUM_KEY_PATH}"); + Self::derive(&seed) + } + None => { + eprintln!( + "keys: WARNING {QUORUM_KEY_PATH} not found — using INSECURE dev seed \ + (not running in an enclave)" + ); + Self::derive(&DEV_SEED) + } + } + } +} + +/// HKDF-SHA512-expand a 32-byte seed into a P-256 signing key, keyed by `salt`. +/// The construction (extract with `salt`, expand with empty `info`) matches both +/// our own API-key derivation and QOS's KeySet sub-derivation. +fn derive_signing_key(seed: &[u8; 32], salt: &[u8]) -> SigningKey { + let hk = Hkdf::::new(Some(salt), seed); + let mut okm = Zeroizing::new([0u8; 32]); + hk.expand(&[], &mut okm[..]) + .expect("32 bytes is well under HKDF-SHA512's output limit"); + let secret = SecretKey::from_slice(&okm[..]) + .expect("HKDF output is a valid P-256 scalar (overwhelmingly)"); + secret.into() +} + +/// Derive one of our Turnkey API keys from the quorum seed. +fn derive_key(seed: &[u8; 32], salt: &[u8]) -> ApiKey { + ApiKey { + signing_key: derive_signing_key(seed, salt), + } +} + +/// Read and hex-decode a 32-byte hex seed file (`/qos.quorum.key` or +/// `/qos.ephemeral.key`). Returns `None` if the file is missing or malformed, so +/// callers can fall back gracefully outside an enclave. +fn read_hex_seed(path: &Path) -> Option> { + let contents = Zeroizing::new(std::fs::read_to_string(path).ok()?); + let bytes = Zeroizing::new(hex::decode(contents.trim()).ok()?); + Some(Zeroizing::new(bytes.as_slice().try_into().ok()?)) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SEED_A: [u8; 32] = [0xAB; 32]; + const SEED_B: [u8; 32] = [0xCD; 32]; + + #[test] + fn derivation_is_deterministic() { + let k1 = KeySet::derive(&SEED_A); + let k2 = KeySet::derive(&SEED_A); + assert_eq!( + k1.programmatic.public_key_hex(), + k2.programmatic.public_key_hex() + ); + assert_eq!(k1.admin.public_key_hex(), k2.admin.public_key_hex()); + } + + #[test] + fn prog_and_admin_are_independent() { + let k = KeySet::derive(&SEED_A); + assert_ne!(k.programmatic.public_key_hex(), k.admin.public_key_hex()); + } + + #[test] + fn different_seeds_give_different_keys() { + let a = KeySet::derive(&SEED_A); + let b = KeySet::derive(&SEED_B); + assert_ne!( + a.programmatic.public_key_hex(), + b.programmatic.public_key_hex() + ); + } + + #[test] + fn public_key_is_compressed_sec1() { + let k = KeySet::derive(&SEED_A); + for hexkey in [k.programmatic.public_key_hex(), k.admin.public_key_hex()] { + let bytes = hex::decode(&hexkey).unwrap(); + assert_eq!(bytes.len(), 33, "compressed SEC1 is 33 bytes"); + assert!( + bytes[0] == 0x02 || bytes[0] == 0x03, + "compressed SEC1 prefix must be 02 or 03, got {:#04x}", + bytes[0] + ); + } + } + + #[test] + fn ephemeral_sign_pubkey_is_uncompressed_sec1() { + let e = EphemeralKey::derive(&SEED_A); + let bytes = hex::decode(e.public_key_hex()).unwrap(); + assert_eq!(bytes.len(), 65, "uncompressed SEC1 is 65 bytes"); + assert_eq!(bytes[0], 0x04, "uncompressed SEC1 prefix must be 0x04"); + } + + #[test] + fn boot_ephemeral_key_is_encrypt_then_sign_pubkeys() { + let e = EphemeralKey::derive(&SEED_A); + let keyset = hex::decode(e.boot_ephemeral_key_hex()).unwrap(); + // encryptPub (65) ‖ signPub (65) — matches the QOS KeySet layout. + assert_eq!(keyset.len(), 130); + assert_eq!(keyset[0], 0x04, "encrypt pubkey is uncompressed"); + assert_eq!(keyset[65], 0x04, "sign pubkey is uncompressed"); + // The sign half is exactly the App Proof public key. + let sign_pub = hex::encode(&keyset[65..]); + assert_eq!(sign_pub, e.public_key_hex()); + } + + #[test] + fn ephemeral_derivation_is_deterministic() { + assert_eq!( + EphemeralKey::derive(&SEED_A).boot_ephemeral_key_hex(), + EphemeralKey::derive(&SEED_A).boot_ephemeral_key_hex() + ); + } + + /// Known-answer, cross-checked byte-for-byte against the production Go + /// reference (tvc-chainalysis `buildBootEphemeralKey` / `qos_p256_sign`) for + /// the seed `[0x22; 32]`. If this drifts, App Proofs stop linking to Boot + /// Proofs — the whole verifiability chain silently breaks. + #[test] + fn ephemeral_matches_qos_reference() { + let e = EphemeralKey::derive(&[0x22; 32]); + assert_eq!( + e.public_key_hex(), + "049050ec6740957f5eefab0fdaf858bb33e0bd1e6c0f7128ad5c5cfc0f64db2877\ + 891a0bf1d63c82630abc16adefff48cd0703956e2f265b82ea8df8a99ca58e89" + ); + assert_eq!( + e.boot_ephemeral_key_hex(), + "04bd9e94d0358df8fec7d6d654cc66f46952f2b0f710f76e8478bc1620b32bb024\ + 2a9d1a9dd3c7a5a71526c3bb3be749e4ced4f7d7344af33ef35d3824c8f58d09\ + 049050ec6740957f5eefab0fdaf858bb33e0bd1e6c0f7128ad5c5cfc0f64db2877\ + 891a0bf1d63c82630abc16adefff48cd0703956e2f265b82ea8df8a99ca58e89" + ); + } + + /// Known-answer regression guard: if the derivation ever changes, the + /// registered Turnkey pubkeys would silently break. Pin the exact output. + #[test] + fn known_answer_pins_derivation() { + let k = KeySet::derive(&SEED_A); + assert_eq!( + k.programmatic.public_key_hex(), + "0240e6b810d86b5b378d4379680c28b7a6b409ce02bc8b9bd07779f03e4eed163b", + "programmatic pubkey drifted" + ); + assert_eq!( + k.admin.public_key_hex(), + "025090879edf953832e696931b201502980e6bfa73ee70d3738eab1f64f549c85e", + "admin pubkey drifted" + ); + } +} diff --git a/tvc-cosign/src/main.rs b/tvc-cosign/src/main.rs new file mode 100644 index 0000000..2605a5f --- /dev/null +++ b/tvc-cosign/src/main.rs @@ -0,0 +1,342 @@ +//! TVC `/cosign` POC — an enclave pivot binary that stamps Turnkey activity +//! requests with quorum-key-derived P-256 keys. +//! +//! Endpoints: +//! GET /health — liveness probe. +//! GET /pubkeys — the derived programmatic + admin public keys (register these +//! as Turnkey API users). +//! POST /cosign — classify an unsigned tx and return a stamped +//! SIGN_TRANSACTION_V2 request for the customer to submit. +//! +//! Runtime config comes from CLI arguments (`pivotArgs` in a TVC deployment): +//! --organization-id the (sub-)org to stamp requests for (attested) +//! --rules-path ruleset TOML (default `rules.toml`; baked image +//! deployments pass `/rules.toml`) +//! --port listen port (default 3000) + +mod activity; +mod config; +mod keys; +mod proof; +mod rules; +mod stamp; +mod tx; + +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use alloy_primitives::Address; +use axum::extract::State; +use axum::http::StatusCode; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; + +use activity::{SignTransaction, build_sign_transaction}; +use config::Config; +use keys::{EphemeralKey, KeySet}; +use proof::{AppProof, ProofInputs, app_proof}; +use rules::{Classification, classify}; + +/// Default listen port when `--port` is not supplied. +const DEFAULT_PORT: u16 = 3000; + +/// Usage text printed by `--help` / `-h`. +const HELP: &str = "\ +tvc-cosign — TVC /cosign pivot binary + +USAGE: + tvc-cosign [OPTIONS] + +OPTIONS: + --organization-id (sub-)org to stamp requests for (attested) + --rules-path ruleset TOML override (local dev only; default: embedded) + --port listen port (default: 3000) + -h, --help print this help and exit +"; + +/// Parsed command-line arguments (a TVC deployment supplies these as `pivotArgs`). +struct Args { + organization_id: Option, + rules_path: Option, + port: u16, +} + +/// Take the next arg as a flag's value, but only if it is not itself a flag. +/// Guards against e.g. `--organization-id --port 3000` silently consuming +/// `--port` as the org id. +fn take_value(iter: &mut std::iter::Peekable>) -> Option { + iter.next_if(|v| !v.starts_with("--")) +} + +/// Minimal hand-rolled arg parsing — avoids a CLI dependency for three flags. +/// TVC pivots serve plain HTTP inside the enclave and bind all interfaces. +fn parse_args() -> Args { + let mut args = Args { + organization_id: None, + rules_path: None, + port: DEFAULT_PORT, + }; + let mut iter = std::env::args().skip(1).peekable(); + while let Some(flag) = iter.next() { + match flag.as_str() { + "--help" | "-h" => { + print!("{HELP}"); + std::process::exit(0); + } + "--organization-id" => args.organization_id = take_value(&mut iter), + "--rules-path" => args.rules_path = take_value(&mut iter), + "--port" => { + if let Some(v) = take_value(&mut iter) { + match v.parse() { + Ok(p) => args.port = p, + Err(_) => { + eprintln!("args: WARNING invalid --port {v:?}, using {DEFAULT_PORT}") + } + } + } + } + other => eprintln!("args: WARNING ignoring unknown argument {other:?}"), + } + } + args +} + +/// Shared, read-only application state. +struct AppState { + keys: KeySet, + /// Per-boot ephemeral key that signs App Proofs (see `proof.rs`). + ephemeral: EphemeralKey, + config: Config, +} + +#[tokio::main] +async fn main() { + let args = parse_args(); + + // Derive the enclave's two API keys and the per-boot ephemeral key at boot. + // Enclave stdout is not observable in production, so these prints are only a + // local-dev aid: the API pubkeys are exposed over GET /pubkeys for + // registration, and the ephemeral key (one per replica) has its public half + // pinned in this replica's Boot Proof. + let keys = KeySet::load(); + let ephemeral = EphemeralKey::load(); + println!( + "keys: + programmatic pubkey = {} + admin pubkey = {} + boot ephemeral key = {}", + keys.programmatic.public_key_hex(), + keys.admin.public_key_hex(), + ephemeral.boot_ephemeral_key_hex(), + ); + + let config = Config::load(args.organization_id, args.rules_path); + let state = Arc::new(AppState { + keys, + ephemeral, + config, + }); + + let listen_addr = format!("0.0.0.0:{}", args.port); + let listener = tokio::net::TcpListener::bind(&listen_addr) + .await + .expect("bind listener"); + println!("tvc-cosign listening on {listen_addr}"); + + axum::serve(listener, router(state)).await.expect("serve"); +} + +/// Builds the app router. Takes state so tests can construct it independently. +fn router(state: Arc) -> Router { + Router::new() + .route("/health", get(health)) + .route("/pubkeys", get(pubkeys)) + .route("/cosign", post(cosign)) + .with_state(state) +} + +/// TVC liveness probe. Must return `200`. +async fn health() -> Json { + Json(serde_json::json!({ "status": "ok" })) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PubkeysResponse { + programmatic: String, + admin: String, +} + +/// Serve the quorum-derived stamping keys so an operator can register them as the +/// two Turnkey API users. These are stable across the TVC's replicas (the quorum +/// key is shared), so they only need to be fetched and registered once. The +/// per-replica ephemeral/Boot-Proof key is NOT here — it rides on each `/cosign` +/// response instead (see [`CosignResponse::boot_ephemeral_key`]). +async fn pubkeys(State(state): State>) -> Json { + Json(PubkeysResponse { + programmatic: state.keys.programmatic.public_key_hex(), + admin: state.keys.admin.public_key_hex(), + }) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CosignRequest { + unsigned_transaction: String, + signer_address: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CosignResponse { + /// Exact JSON bytes to POST to Turnkey — send verbatim; the stamp covers + /// these exact bytes, so re-serializing would break it. + activity_body: String, + x_stamp: String, + classification: Classification, + /// Enclave-signed proof committing to this decision (see `proof.rs`). + app_proof: AppProof, + /// The QOS KeySet of the replica that produced `app_proof`. Pass it to + /// `get_boot_proof` to fetch the Boot Proof and verify the proof against this + /// enclave's attested code. Per-replica, so it must come from this response. + boot_ephemeral_key: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CosignError { + error: String, + #[serde(skip_serializing_if = "Option::is_none")] + classification: Option, +} + +/// Parse an unsigned tx, classify it, then build + stamp a `SIGN_TRANSACTION_V2`. +async fn cosign( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + // Decode + parse the unsigned transaction. A malformed tx is a client error. + let raw = decode_hex(&req.unsigned_transaction) + .map_err(|e| bad_request(format!("invalid unsignedTransaction hex: {e}"), None))?; + let parsed = tx::parse_unsigned(&raw) + .map_err(|e| bad_request(format!("could not parse transaction: {e}"), None))?; + + // The wallet to sign with is a global gate — it must be an allowlisted signer. + let signer = req + .signer_address + .parse::
() + .map_err(|e| bad_request(format!("invalid signerAddress: {e}"), None))?; + + let classification = classify(signer, &parsed, &state.config.ruleset); + + let key = match classification { + Classification::Programmatic => &state.keys.programmatic, + Classification::Admin => &state.keys.admin, + Classification::Reject => { + return Err(bad_request( + "transaction rejected by ruleset".to_string(), + Some(Classification::Reject), + )); + } + }; + + // One timestamp shared by the activity body and the proof for this request. + let timestamp_ms = now_ms(); + let body = build_sign_transaction(&SignTransaction { + organization_id: &state.config.organization_id, + sign_with: &req.signer_address, + unsigned_transaction: &req.unsigned_transaction, + timestamp_ms, + }); + let stamped = stamp::stamp(key, &body); + + // Attach an App Proof committing to this decision. `raw` re-encoded is the + // same normalized (no-`0x`, lowercase) form the activity body carries. + let proof = app_proof( + &state.ephemeral, + &ProofInputs { + organization_id: &state.config.organization_id, + signer_address: &req.signer_address, + unsigned_transaction: &hex::encode(&raw), + classification, + stamped_with: &key.public_key_hex(), + activity_body: &stamped.body, + timestamp_ms, + }, + ); + + Ok(Json(CosignResponse { + activity_body: stamped.body, + x_stamp: stamped.x_stamp, + classification, + app_proof: proof, + boot_ephemeral_key: state.ephemeral.boot_ephemeral_key_hex().to_string(), + })) +} + +/// Current time in milliseconds since the Unix epoch (Turnkey liveness stamp). +fn now_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock is after the Unix epoch") + .as_millis() as u64 +} + +/// Hex-decode, tolerating an optional `0x` prefix. +fn decode_hex(s: &str) -> Result, hex::FromHexError> { + let s = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s); + hex::decode(s) +} + +/// Build a `400` response with an error message and optional classification. +fn bad_request( + error: String, + classification: Option, +) -> (StatusCode, Json) { + ( + StatusCode::BAD_REQUEST, + Json(CosignError { + error, + classification, + }), + ) +} + +#[cfg(test)] +mod tests { + use super::take_value; + + fn peekable(args: &[&str]) -> std::iter::Peekable> { + args.iter() + .map(|s| s.to_string()) + .collect::>() + .into_iter() + .peekable() + } + + #[test] + fn take_value_returns_a_non_flag_value() { + let mut it = peekable(&["value", "--next"]); + assert_eq!(take_value(&mut it), Some("value".to_string())); + // The following flag is left for the main loop to handle. + assert_eq!(it.next(), Some("--next".to_string())); + } + + #[test] + fn take_value_does_not_consume_a_following_flag() { + let mut it = peekable(&["--port", "3000"]); + assert_eq!(take_value(&mut it), None); + // `--port` was not swallowed as a value. + assert_eq!(it.next(), Some("--port".to_string())); + } + + #[test] + fn take_value_none_at_end_of_args() { + let mut it = peekable(&[]); + assert_eq!(take_value(&mut it), None); + } +} diff --git a/tvc-cosign/src/proof.rs b/tvc-cosign/src/proof.rs new file mode 100644 index 0000000..3c0f3c4 --- /dev/null +++ b/tvc-cosign/src/proof.rs @@ -0,0 +1,207 @@ +//! App Proof over the cosign decision — the verifiability story. +//! +//! Stamping (see `stamp.rs`) authenticates the request to Turnkey as an API user. +//! An *App Proof* proves something stronger and orthogonal: that *this enclave*, +//! running *this attested code*, classified *this transaction* *this way*. It is +//! a statement signed by the enclave's Ephemeral Key — a per-boot P-256 key whose +//! public half is pinned in the enclave's Boot Proof. A verifier fetches the Boot +//! Proof from Turnkey by that public key, confirms it against the expected code +//! manifest, then verifies this signature — linking the decision to the code. +//! +//! Envelope matches Turnkey's standardized App Proof: +//! +//! { scheme, publicKey, proofPayload (stringified JSON), signature } +//! +//! signature = ECDSA-P256 over SHA-256(proofPayload bytes), DER-encoded, hex. +//! +//! `proofPayload` is a strictly-typed JSON string. We define one proof type, +//! `APP_PROOF_TYPE_COSIGN_DECISION`, committing to the facts a verifier cares +//! about: which wallet, which transaction, the classification, the stamping key, +//! and a digest binding the proof to the exact request that was stamped. + +use p256::ecdsa::Signature; +use p256::ecdsa::signature::Signer; +use serde::Serialize; +use sha2::{Digest, Sha256}; + +use crate::keys::EphemeralKey; +use crate::rules::Classification; + +/// Turnkey's signature-scheme identifier for Ephemeral-Key App Proofs. +const APP_PROOF_SCHEME: &str = "SIGNATURE_SCHEME_EPHEMERAL_KEY_P256"; + +/// This application's proof type. Turnkey's own proof types (e.g. +/// `APP_PROOF_TYPE_POLICY_OUTCOME`) are reserved; TVC apps define their own. +const COSIGN_PROOF_TYPE: &str = "APP_PROOF_TYPE_COSIGN_DECISION"; + +/// The facts this proof commits to. Serialized (as a string) into `proofPayload`. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CosignDecision<'a> { + /// The (sub-)organization the request targets. + organization_id: &'a str, + /// The wallet the transaction would be signed by (`signWith`). + signer_address: &'a str, + /// The unsigned transaction, normalized to no-`0x` lowercase hex — the exact + /// form that appears in the stamped activity body. + unsigned_transaction: &'a str, + /// How the enclave routed it: `PROGRAMMATIC` or `ADMIN`. + classification: Classification, + /// Compressed-SEC1 hex of the derived API key that stamped the request. Lets a + /// verifier confirm which Turnkey user (and thus which policy path) applies. + stamped_with: &'a str, + /// SHA-256 (hex) of the exact stamped activity body. Binds this proof to the + /// precise bytes the customer submits to Turnkey — re-serialize and it breaks. + activity_body_sha256: String, +} + +/// The typed proof payload: `{ type, timestampMs, cosignDecision }`, matching the +/// shape of Turnkey's own App Proof payloads. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ProofPayload<'a> { + #[serde(rename = "type")] + proof_type: &'static str, + timestamp_ms: String, + cosign_decision: CosignDecision<'a>, +} + +/// A finished App Proof, ready to embed in the `/cosign` response. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppProof { + scheme: &'static str, + public_key: String, + /// The JSON proof payload, serialized to a string (verifiers hash these bytes). + proof_payload: String, + /// ECDSA-P256/SHA-256/DER signature over `proof_payload`, hex. + signature: String, +} + +/// Everything needed to build a cosign App Proof. +pub struct ProofInputs<'a> { + pub organization_id: &'a str, + pub signer_address: &'a str, + pub unsigned_transaction: &'a str, + pub classification: Classification, + /// Compressed-SEC1 hex of the key that stamped the request. + pub stamped_with: &'a str, + /// The exact stamped activity body (bound into the proof via its digest). + pub activity_body: &'a str, + pub timestamp_ms: u64, +} + +/// Build and sign an App Proof over a cosign decision, using the enclave's +/// ephemeral key. +pub fn app_proof(ephemeral: &EphemeralKey, inputs: &ProofInputs) -> AppProof { + let activity_body_sha256 = hex::encode(Sha256::digest(inputs.activity_body.as_bytes())); + + let payload = ProofPayload { + proof_type: COSIGN_PROOF_TYPE, + timestamp_ms: inputs.timestamp_ms.to_string(), + cosign_decision: CosignDecision { + organization_id: inputs.organization_id, + signer_address: inputs.signer_address, + unsigned_transaction: inputs.unsigned_transaction, + classification: inputs.classification, + stamped_with: inputs.stamped_with, + activity_body_sha256, + }, + }; + + // Serialize once — this exact string is both signed and returned. A verifier + // re-hashes these bytes, so they must not be re-serialized downstream. + let proof_payload = serde_json::to_string(&payload).expect("proof payload serializes"); + + // RustCrypto's `Signer` for P-256 hashes with SHA-256 and low-S–normalizes — + // the construction Turnkey verifies App Proofs against. + let sig: Signature = ephemeral.signing_key().sign(proof_payload.as_bytes()); + + AppProof { + scheme: APP_PROOF_SCHEME, + public_key: ephemeral.public_key_hex().to_string(), + proof_payload, + signature: hex::encode(sig.to_der().as_bytes()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use p256::ecdsa::VerifyingKey; + use p256::ecdsa::signature::Verifier; + use serde_json::Value; + + fn ephemeral() -> EphemeralKey { + EphemeralKey::derive(&[0x42; 32]) + } + + fn inputs<'a>() -> ProofInputs<'a> { + ProofInputs { + organization_id: "org-123", + signer_address: "0xabc0000000000000000000000000000000000001", + unsigned_transaction: "deadbeef", + classification: Classification::Programmatic, + stamped_with: "02aaaa", + activity_body: r#"{"type":"ACTIVITY_TYPE_SIGN_TRANSACTION_V2"}"#, + timestamp_ms: 1_700_000_000_000, + } + } + + #[test] + fn envelope_has_expected_shape() { + let key = ephemeral(); + let proof = app_proof(&key, &inputs()); + let v: Value = serde_json::to_value(&proof).unwrap(); + + assert_eq!(v["scheme"], APP_PROOF_SCHEME); + assert_eq!(v["publicKey"], key.public_key_hex()); + assert!(!v["signature"].as_str().unwrap().is_empty()); + + // proofPayload is a JSON *string*; parse it and check the typed schema. + let payload: Value = serde_json::from_str(v["proofPayload"].as_str().unwrap()).unwrap(); + assert_eq!(payload["type"], COSIGN_PROOF_TYPE); + assert_eq!(payload["timestampMs"], "1700000000000"); + assert_eq!(payload["cosignDecision"]["classification"], "PROGRAMMATIC"); + assert_eq!( + payload["cosignDecision"]["signerAddress"], + inputs().signer_address + ); + } + + #[test] + fn payload_commits_to_the_activity_body() { + let proof = app_proof(&ephemeral(), &inputs()); + let payload: Value = serde_json::from_str(&proof.proof_payload).unwrap(); + let expected = hex::encode(Sha256::digest(inputs().activity_body.as_bytes())); + assert_eq!(payload["cosignDecision"]["activityBodySha256"], expected); + } + + #[test] + fn signature_verifies_against_the_ephemeral_key() { + let key = ephemeral(); + let proof = app_proof(&key, &inputs()); + + let vk = VerifyingKey::from_sec1_bytes(&hex::decode(&proof.public_key).unwrap()).unwrap(); + let sig = Signature::from_der(&hex::decode(&proof.signature).unwrap()).unwrap(); + + vk.verify(proof.proof_payload.as_bytes(), &sig) + .expect("app proof signature verifies over its payload"); + } + + #[test] + fn tampered_payload_fails_verification() { + let key = ephemeral(); + let proof = app_proof(&key, &inputs()); + + let vk = VerifyingKey::from_sec1_bytes(&hex::decode(&proof.public_key).unwrap()).unwrap(); + let sig = Signature::from_der(&hex::decode(&proof.signature).unwrap()).unwrap(); + + let tampered = proof.proof_payload.replace("PROGRAMMATIC", "ADMIN"); + assert_ne!(tampered, proof.proof_payload); + assert!( + vk.verify(tampered.as_bytes(), &sig).is_err(), + "a proof must not verify after its decision is altered" + ); + } +} diff --git a/tvc-cosign/src/rules.rs b/tvc-cosign/src/rules.rs new file mode 100644 index 0000000..531f20e --- /dev/null +++ b/tvc-cosign/src/rules.rs @@ -0,0 +1,360 @@ +//! The classification engine: decide whether an unsigned transaction is +//! `PROGRAMMATIC` (auto-signable), `ADMIN` (needs human consensus), or `REJECT`. +//! +//! Rules are config-driven (see `rules.example.toml`) so the customer can tune +//! allowlists and caps without code changes. In production the ruleset is baked +//! into the reproducible enclave image, so it's covered by the image measurement. + +use std::collections::HashSet; + +use alloy_primitives::{Address, U256}; +use serde::Deserialize; + +use crate::tx::ParsedTx; + +/// ERC-20 `transfer(address,uint256)` selector (`keccak256(...)[..4]`). +const TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb]; + +/// Ruleset compiled into the binary at build time. TVC runs only the pivot binary, +/// so the ruleset ships inside it rather than as a file in the image. +const EMBEDDED_RULES_TOML: &str = include_str!("../rules.toml"); + +/// How a transaction is routed. Serialized as `PROGRAMMATIC` / `ADMIN` / `REJECT`. +#[derive(serde::Serialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Classification { + Programmatic, + Admin, + Reject, +} + +/// The active ruleset, in runtime form (typed, deduplicated). +pub struct Ruleset { + allowed_signers: HashSet
, + allowed_tokens: HashSet
, + allowed_recipients: HashSet
, + max_amount: U256, + admin_selectors: HashSet<[u8; 4]>, +} + +impl Ruleset { + /// Load and validate a ruleset from a TOML file (local-dev override only; see + /// [`Ruleset::embedded`]). + pub fn load(path: &str) -> Result { + let text = std::fs::read_to_string(path).map_err(|e| format!("read {path}: {e}"))?; + let raw: RawRuleset = toml::from_str(&text).map_err(|e| format!("parse {path}: {e}"))?; + raw.into_ruleset() + } + + /// The ruleset compiled into the binary, covered by the attested + /// `expectedPivotDigest`. This is what a real TVC deployment enforces. + pub fn embedded() -> Result { + let raw: RawRuleset = toml::from_str(EMBEDDED_RULES_TOML) + .map_err(|e| format!("parse embedded rules.toml: {e}"))?; + raw.into_ruleset() + } +} + +/// Classify a parsed transaction against the ruleset. +/// +/// `signer` is the wallet the transaction would be signed by (`signWith`). It is +/// a global gate: the enclave only stamps for allowlisted wallets, whatever the +/// classification. +pub fn classify(signer: Address, tx: &ParsedTx, rules: &Ruleset) -> Classification { + if !rules.allowed_signers.contains(&signer) { + return Classification::Reject; + } + + if tx.to.is_none() { + // Contract creation (`to == None`) is out of scope for this POC. Reject it + // before the selector checks: otherwise initcode whose first 4 bytes happen + // to match an admin selector would misclassify as ADMIN. + return Classification::Reject; + } + + let Some(selector) = tx.selector() else { + // No calldata (e.g. a native transfer) — out of scope for this POC. + return Classification::Reject; + }; + + if selector == TRANSFER_SELECTOR { + return classify_transfer(tx, rules); + } + if rules.admin_selectors.contains(&selector) { + return Classification::Admin; + } + Classification::Reject +} + +/// An ERC-20 `transfer` is PROGRAMMATIC only if the token, recipient, and amount +/// all pass the allowlists/cap; otherwise REJECT. +fn classify_transfer(tx: &ParsedTx, rules: &Ruleset) -> Classification { + // A token transfer should not also move native ETH. + if !tx.value.is_zero() { + return Classification::Reject; + } + let Some(token) = tx.to else { + return Classification::Reject; + }; + if !rules.allowed_tokens.contains(&token) { + return Classification::Reject; + } + let Some((recipient, amount)) = decode_transfer_args(&tx.input) else { + return Classification::Reject; + }; + if !rules.allowed_recipients.contains(&recipient) || amount > rules.max_amount { + return Classification::Reject; + } + Classification::Programmatic +} + +/// Decode `transfer(address,uint256)` args: a 32-byte right-aligned address word +/// followed by a 32-byte amount. Rejects non-canonical (non-zero-padded) addresses. +fn decode_transfer_args(input: &[u8]) -> Option<(Address, U256)> { + // 4 (selector) + 32 (address) + 32 (amount) + if input.len() != 68 { + return None; + } + let (addr_word, amount_word) = input[4..].split_at(32); + if addr_word[..12].iter().any(|&b| b != 0) { + return None; // address must be zero-padded in its 32-byte word + } + let recipient = Address::from_slice(&addr_word[12..]); + let amount = U256::from_be_slice(amount_word); + Some((recipient, amount)) +} + +// --- config deserialization (raw TOML shape -> runtime Ruleset) --- + +#[derive(Deserialize)] +struct RawRuleset { + /// Wallets (`signWith` targets) this deployment is permitted to sign for. + #[serde(default)] + allowed_signers: Vec
, + programmatic: RawProgrammatic, + #[serde(default)] + admin: RawAdmin, +} + +#[derive(Deserialize)] +struct RawProgrammatic { + #[serde(default)] + allowed_tokens: Vec
, + #[serde(default)] + allowed_recipients: Vec
, + /// Max transfer amount as a decimal (or `0x`-hex) string, so it can exceed i64. + max_amount: String, +} + +#[derive(Deserialize, Default)] +struct RawAdmin { + #[serde(default)] + selectors: Vec, +} + +impl RawRuleset { + fn into_ruleset(self) -> Result { + let max_amount = self + .programmatic + .max_amount + .parse::() + .map_err(|e| format!("invalid max_amount: {e}"))?; + + let admin_selectors = self + .admin + .selectors + .iter() + .map(|s| parse_selector(s)) + .collect::, _>>()?; + + Ok(Ruleset { + allowed_signers: self.allowed_signers.into_iter().collect(), + allowed_tokens: self.programmatic.allowed_tokens.into_iter().collect(), + allowed_recipients: self.programmatic.allowed_recipients.into_iter().collect(), + max_amount, + admin_selectors, + }) + } +} + +/// Parse a 4-byte selector like `"0x12345678"`. +fn parse_selector(s: &str) -> Result<[u8; 4], String> { + let hex = s.strip_prefix("0x").unwrap_or(s); + let bytes = hex::decode(hex).map_err(|e| format!("invalid selector {s}: {e}"))?; + bytes + .try_into() + .map_err(|_| format!("selector {s} must be exactly 4 bytes")) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{Bytes, U256, address}; + + const SIGNER: Address = address!("00000000000000000000000000000000000000a1"); + const TOKEN: Address = address!("1111111111111111111111111111111111111111"); + const RECIPIENT: Address = address!("00000000000000000000000000000000000000ff"); + + fn ruleset_toml() -> Ruleset { + let toml = r#" + allowed_signers = ["0x00000000000000000000000000000000000000a1"] + + [programmatic] + allowed_tokens = ["0x1111111111111111111111111111111111111111"] + allowed_recipients = ["0x00000000000000000000000000000000000000ff"] + max_amount = "1000" + + [admin] + selectors = ["0x12345678"] + "#; + let raw: RawRuleset = toml::from_str(toml).unwrap(); + raw.into_ruleset().unwrap() + } + + fn transfer_tx(token: Address, recipient: Address, amount: u64) -> ParsedTx { + let mut input = vec![0xa9, 0x05, 0x9c, 0xbb]; + input.extend_from_slice(&[0u8; 12]); + input.extend_from_slice(recipient.as_slice()); + input.extend_from_slice(&U256::from(amount).to_be_bytes::<32>()); + ParsedTx { + to: Some(token), + value: U256::ZERO, + input: Bytes::from(input), + } + } + + #[test] + fn allowlisted_transfer_under_cap_is_programmatic() { + let tx = transfer_tx(TOKEN, RECIPIENT, 500); + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Programmatic + ); + } + + #[test] + fn transfer_over_cap_is_rejected() { + let tx = transfer_tx(TOKEN, RECIPIENT, 5000); + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn transfer_to_unlisted_recipient_is_rejected() { + let other = address!("00000000000000000000000000000000000000ee"); + let tx = transfer_tx(TOKEN, other, 100); + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn transfer_of_unlisted_token_is_rejected() { + let other = address!("2222222222222222222222222222222222222222"); + let tx = transfer_tx(other, RECIPIENT, 100); + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn admin_selector_routes_to_admin() { + let tx = ParsedTx { + to: Some(TOKEN), + value: U256::ZERO, + input: Bytes::from(vec![0x12, 0x34, 0x56, 0x78]), + }; + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Admin + ); + } + + #[test] + fn contract_creation_with_admin_selector_initcode_is_rejected() { + // `to == None` is contract creation (out of scope). Even if the initcode's + // first 4 bytes collide with an allowlisted admin selector, it must REJECT, + // not route to ADMIN. + let tx = ParsedTx { + to: None, + value: U256::ZERO, + input: Bytes::from(vec![0x12, 0x34, 0x56, 0x78]), + }; + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn unknown_selector_is_rejected() { + let tx = ParsedTx { + to: Some(TOKEN), + value: U256::ZERO, + input: Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]), + }; + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn no_calldata_is_rejected() { + let tx = ParsedTx { + to: Some(RECIPIENT), + value: U256::from(1u64), + input: Bytes::new(), + }; + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn transfer_with_trailing_calldata_is_rejected() { + // Canonical transfer calldata is exactly 68 bytes; extra trailing bytes + // are non-canonical and must not be auto-classified as PROGRAMMATIC. + let mut tx = transfer_tx(TOKEN, RECIPIENT, 500); + let mut input = tx.input.to_vec(); + input.push(0xff); + tx.input = Bytes::from(input); + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn transfer_with_attached_eth_is_rejected() { + let mut tx = transfer_tx(TOKEN, RECIPIENT, 100); + tx.value = U256::from(1u64); + assert_eq!( + classify(SIGNER, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn non_allowlisted_signer_is_rejected() { + let other_signer = address!("00000000000000000000000000000000000000b2"); + let tx = transfer_tx(TOKEN, RECIPIENT, 500); // otherwise valid + assert_eq!( + classify(other_signer, &tx, &ruleset_toml()), + Classification::Reject + ); + } + + #[test] + fn embedded_ruleset_parses() { + // The ruleset compiled in via include_str! is what a deployment enforces, + // so it must always be valid; a malformed rules.toml should fail here, not + // silently at enclave startup. + Ruleset::embedded().expect("embedded rules.toml parses"); + } +} diff --git a/tvc-cosign/src/stamp.rs b/tvc-cosign/src/stamp.rs new file mode 100644 index 0000000..234c1fd --- /dev/null +++ b/tvc-cosign/src/stamp.rs @@ -0,0 +1,127 @@ +//! Turnkey request stamping. +//! +//! A "stamp" is how a request authenticates itself to Turnkey as a given API +//! user. We sign the exact request body with a derived P-256 key and wrap the +//! signature in the envelope Turnkey expects: +//! +//! signature = ECDSA-P256 over SHA-256(body), DER-encoded, hex. +//! envelope = base64url(JSON { publicKey, scheme, signature }). +//! +//! The envelope string is sent as the `X-Stamp` HTTP header alongside the +//! unmodified body. Turnkey recomputes SHA-256(body) and verifies the signature +//! against the registered public key. + +use base64::Engine; +use p256::ecdsa::Signature; +use p256::ecdsa::signature::Signer; +use serde::Serialize; + +use crate::keys::ApiKey; + +/// Turnkey's signature-scheme identifier for P-256 API-key stamps. +const STAMP_SCHEME: &str = "SIGNATURE_SCHEME_TK_API_P256"; + +/// The stamp envelope. Field names are camelCase to match Turnkey's JSON. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct StampEnvelope { + public_key: String, + scheme: &'static str, + signature: String, +} + +/// A signed request, ready to POST to Turnkey. +pub struct Stamped { + /// The exact body bytes that were signed — send these unmodified. + pub body: String, + /// Value for the `X-Stamp` header. + pub x_stamp: String, +} + +/// Sign `body` with `key` and produce the `X-Stamp` header value. +pub fn stamp(key: &ApiKey, body: &str) -> Stamped { + // RustCrypto's `Signer` for P-256 hashes the message with SHA-256 and emits + // a low-S–normalized signature — the same construction Turnkey verifies. + let sig: Signature = key.signing_key().sign(body.as_bytes()); + let signature_hex = hex::encode(sig.to_der().as_bytes()); + + let envelope = StampEnvelope { + public_key: key.public_key_hex(), + scheme: STAMP_SCHEME, + signature: signature_hex, + }; + let json = serde_json::to_string(&envelope).expect("stamp envelope serializes"); + let x_stamp = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json); + + Stamped { + body: body.to_string(), + x_stamp, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::keys::KeySet; + use p256::ecdsa::VerifyingKey; + use p256::ecdsa::signature::Verifier; + use serde_json::Value; + + const SEED: [u8; 32] = [0x42; 32]; + const BODY: &str = r#"{"type":"ACTIVITY_TYPE_SIGN_TRANSACTION_V2","hello":"world"}"#; + + /// Decode the base64url X-Stamp back into its JSON envelope. + fn decode_stamp(x_stamp: &str) -> Value { + let json = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(x_stamp) + .expect("x_stamp is valid base64url"); + serde_json::from_slice(&json).expect("x_stamp decodes to JSON") + } + + #[test] + fn envelope_has_expected_shape() { + let keys = KeySet::derive(&SEED); + let stamped = stamp(&keys.programmatic, BODY); + let env = decode_stamp(&stamped.x_stamp); + + assert_eq!(env["scheme"], STAMP_SCHEME); + assert_eq!(env["publicKey"], keys.programmatic.public_key_hex()); + assert!(!env["signature"].as_str().unwrap().is_empty()); + assert_eq!(stamped.body, BODY, "body is returned unmodified"); + } + + #[test] + fn signature_verifies_against_the_public_key() { + let keys = KeySet::derive(&SEED); + let stamped = stamp(&keys.admin, BODY); + let env = decode_stamp(&stamped.x_stamp); + + // Reconstruct the verifying key from the compressed SEC1 hex in the stamp. + let pk_bytes = hex::decode(env["publicKey"].as_str().unwrap()).unwrap(); + let vk = VerifyingKey::from_sec1_bytes(&pk_bytes).expect("valid SEC1 pubkey"); + + // Parse the DER signature and verify it over the body (SHA-256 internally). + let sig_bytes = hex::decode(env["signature"].as_str().unwrap()).unwrap(); + let sig = Signature::from_der(&sig_bytes).expect("valid DER signature"); + + vk.verify(BODY.as_bytes(), &sig) + .expect("stamp signature verifies against its own public key"); + } + + #[test] + fn wrong_body_fails_verification() { + let keys = KeySet::derive(&SEED); + let stamped = stamp(&keys.programmatic, BODY); + let env = decode_stamp(&stamped.x_stamp); + + let pk_bytes = hex::decode(env["publicKey"].as_str().unwrap()).unwrap(); + let vk = VerifyingKey::from_sec1_bytes(&pk_bytes).unwrap(); + let sig_bytes = hex::decode(env["signature"].as_str().unwrap()).unwrap(); + let sig = Signature::from_der(&sig_bytes).unwrap(); + + assert!( + vk.verify(b"tampered body", &sig).is_err(), + "a signature must not verify over a different body" + ); + } +} diff --git a/tvc-cosign/src/tx.rs b/tvc-cosign/src/tx.rs new file mode 100644 index 0000000..154e5c3 --- /dev/null +++ b/tvc-cosign/src/tx.rs @@ -0,0 +1,163 @@ +//! Minimal unsigned-EVM-transaction parser. +//! +//! Turnkey's `unsignedTransaction` is the raw serialized transaction that gets +//! hashed for signing — i.e. what alloy calls the "encoded for signing" form: +//! - EIP-1559: `0x02 || rlp([chainId, nonce, maxPrio, maxFee, gas, to, value, data, accessList])` +//! - EIP-2930: `0x01 || rlp([...])` +//! - legacy: `rlp([nonce, gasPrice, gas, to, value, data, chainId, 0, 0])` +//! +//! The rules engine only needs `to`, `value`, and the calldata (for the 4-byte +//! selector + args), so we decode into that and drop the rest. + +use alloy_consensus::transaction::RlpEcdsaDecodableTx; +use alloy_consensus::{Transaction, TxEip1559, TxEip2930}; +use alloy_primitives::{Address, Bytes, TxKind, U256}; +use alloy_rlp::{Decodable, Header}; + +/// The fields of an unsigned transaction that classification cares about. +#[derive(Debug, Clone, PartialEq)] +pub struct ParsedTx { + /// Recipient. `None` for contract-creation transactions. + pub to: Option
, + /// Native value transferred, in wei. + pub value: U256, + /// Calldata. + pub input: Bytes, +} + +impl ParsedTx { + /// The 4-byte function selector, if the calldata is long enough. + pub fn selector(&self) -> Option<[u8; 4]> { + self.input.get(..4).map(|s| s.try_into().unwrap()) + } +} + +/// Why an unsigned transaction could not be parsed. +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + /// No bytes to parse. + #[error("empty transaction")] + Empty, + /// First byte is not a supported transaction type / RLP list. + #[error("unsupported transaction type byte {0:#04x}")] + UnsupportedType(u8), + /// The RLP body was malformed. + #[error("malformed RLP: {0}")] + Rlp(alloy_rlp::Error), +} + +/// Parse a raw unsigned transaction (already hex-decoded) into [`ParsedTx`]. +pub fn parse_unsigned(raw: &[u8]) -> Result { + let first = *raw.first().ok_or(ParseError::Empty)?; + match first { + // Typed transactions: strip the type byte, decode the RLP payload. + // Their signing form is symmetric with `rlp_decode`. + 0x02 => decode_typed::(&raw[1..]), + 0x01 => decode_typed::(&raw[1..]), + // Legacy: the whole thing is an RLP list (first byte is a list header, >= 0xc0). + b if b >= 0xc0 => decode_legacy(raw), + b => Err(ParseError::UnsupportedType(b)), + } +} + +/// Decode a typed tx's RLP body via its [`Transaction`] accessors. +fn decode_typed( + mut buf: &[u8], +) -> Result { + let tx = T::rlp_decode(&mut buf).map_err(ParseError::Rlp)?; + Ok(ParsedTx { + to: tx.to(), + value: tx.value(), + input: tx.input().clone(), + }) +} + +/// Decode a legacy tx's *signing payload*. Unlike a stored legacy tx, this is +/// `rlp([nonce, gasPrice, gas, to, value, data, chainId, 0, 0])` (EIP-155), so +/// we read the six fields we need and ignore any EIP-155 trailer. +fn decode_legacy(mut buf: &[u8]) -> Result { + let header = Header::decode(&mut buf).map_err(ParseError::Rlp)?; + if !header.list { + return Err(ParseError::Rlp(alloy_rlp::Error::UnexpectedString)); + } + let _nonce = u64::decode(&mut buf).map_err(ParseError::Rlp)?; + let _gas_price = U256::decode(&mut buf).map_err(ParseError::Rlp)?; + let _gas_limit = u64::decode(&mut buf).map_err(ParseError::Rlp)?; + let kind = TxKind::decode(&mut buf).map_err(ParseError::Rlp)?; + let value = U256::decode(&mut buf).map_err(ParseError::Rlp)?; + let input = Bytes::decode(&mut buf).map_err(ParseError::Rlp)?; + let to = match kind { + TxKind::Call(addr) => Some(addr), + TxKind::Create => None, + }; + Ok(ParsedTx { to, value, input }) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::{SignableTransaction, TxLegacy}; + use alloy_primitives::{TxKind, U256, address}; + + /// Build `transfer(address,uint256)` calldata. + fn transfer_calldata(recipient: Address, amount: u64) -> Vec { + let mut data = vec![0xa9, 0x05, 0x9c, 0xbb]; // transfer selector + data.extend_from_slice(&[0u8; 12]); + data.extend_from_slice(recipient.as_slice()); // left-padded with zeros to 32 bytes + data.extend_from_slice(&U256::from(amount).to_be_bytes::<32>()); + data + } + + #[test] + fn round_trip_eip1559_erc20_transfer() { + let token = address!("1111111111111111111111111111111111111111"); + let recipient = address!("00000000000000000000000000000000000000ff"); + let data = transfer_calldata(recipient, 1000); + + let tx = TxEip1559 { + chain_id: 1, + to: TxKind::Call(token), + value: U256::ZERO, + input: data.clone().into(), + ..Default::default() + }; + let raw = tx.encoded_for_signing(); + + let parsed = parse_unsigned(&raw).expect("parses"); + assert_eq!(parsed.to, Some(token)); + assert_eq!(parsed.value, U256::ZERO); + assert_eq!(parsed.selector(), Some([0xa9, 0x05, 0x9c, 0xbb])); + assert_eq!(parsed.input.as_ref(), data.as_slice()); + } + + #[test] + fn round_trip_legacy_native_transfer() { + let recipient = address!("00000000000000000000000000000000000000aa"); + let tx = TxLegacy { + chain_id: Some(1), + to: TxKind::Call(recipient), + value: U256::from(5_000_000u64), + input: Bytes::new(), + ..Default::default() + }; + let raw = tx.encoded_for_signing(); + + let parsed = parse_unsigned(&raw).expect("parses"); + assert_eq!(parsed.to, Some(recipient)); + assert_eq!(parsed.value, U256::from(5_000_000u64)); + assert_eq!(parsed.selector(), None, "no calldata -> no selector"); + } + + #[test] + fn rejects_empty() { + assert!(matches!(parse_unsigned(&[]), Err(ParseError::Empty))); + } + + #[test] + fn rejects_unknown_type_byte() { + assert!(matches!( + parse_unsigned(&[0x7f, 0x00]), + Err(ParseError::UnsupportedType(0x7f)) + )); + } +}