diff --git a/Cargo.lock b/Cargo.lock index ac5904b2..f6aaa021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,47 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "1.0.0" @@ -38,7 +79,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,7 +90,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,12 +99,146 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -75,11 +250,71 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-take" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[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 = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" -version = "2.13.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitvec" @@ -93,18 +328,140 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.4.2", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[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 = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[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" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.6.1" @@ -151,6 +508,41 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_format" version = "0.2.36" @@ -172,6 +564,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.6.0" @@ -191,129 +595,136 @@ dependencies = [ ] [[package]] -name = "derive_more" -version = "2.1.1" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ - "derive_more-impl", + "core-foundation-sys", + "libc", ] [[package]] -name = "derive_more-impl" -version = "2.1.1" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "convert_case 0.10.0", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "unicode-xid", + "libc", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "proc-macro2", - "quote", - "syn", + "crossbeam-utils", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "errno" -version = "0.3.14" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] -name = "fastrand" -version = "2.4.1" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "percent-encoding", + "generic-array", + "rand_core", + "typenum", ] [[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.32" +name = "crypto-mac" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "generic-array", + "subtle", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "ctr" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "futures-core", - "futures-sink", + "cipher", ] [[package]] -name = "futures-core" -version = "0.3.32" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] [[package]] -name = "futures-executor" -version = "0.3.32" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "futures-io" -version = "0.3.32" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] [[package]] -name = "futures-macro" -version = "0.3.32" +name = "derive-where" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", @@ -321,639 +732,3199 @@ dependencies = [ ] [[package]] -name = "futures-sink" -version = "0.3.32" +name = "derive_more" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] [[package]] -name = "futures-task" -version = "0.3.32" +name = "derive_more" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl 2.1.1", +] [[package]] -name = "futures-util" -version = "0.3.32" +name = "derive_more-impl" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "getrandom" -version = "0.4.3" +name = "derive_more-impl" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "cfg-if", - "libc", - "r-efi", + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", ] [[package]] -name = "hashbrown" -version = "0.17.0" +name = "digest" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] [[package]] -name = "heck" -version = "0.5.0" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] [[package]] -name = "hex" -version = "0.4.3" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "icu_collections" -version = "2.2.0" +name = "downcast-rs" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] -name = "icu_locale_core" -version = "2.2.0" +name = "ed25519" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "pkcs8", + "signature", ] [[package]] -name = "icu_normalizer" -version = "2.2.0" +name = "ed25519-zebra" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "775765289f7c6336c18d3d66127527820dd45ffd9eb3b6b8ee4708590e6c20f5" dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", + "curve25519-dalek", + "ed25519", + "hashbrown 0.16.1", + "pkcs8", + "rand_core", + "sha2 0.10.9", + "subtle", + "zeroize", ] [[package]] -name = "icu_normalizer_data" -version = "2.2.0" +name = "either" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] -name = "icu_properties" -version = "2.2.0" +name = "elliptic-curve" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "rand_core", + "sec1", + "subtle", + "zeroize", ] [[package]] -name = "icu_properties_data" -version = "2.2.0" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "icu_provider" -version = "2.2.0" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "idna" -version = "1.1.0" +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] -name = "idna_adapter" -version = "1.2.2" +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "icu_normalizer", - "icu_properties", + "event-listener", + "pin-project-lite", ] [[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" +name = "fastbloom" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" dependencies = [ - "proc-macro2", - "quote", - "syn", + "foldhash 0.2.0", + "libm", + "portable-atomic", + "siphasher", ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "fastrand" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] -name = "indoc" -version = "2.0.7" +name = "ff" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rustversion", + "rand_core", + "subtle", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "itoa" -version = "1.0.18" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "konst" -version = "0.2.20" +name = "finito" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +checksum = "2384245d85162258a14b43567a9ee3598f5ae746a1581fb5d3d2cb780f0dbf95" dependencies = [ - "konst_macro_rules", + "futures-timer", + "pin-project", ] [[package]] -name = "konst_macro_rules" -version = "0.2.19" +name = "fixed-hash" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] [[package]] -name = "libc" -version = "0.2.186" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "litemap" -version = "0.8.2" +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] -name = "memchr" -version = "2.8.0" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] [[package]] -name = "once_cell" -version = "1.21.4" +name = "frame-metadata" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "9ba5be0edbdb824843a0f9c6f0906ecfc66c5316218d74457003218b24909ed0" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", +] [[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand_core", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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 0.7.6", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.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-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-serde" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a143eada6a1ec4aefa5049037a26a6d597bfd64f8c026d07b77133e02b7dd0b" +dependencies = [ + "serde", +] + +[[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", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.24.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c4b1f204b655b36b24dc4939af20366c649431d4711863bbbae5c495f3eeb4" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.24.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e1420b1792cff778e2a1ebaa44115f156ee62a94dd106eaa51163f037d2023" +dependencies = [ + "base64", + "futures-channel", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "pin-project", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "soketto", + "thiserror 1.0.69", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.24.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49bfa9334963e1c85866b39dff3ffcc81f1c286eb23334267c5cb97677543a4" +dependencies = [ + "async-trait", + "futures-timer", + "futures-util", + "jsonrpsee-types", + "pin-project", + "rustc-hash", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.24.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d86fc943f81dab0ecdd6c0240b6e0f55ad57a2ea9ad8ad7efe8456fb9cc7a4" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.24.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735df2088674c87f7fecdf51c80878a7aa19a8116b32d703b000f5b1a7acf95a" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.24.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df5bd5c38c0906a6e8b3a38c8c22cc8525fda25fd1a03a3fe010686aea66b70" +dependencies = [ + "http", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "url", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[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 = "libsecp256k1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multi-stash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[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-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "elliptic-curve", + "primeorder", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec 0.7.6", + "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", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-serde", + "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 = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[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.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[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 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ruzstd" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "cfg-if", + "derive_more 1.0.0", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9fcb6c2e176e86ec703e22560d99d65a5ee9056ae45a08e13e84ebf796296f" +dependencies = [ + "aead", + "arrayref", + "arrayvec 0.7.6", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[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", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[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_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[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", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smoldot" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e8b3be57abd860ec235a62084ad8a72772d4bf799ba26aa3aefc282273fcf5e" +dependencies = [ + "arrayvec 0.7.6", + "async-lock", + "atomic-take", + "base32", + "base64", + "bip39", + "blake2-rfc", + "bs58", + "chacha20", + "crossbeam-queue", + "derive_more 2.1.1", + "ed25519-zebra", + "either", + "event-listener", + "fastbloom", + "fnv", + "futures-lite", + "futures-util", + "hashbrown 0.16.1", + "hex", + "hmac 0.12.1", + "itertools", + "libm", + "libsecp256k1", + "merlin", + "nom", + "num-bigint", + "num-rational", + "num-traits", + "pbkdf2", + "pin-project", + "poly1305", + "rand", + "rand_chacha", + "ruzstd", + "schnorrkel", + "serde", + "serde_json", + "sha2 0.10.9", + "sha3", + "siphasher", + "slab", + "smallvec", + "soketto", + "twox-hash 2.1.2", + "wasmi", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "smoldot" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f753549b68687bf8e27cda1e26dfbe8762701216ea722d43bd992a3a2576daa1" +dependencies = [ + "arrayvec 0.7.6", + "async-lock", + "atomic-take", + "base32", + "base64", + "bip39", + "blake2-rfc", + "bs58", + "chacha20", + "crossbeam-queue", + "derive_more 2.1.1", + "ed25519-zebra", + "either", + "event-listener", + "fastbloom", + "fnv", + "futures-lite", + "futures-util", + "hashbrown 0.16.1", + "hex", + "hmac 0.12.1", + "itertools", + "libm", + "libsecp256k1", + "merlin", + "nom", + "num-bigint", + "num-rational", + "num-traits", + "pbkdf2", + "pin-project", + "poly1305", + "rand", + "rand_chacha", + "ruzstd", + "schnorrkel", + "serde", + "serde_json", + "sha2 0.10.9", + "sha3", + "siphasher", + "slab", + "smallvec", + "soketto", + "twox-hash 2.1.2", + "wasmi", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "smoldot-light" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50345b88d93e5fd400b04cc9c64ab34fa87235e2c0aba4f1973916d7547feb4f" +dependencies = [ + "async-channel", + "async-lock", + "base64", + "blake2-rfc", + "bs58", + "derive_more 2.1.1", + "either", + "event-listener", + "fnv", + "futures-channel", + "futures-lite", + "futures-util", + "hashbrown 0.16.1", + "hex", + "itertools", + "log", + "lru", + "parking_lot", + "pin-project", + "rand", + "rand_chacha", + "serde", + "serde_json", + "siphasher", + "slab", + "smol", + "smoldot 2.0.0", + "zeroize", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "httparse", + "log", + "rand", + "sha1", +] + +[[package]] +name = "sp-crypto-hashing" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9927a7f81334ed5b8a98a4a978c81324d12bd9713ec76b5c68fd410174c5eb" +dependencies = [ + "blake2b_simd", + "byteorder", + "digest 0.10.7", + "sha2 0.10.9", + "sha3", + "twox-hash 1.6.3", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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 = "subxt-lightclient" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "961bd1b7d5531f7a6b8086364eb4c6c09b21675e5e8b29b56ea281187d151eef" +dependencies = [ + "futures", + "futures-timer", + "futures-util", + "getrandom 0.2.17", + "js-sys", + "pin-project", + "send_wrapper 0.6.0", + "serde", + "serde_json", + "smoldot 1.2.0", + "smoldot-light", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", +] + +[[package]] +name = "subxt-rpcs" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e4b23044ba59654f30e25cc48f8865e70439ee137764793cdfb0f8452dc638" +dependencies = [ + "derive-where", + "finito", + "frame-metadata", + "futures", + "getrandom 0.2.17", + "hex", + "impl-serde", + "jsonrpsee", + "parity-scale-codec", + "primitive-types", + "serde", + "serde_json", + "subxt-lightclient", + "thiserror 2.0.18", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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 = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[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", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[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.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "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 = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "truapi" +version = "0.3.1" +dependencies = [ + "derive_more 2.1.1", + "futures", + "hex", + "parity-scale-codec", + "truapi-macros", +] + +[[package]] +name = "truapi-codegen" +version = "0.1.0" dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", + "anyhow", + "clap", + "convert_case 0.6.0", + "indoc", "serde", + "serde_json", + "tempfile", + "truapi", ] [[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +name = "truapi-macros" +version = "0.1.0" dependencies = [ - "proc-macro-crate", "proc-macro2", "quote", "syn", ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "truapi-platform" +version = "0.1.0" +dependencies = [ + "async-trait", + "derive_more 2.1.1", + "futures", + "parity-scale-codec", + "truapi", + "url", +] + +[[package]] +name = "truapi-server" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "async-trait", + "blake2-rfc", + "bs58", + "console_error_panic_hook", + "derive_more 2.1.1", + "futures", + "futures-timer", + "futures-util", + "getrandom 0.2.17", + "hex", + "hkdf", + "js-sys", + "p256", + "parity-scale-codec", + "pin-project", + "primitive-types", + "schnorrkel", + "send_wrapper 0.6.0", + "serde", + "serde_json", + "sha2 0.10.9", + "sp-crypto-hashing", + "subxt-rpcs", + "thiserror 1.0.69", + "tracing", + "tracing-subscriber", + "truapi", + "truapi-platform", + "unicode-normalization", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", + "web-time", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "digest 0.10.7", + "static_assertions", +] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "twox-hash" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] -name = "potential_utf" -version = "0.1.5" +name = "typenum" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" dependencies = [ - "zerovec", + "byteorder", + "crunchy", + "hex", + "static_assertions", ] [[package]] -name = "proc-macro-crate" -version = "3.5.0" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ - "toml_edit", + "tinyvec", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "unicode-segmentation" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "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.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] [[package]] -name = "quote" -version = "1.0.45" +name = "wasm-bindgen-test" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" dependencies = [ "proc-macro2", + "quote", + "syn", ] [[package]] -name = "r-efi" -version = "6.0.0" +name = "wasm-bindgen-test-shared" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" [[package]] -name = "radium" -version = "0.7.0" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] [[package]] -name = "rustc_version" -version = "0.4.1" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ - "semver", + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser 0.244.0", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "wasmi" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" +dependencies = [ + "arrayvec 0.7.6", + "multi-stash", + "smallvec", + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser 0.221.3", +] + +[[package]] +name = "wasmi_collections" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" + +[[package]] +name = "wasmi_core" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" +dependencies = [ + "downcast-rs", + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" dependencies = [ "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.8", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] -name = "semver" -version = "1.0.28" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "serde" -version = "1.0.228" +name = "windows-sys" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "serde_core", - "serde_derive", + "windows-targets 0.42.2", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "serde_derive", + "windows-targets 0.52.6", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-targets 0.52.6", ] [[package]] -name = "serde_json" -version = "1.0.149" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "windows-link", ] [[package]] -name = "slab" -version = "0.4.12" +name = "windows-targets" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] [[package]] -name = "smallvec" -version = "1.15.1" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "windows_aarch64_gnullvm" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] -name = "strsim" -version = "0.11.1" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "syn" -version = "2.0.117" +name = "windows_aarch64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] -name = "synstructure" -version = "0.13.2" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "tap" -version = "1.0.1" +name = "windows_i686_gnu" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] -name = "tempfile" -version = "3.27.0" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", -] +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "tinystr" -version = "0.8.3" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "windows_i686_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" +name = "windows_x86_64_gnu" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] -name = "truapi" -version = "0.3.1" -dependencies = [ - "derive_more", - "futures", - "hex", - "parity-scale-codec", - "truapi-macros", -] - -[[package]] -name = "truapi-codegen" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "convert_case 0.6.0", - "indoc", - "serde", - "serde_json", - "tempfile", - "truapi", -] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "truapi-macros" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] -name = "truapi-platform" -version = "0.1.0" -dependencies = [ - "async-trait", - "derive_more", - "futures", - "parity-scale-codec", - "truapi", - "url", -] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "windows_x86_64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] -name = "unicode-segmentation" -version = "1.13.2" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "winnow" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] [[package]] -name = "url" -version = "2.5.8" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", + "wit-bindgen-rust-macro", ] [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "windows-link" -version = "0.2.1" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ - "windows-link", + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] -name = "winnow" -version = "1.0.2" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ - "memchr", + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", ] [[package]] @@ -971,6 +3942,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "yoke" version = "0.8.2" @@ -994,6 +3977,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -1015,6 +4018,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index d657d267..042aff82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "2" members = ["rust/crates/*"] -exclude = ["rust/crates/truapi-server"] [workspace.package] edition = "2024" diff --git a/deny.toml b/deny.toml index 707c6f77..4fdc03c6 100644 --- a/deny.toml +++ b/deny.toml @@ -5,6 +5,9 @@ allow = [ "MIT", "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "CC0-1.0", "Unicode-3.0", "Unlicense", "Zlib", diff --git a/rust/crates/truapi-server/Cargo.toml b/rust/crates/truapi-server/Cargo.toml new file mode 100644 index 00000000..6cecb5f6 --- /dev/null +++ b/rust/crates/truapi-server/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "truapi-server" +version = "0.1.0" +edition.workspace = true +description = "TrUAPI server runtime: dispatcher, frames, SCALE, streams" +license = "MIT" + +[lib] +crate-type = ["rlib", "cdylib"] + +[features] +default = [] + +[dependencies] +truapi = { path = "../truapi" } +truapi-platform = { path = "../truapi-platform" } +async-trait = "0.1" +derive_more = { version = "2", features = ["display"] } +futures = "0.3" +futures-timer = { version = "3", features = ["wasm-bindgen"] } +parity-scale-codec = { version = "3", features = ["derive"] } +primitive-types = { version = "0.13", default-features = false, features = ["serde"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "1" +unicode-normalization = "0.1" +url = "2" +hex = "0.4" +blake2-rfc = { version = "0.2", default-features = false } +sp-crypto-hashing = { version = "0.1", default-features = false } +bs58 = { version = "0.5", default-features = false, features = ["alloc"] } +schnorrkel = { version = "0.11.5", default-features = false, features = ["alloc", "getrandom"] } +getrandom = { version = "0.2", features = ["js"] } +p256 = { version = "0.13", default-features = false, features = ["ecdh"] } +hkdf = "0.12" +sha2 = "0.10" +aes-gcm = { version = "0.10", default-features = false, features = ["aes", "alloc"] } +tracing = "0.1" +# `registry` + `std` only: pulls the Registry + per-layer filter/reload, but +# not `env-filter` (which drags in `regex`, heavy on wasm). +tracing-subscriber = { version = "0.3", default-features = false, features = ["registry", "std"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +subxt-rpcs = { version = "0.50.1", default-features = false, features = ["native"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3" +subxt-rpcs = { version = "0.50.1", default-features = false, features = ["web"] } +wasm-bindgen = "0.2.118" +wasm-bindgen-futures = "0.4" +console_error_panic_hook = "0.1" +futures-util = "0.3" +pin-project = "1" +send_wrapper = { version = "0.6", features = ["futures"] } +web-time = "1" +web-sys = { version = "0.3", features = [ + "BinaryType", + "CloseEvent", + "console", + "Event", + "MessageEvent", + "WebSocket", +] } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md new file mode 100644 index 00000000..c4a91856 --- /dev/null +++ b/rust/crates/truapi-server/README.md @@ -0,0 +1,33 @@ +# truapi-server + +_Runtime core for TrUAPI: dispatcher, protocol frames, SCALE-coded wire envelope._ + +## What this crate is for + +`truapi-server` is the runtime that turns trait implementations of the +`truapi` API into a working host. It owns: + +- the [`ProtocolMessage`] wire envelope and SCALE codec +- the [`Dispatcher`] that routes incoming frames to per-method handlers +- the subscription lifecycle (start/receive/stop/interrupt) +- the [`Transport`] trait that platform-specific IPC backends implement +- the auto-generated dispatcher/wire-table tables shipped under + [`crate::generated`] + +## Wire envelope + +Every frame on the wire is encoded as: + +```text +[requestId: SCALE str][discriminant: u8][payload bytes...] +``` + +The discriminant identifies a method + frame kind via the auto-generated +[`crate::generated::wire_table::WIRE_TABLE`]. Each method's ids are exposed +as a named const (`PREIMAGE_SUBMIT`, ...); both `WIRE_TABLE` and the generated +dispatcher reference those consts. Method ordering is part of the wire +protocol; only ever append. + +The payload bytes are the SCALE-encoded inner value, inlined without a +length prefix. The discriminant is carried directly as `Payload::id`, and the +dispatcher routes on that numeric id via id-keyed tables. diff --git a/rust/crates/truapi-server/src/host_logic.rs b/rust/crates/truapi-server/src/host_logic.rs new file mode 100644 index 00000000..183688fa --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic.rs @@ -0,0 +1,16 @@ +//! Host-agnostic logic the Rust core owns on behalf of every platform host. +//! +//! Platform callbacks are a syscall layer for OS primitives (modals, native +//! storage, URL handler, notification center). Everything else lives here so +//! iOS, Android, and web hosts share one canonical implementation. + +pub mod dotns; +pub mod entropy; +pub mod features; +pub mod identity; +pub mod permissions; +pub mod product_account; +pub mod session; +pub mod session_store; +pub mod sso; +pub mod statement_store; diff --git a/rust/crates/truapi-server/src/host_logic/dotns.rs b/rust/crates/truapi-server/src/host_logic/dotns.rs new file mode 100644 index 00000000..67f93351 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/dotns.rs @@ -0,0 +1,473 @@ +//! dotns URL parsing, normalization, and classification. +//! +//! The Rust core owns the whole decision so every platform host sees the +//! same categorization and the `navigate_to` callback only receives +//! already-validated input. + +use unicode_normalization::UnicodeNormalization; +use url::Url; + +/// How the input URL should be opened. Kept in one enum rather than passing +/// a raw string so the dispatcher can reject invalid input before reaching +/// any platform callback. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NavigateDecision { + /// A `.dot` identifier plus path/query/hash suffix (no leading `/`). + DotName { + /// Lower-cased `.dot` host (e.g. `mytestapp.dot`). + identifier: String, + /// Path/query/hash suffix without a leading `/`. + path: String, + }, + /// A `localhost[:port]` URL plus path/query/hash suffix (no leading `/`). + Localhost { + /// `localhost` with optional `:port` suffix. + host: String, + /// Path/query/hash suffix without a leading `/`. + path: String, + }, + /// An absolute external URL with an `http(s):` scheme prepended if missing. + External { + /// Canonical URL string. + url: String, + }, + /// Input that fails every branch: empty, unparseable, or a `.dot` URL + /// carrying port/userinfo (both forbidden since dotns resolves via the + /// chain and has no notion of either). + Reject { + /// Human-readable reason for the rejection. + reason: String, + }, +} + +impl NavigateDecision { + /// Canonical URL string for the three `Open*` variants; `None` for + /// `Reject`. `DotName` and `Localhost` keep the dotns/localhost identity + /// visible so env-aware hosts (e.g. dotli rewriting `.dot` to `.dot.li`) + /// can re-parse and do their own assembly without losing information. + pub fn canonical_url(&self) -> Option { + match self { + Self::DotName { identifier, path } => Some(join_url("https://", identifier, path)), + Self::Localhost { host, path } => Some(join_url("http://", host, path)), + Self::External { url } => Some(url.clone()), + Self::Reject { .. } => None, + } + } +} + +fn join_url(scheme: &str, host: &str, path: &str) -> String { + if path.is_empty() { + format!("{scheme}{host}") + } else { + format!("{scheme}{host}/{path}") + } +} + +/// Classify a URL the way dotli's `handleNavigateTo` does: try `.dot` first, +/// then `localhost`, then normalize as external. +pub fn parse_navigate(input: &str) -> NavigateDecision { + let trimmed = input.trim(); + if trimmed.is_empty() { + return NavigateDecision::Reject { + reason: "empty input".to_string(), + }; + } + + if let Some(decision) = classify_dot(trimmed) { + return decision; + } + + if let Some(decision) = classify_localhost(trimmed) { + return decision; + } + + match normalize_external(trimmed) { + Ok(url) => NavigateDecision::External { url }, + Err(reason) => NavigateDecision::Reject { reason }, + } +} + +/// Canonical host form: case-folded and NFC-normalized (belt-and-suspenders; +/// `url` already applies IDNA to parsed hosts), with a trailing root dot +/// dropped so the absolute form `example.dot.` keys identically to +/// `example.dot`. +fn normalize_host(host: &str) -> String { + let normalized: String = host.nfc().collect::().to_lowercase(); + normalized + .strip_suffix('.') + .unwrap_or(&normalized) + .to_string() +} + +/// `.dot` TLD check, applied to the [`normalize_host`] form so `Example.DOT` +/// and the trailing-dot FQDN `example.dot.` classify like `example.dot`. +fn is_dot_domain(host: &str) -> bool { + normalize_host(host).ends_with(".dot") +} + +fn parse_with_explicit_https(input: &str) -> Option { + if let Ok(direct) = Url::parse(input) { + return Some(direct); + } + Url::parse(&format!("https://{input}")).ok() +} + +/// Recognize `.dot` URLs (including the `polkadot://` scheme). Returns: +/// - `Some(DotName)` for a clean `.dot` URL +/// - `Some(Reject)` for a `.dot` URL with port or userinfo +/// - `None` when the input isn't a `.dot` URL (caller falls through to +/// localhost / external) +fn classify_dot(input: &str) -> Option { + let parsed = if input.starts_with("polkadot://") { + Url::parse(input).ok()? + } else { + parse_with_explicit_https(input)? + }; + + let hostname = parsed.host_str()?; + if !is_dot_domain(hostname) { + return None; + } + + if parsed.port().is_some() || !parsed.username().is_empty() || parsed.password().is_some() { + return Some(NavigateDecision::Reject { + reason: format!("{hostname} carries port or userinfo; dotns forbids both"), + }); + } + + Some(NavigateDecision::DotName { + identifier: normalize_host(hostname), + path: strip_leading_slash(parsed.path()) + &suffix(&parsed), + }) +} + +/// Recognize `localhost[:port]` URLs, with or without an explicit scheme. +fn classify_localhost(input: &str) -> Option { + let with_scheme = if input.starts_with("localhost") { + format!("http://{input}") + } else { + input.to_string() + }; + + let parsed = Url::parse(&with_scheme).ok()?; + if parsed.host_str()? != "localhost" { + return None; + } + + let host = match parsed.port() { + Some(port) => format!("localhost:{port}"), + None => "localhost".to_string(), + }; + + Some(NavigateDecision::Localhost { + host, + path: strip_leading_slash(parsed.path()) + &suffix(&parsed), + }) +} + +/// External URL scheme allowlist. Anything outside this set is treated as +/// a [`NavigateDecision::Reject`] so dangerous schemes (`javascript:`, +/// `data:`, `file:`, `vbscript:`, ...) cannot reach `Platform::navigate_to`. +const ALLOWED_EXTERNAL_SCHEMES: &[&str] = &["http", "https", "mailto", "tel", "polkadot", "dot"]; + +/// Mirrors `normalizeUrl`: prepend `https://` if missing, otherwise pass the +/// URL through as its canonical string form. Returns `Err(reason)` for an +/// unparseable input or a scheme outside [`ALLOWED_EXTERNAL_SCHEMES`]. +fn normalize_external(input: &str) -> Result { + // `parse_with_explicit_https` returns a successful direct parse as-is and + // only prepends `https://` when the direct parse fails, so a disallowed + // scheme (e.g. `javascript:`) is never rewritten to https: the single + // scheme check below rejects it. + let url = parse_with_explicit_https(input) + .ok_or_else(|| "URL constructor rejected input".to_string())?; + if !ALLOWED_EXTERNAL_SCHEMES.contains(&url.scheme()) { + return Err(format!("scheme `{}` is not allowed", url.scheme())); + } + Ok(url.to_string()) +} + +fn strip_leading_slash(path: &str) -> String { + path.strip_prefix('/').unwrap_or(path).to_string() +} + +fn suffix(url: &Url) -> String { + let mut out = String::new(); + if let Some(q) = url.query() { + out.push('?'); + out.push_str(q); + } + if let Some(f) = url.fragment() { + out.push('#'); + out.push_str(f); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + enum Expected { + Decision(NavigateDecision), + AnyExternalOrReject, + Reject, + } + + struct TestCase { + name: &'static str, + input: &'static str, + expected: Expected, + } + + fn dot(identifier: &str, path: &str) -> Expected { + Expected::Decision(NavigateDecision::DotName { + identifier: identifier.to_string(), + path: path.to_string(), + }) + } + + fn localhost(host: &str, path: &str) -> Expected { + Expected::Decision(NavigateDecision::Localhost { + host: host.to_string(), + path: path.to_string(), + }) + } + + fn external(url: &str) -> Expected { + Expected::Decision(NavigateDecision::External { + url: url.to_string(), + }) + } + + #[test] + fn parse_navigate_cases() { + let cases = vec![ + TestCase { + name: "dot bare", + input: "mytestapp.dot", + expected: dot("mytestapp.dot", ""), + }, + TestCase { + name: "dot trailing root dot", + input: "example.dot.", + expected: dot("example.dot", ""), + }, + TestCase { + name: "dot trailing root dot with path", + input: "https://example.dot./path", + expected: dot("example.dot", "path"), + }, + TestCase { + name: "dot li is external", + input: "mytestapp.dot.li", + expected: external("https://mytestapp.dot.li/"), + }, + TestCase { + name: "dot with https", + input: "https://mytestapp.dot", + expected: dot("mytestapp.dot", ""), + }, + TestCase { + name: "dot with http", + input: "http://mytestapp.dot", + expected: dot("mytestapp.dot", ""), + }, + TestCase { + name: "dot with path", + input: "mytestapp.dot/some/path", + expected: dot("mytestapp.dot", "some/path"), + }, + TestCase { + name: "dot with query only", + input: "pr508.faucet.dot?embed=1", + expected: dot("pr508.faucet.dot", "?embed=1"), + }, + TestCase { + name: "dot with hash only", + input: "pr508.faucet.dot#section=main", + expected: dot("pr508.faucet.dot", "#section=main"), + }, + TestCase { + name: "dot with path query hash", + input: "pr508.faucet.dot/nested/path?embed=1#frame=compact", + expected: dot("pr508.faucet.dot", "nested/path?embed=1#frame=compact"), + }, + TestCase { + name: "polkadot scheme dot host", + input: "polkadot://currenthost.dot/mytestapp.dot", + expected: dot("currenthost.dot", "mytestapp.dot"), + }, + TestCase { + name: "polkadot scheme non dot host falls through", + input: "polkadot://example.com/settings", + expected: Expected::AnyExternalOrReject, + }, + TestCase { + name: "polkadot scheme with path", + input: "polkadot://currenthost.dot/mytestapp.dot/settings", + expected: dot("currenthost.dot", "mytestapp.dot/settings"), + }, + TestCase { + name: "polkadot scheme with query and hash", + input: "polkadot://currenthost.dot/mytestapp.dot?embed=1#frame=compact", + expected: dot("currenthost.dot", "mytestapp.dot?embed=1#frame=compact"), + }, + TestCase { + name: "dot subdomain", + input: "sub.acme.dot/path", + expected: dot("sub.acme.dot", "path"), + }, + TestCase { + name: "dot mixed case", + input: "Example.DOT/Path", + expected: dot("example.dot", "Path"), + }, + TestCase { + name: "dot with port is rejected", + input: "https://x.dot:8080/path", + expected: Expected::Reject, + }, + TestCase { + name: "dot with userinfo is rejected", + input: "https://user:pass@x.dot/path", + expected: Expected::Reject, + }, + TestCase { + name: "trim whitespace", + input: " mytestapp.dot/path ", + expected: dot("mytestapp.dot", "path"), + }, + TestCase { + name: "localhost bare with port", + input: "localhost:3000", + expected: localhost("localhost:3000", ""), + }, + TestCase { + name: "localhost with port and path", + input: "localhost:3000/some/path", + expected: localhost("localhost:3000", "some/path"), + }, + TestCase { + name: "localhost with explicit http", + input: "http://localhost:5000", + expected: localhost("localhost:5000", ""), + }, + TestCase { + name: "localhost with http and path", + input: "http://localhost:5000/path", + expected: localhost("localhost:5000", "path"), + }, + TestCase { + name: "localhost with query and hash", + input: "localhost:3000/path?q=1#h", + expected: localhost("localhost:3000", "path?q=1#h"), + }, + TestCase { + name: "localhost without port", + input: "localhost", + expected: localhost("localhost", ""), + }, + TestCase { + name: "localhost without port with path", + input: "localhost/path", + expected: localhost("localhost", "path"), + }, + TestCase { + name: "external bare domain", + input: "google.com", + expected: external("https://google.com/"), + }, + TestCase { + name: "external bare domain with path", + input: "google.com/search?q=test", + expected: external("https://google.com/search?q=test"), + }, + TestCase { + name: "external preserves https", + input: "https://example.com/page", + expected: external("https://example.com/page"), + }, + TestCase { + name: "external preserves http", + input: "http://example.com/page", + expected: external("http://example.com/page"), + }, + TestCase { + name: "external dot li", + input: "acme.dot.li/path/1", + expected: external("https://acme.dot.li/path/1"), + }, + TestCase { + name: "reject empty", + input: "", + expected: Expected::Reject, + }, + TestCase { + name: "reject whitespace", + input: " ", + expected: Expected::Reject, + }, + TestCase { + name: "reject unparseable", + input: ":::invalid", + expected: Expected::Reject, + }, + TestCase { + name: "reject javascript URI", + input: "javascript:alert(1)", + expected: Expected::Reject, + }, + TestCase { + name: "reject file URI", + input: "file:///etc/passwd", + expected: Expected::Reject, + }, + TestCase { + name: "reject data URI", + input: "data:text/html,", + expected: Expected::Reject, + }, + TestCase { + name: "reject vbscript URI", + input: "vbscript:msgbox(1)", + expected: Expected::Reject, + }, + ]; + + for case in cases { + let actual = parse_navigate(case.input); + match case.expected { + Expected::Decision(expected) => assert_eq!(actual, expected, "{}", case.name), + Expected::AnyExternalOrReject => assert!( + matches!( + actual, + NavigateDecision::External { .. } | NavigateDecision::Reject { .. } + ), + "{}: expected External or Reject, got {actual:?}", + case.name, + ), + Expected::Reject => assert!( + matches!(actual, NavigateDecision::Reject { .. }), + "{}: expected Reject, got {actual:?}", + case.name, + ), + } + } + + let nfc = parse_navigate("café.dot"); + let nfd = parse_navigate("cafe\u{0301}.dot"); + match (&nfc, &nfd) { + ( + NavigateDecision::DotName { + identifier: a, + path: _, + }, + NavigateDecision::DotName { + identifier: b, + path: _, + }, + ) => assert_eq!(a, b, "NFC and NFD inputs must normalize to one identifier"), + other => panic!("expected two DotName decisions, got {other:?}"), + } + } +} diff --git a/rust/crates/truapi-server/src/host_logic/entropy.rs b/rust/crates/truapi-server/src/host_logic/entropy.rs new file mode 100644 index 00000000..68e9e1fb --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/entropy.rs @@ -0,0 +1,113 @@ +//! Product-scoped deterministic entropy derivation. +//! +//! Matches dotli's product entropy contract: three keyed BLAKE2b-256 layers +//! over the session secret, product id, and caller key. + +use blake2_rfc::blake2b::blake2b; +use thiserror::Error; + +const DOMAIN_SEPARATOR: &[u8] = b"product-entropy-derivation"; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ProductEntropyError { + #[error("\"key\" must be between 1 and 32 bytes, got {0}")] + InvalidKeyLength(usize), + #[error("entropy secret is missing")] + MissingSecret, +} + +/// Derive product-scoped entropy from the session root entropy secret. +pub fn derive_product_entropy( + entropy_secret: &[u8], + product_id: &str, + key: &[u8], +) -> Result<[u8; 32], ProductEntropyError> { + let root_entropy_source = blake2b256_keyed(entropy_secret, DOMAIN_SEPARATOR); + derive_product_entropy_from_source(&root_entropy_source, product_id, key) +} + +/// Derive product-scoped entropy from an already normalized root entropy source. +pub fn derive_product_entropy_from_source( + root_entropy_source: &[u8; 32], + product_id: &str, + key: &[u8], +) -> Result<[u8; 32], ProductEntropyError> { + if key.is_empty() || key.len() > 32 { + return Err(ProductEntropyError::InvalidKeyLength(key.len())); + } + + let product_id_hash = blake2b256(product_id.as_bytes()); + let per_product_entropy = blake2b256_keyed(root_entropy_source, &product_id_hash); + Ok(blake2b256_keyed(&per_product_entropy, key)) +} + +fn blake2b256_keyed(message: &[u8], key: &[u8]) -> [u8; 32] { + let hash = blake2b(32, key, message); + hash.as_bytes() + .try_into() + .expect("BLAKE2b-256 returns 32 bytes") +} + +fn blake2b256(message: &[u8]) -> [u8; 32] { + blake2b256_keyed(message, &[]) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn secret() -> [u8; 32] { + let mut secret = [0u8; 32]; + for (i, byte) in secret.iter_mut().enumerate() { + *byte = i as u8; + } + secret + } + + #[test] + fn product_entropy_cases() { + struct SuccessCase { + name: &'static str, + product_id: &'static str, + key: Vec, + expected_hex: &'static str, + } + + let success_cases = vec![ + SuccessCase { + name: "single byte key", + product_id: "myapp.dot", + key: vec![1], + expected_hex: "4bafd6a34182959bad8914dcff88c6b6842d551d6f0067afbd407e9584223404", + }, + SuccessCase { + name: "text key", + product_id: "myapp.dot", + key: b"product-key".to_vec(), + expected_hex: "ab1887248c9de3cf4b8c5a255782796d3d35a98c8eb2d7df61a410db8b14da36", + }, + SuccessCase { + name: "localhost product", + product_id: "localhost:3000", + key: (0..32).map(|i| 255 - i).collect(), + expected_hex: "437d0a6236c51fe114cf6a16b79c9c2b5f95b1e105e2d5269cc254a8c593925f", + }, + ]; + + for case in success_cases { + let entropy = derive_product_entropy(&secret(), case.product_id, &case.key).unwrap(); + assert_eq!(hex::encode(entropy), case.expected_hex, "{}", case.name); + } + + let error_cases = vec![ + (Vec::new(), ProductEntropyError::InvalidKeyLength(0)), + (vec![0u8; 33], ProductEntropyError::InvalidKeyLength(33)), + ]; + for (key, expected) in error_cases { + assert_eq!( + derive_product_entropy(&secret(), "myapp.dot", &key).unwrap_err(), + expected, + ); + } + } +} diff --git a/rust/crates/truapi-server/src/host_logic/features.rs b/rust/crates/truapi-server/src/host_logic/features.rs new file mode 100644 index 00000000..a71a106b --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/features.rs @@ -0,0 +1,66 @@ +//! Feature-detection delegation. +//! +//! `feature_supported` is a platform syscall: each host owns the set of +//! chains it can service. This module is a thin shim that forwards the +//! request through to [`truapi_platform::Features`]. + +use truapi::v01::{GenericError, HostFeatureSupportedRequest, HostFeatureSupportedResponse}; +use truapi_platform::Features; + +/// Forward a feature-support query to the platform implementation. +pub async fn feature_supported( + platform: &P, + request: HostFeatureSupportedRequest, +) -> Result { + platform.feature_supported(request).await +} + +#[cfg(test)] +mod tests { + use super::*; + + struct AlwaysSupported; + + #[truapi_platform::async_trait] + impl Features for AlwaysSupported { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + assert!(matches!(request, HostFeatureSupportedRequest::Chain { .. })); + Ok(HostFeatureSupportedResponse { supported: true }) + } + } + + struct AlwaysUnsupported; + + #[truapi_platform::async_trait] + impl Features for AlwaysUnsupported { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + assert!(matches!(request, HostFeatureSupportedRequest::Chain { .. })); + Ok(HostFeatureSupportedResponse { supported: false }) + } + } + + fn req() -> HostFeatureSupportedRequest { + HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + } + } + + #[test] + fn delegates_supported_to_platform() { + let resp = futures::executor::block_on(feature_supported(&AlwaysSupported, req())).unwrap(); + assert!(resp.supported); + } + + #[test] + fn delegates_unsupported_to_platform() { + let resp = + futures::executor::block_on(feature_supported(&AlwaysUnsupported, req())).unwrap(); + assert!(!resp.supported); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/identity.rs b/rust/crates/truapi-server/src/host_logic/identity.rs new file mode 100644 index 00000000..0fbac76f --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/identity.rs @@ -0,0 +1,123 @@ +//! People-chain identity lookup for paired SSO sessions. +//! +//! dotli's previous host-papp path read `Resources.Consumers[account]` from +//! the People chain and used only the username fields. Keep this module narrow: +//! it builds that storage key and decodes the leading username fields from the +//! SCALE value. The record begins with a fixed identifier public key; credibility +//! and statement-store slots are intentionally ignored. + +use parity_scale_codec::Decode; +use sp_crypto_hashing::{blake2_128, twox_128}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PeopleIdentity { + pub lite_username: Option, + pub full_username: Option, +} + +#[derive(Debug, Decode)] +struct ConsumerUsernamePrefix { + full_username: Option>, + lite_username: Vec, +} + +/// Build the People-chain `Resources.Consumers` storage key for `account_id`. +pub fn resources_consumers_storage_key(account_id: &[u8; 32]) -> Vec { + let mut key = Vec::with_capacity(32 + 16 + account_id.len()); + key.extend_from_slice(&twox_128(b"Resources")); + key.extend_from_slice(&twox_128(b"Consumers")); + key.extend_from_slice(&blake2_128(account_id)); + key.extend_from_slice(account_id); + key +} + +/// Decode the username fields from a `Resources.Consumers` storage value. +pub fn decode_people_identity(value: &[u8]) -> Result { + if value.len() < 65 { + return Err(format!( + "invalid Resources.Consumers record: expected 65-byte identifier key, got {} bytes", + value.len() + )); + } + + // ConsumerInfo starts with a fixed 65-byte P-256 identifier key. The + // username fields follow immediately after it. + let mut input = &value[65..]; + let decoded = ConsumerUsernamePrefix::decode(&mut input) + .map_err(|err| format!("invalid Resources.Consumers record: {err}"))?; + let lite_username = non_empty_string(decoded.lite_username)?; + let full_username = decoded + .full_username + .map(non_empty_string) + .transpose()? + .flatten(); + Ok(PeopleIdentity { + lite_username, + full_username, + }) +} + +fn non_empty_string(bytes: Vec) -> Result, String> { + if bytes.is_empty() { + return Ok(None); + } + let value = String::from_utf8(bytes) + .map_err(|err| format!("Resources.Consumers username is not UTF-8: {err}"))?; + Ok(Some(value)) +} + +#[cfg(test)] +mod tests { + use super::*; + use parity_scale_codec::Encode; + + #[test] + fn resources_consumers_key_uses_expected_prefix() { + let key = resources_consumers_storage_key(&[0x42; 32]); + + assert_eq!(key.len(), 80); + assert_eq!(&key[..16], &twox_128(b"Resources")); + assert_eq!(&key[16..32], &twox_128(b"Consumers")); + assert_eq!(&key[48..], &[0x42; 32]); + } + + #[test] + fn twox128_matches_substrate_storage_prefix_vector() { + assert_eq!( + hex::encode(twox_128(b"System")), + "26aa394eea5630e07c48ae0c9558cef7" + ); + } + + #[test] + fn decodes_username_prefix_and_ignores_trailing_fields() { + let mut value = vec![0x04; 65]; + value.extend((Some(b"Alice Smith".to_vec()), b"alice.01".to_vec()).encode()); + value.extend_from_slice(&[0xff; 8]); + + let decoded = decode_people_identity(&value).expect("identity should decode"); + + assert_eq!(decoded.full_username.as_deref(), Some("Alice Smith")); + assert_eq!(decoded.lite_username.as_deref(), Some("alice.01")); + } + + #[test] + fn empty_full_username_is_none() { + let mut value = vec![0x04; 65]; + value.extend((Some(Vec::::new()), b"alice.01".to_vec()).encode()); + + let decoded = decode_people_identity(&value).expect("identity should decode"); + + assert_eq!(decoded.full_username, None); + assert_eq!(decoded.lite_username.as_deref(), Some("alice.01")); + } + + #[test] + fn rejects_missing_identifier_key() { + let value = (None::>, b"alice.01".to_vec()).encode(); + + let error = decode_people_identity(&value).expect_err("identity should reject"); + + assert!(error.contains("65-byte identifier key")); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/permissions.rs b/rust/crates/truapi-server/src/host_logic/permissions.rs new file mode 100644 index 00000000..b1189689 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/permissions.rs @@ -0,0 +1,706 @@ +//! Permission authorization state machine (ask -> authorized | denied), backed +//! by the platform [`CoreStorage`] trait with typed [`CoreStorageKey`] slots. +//! +//! Device permissions (camera, mic, NFC, ...) are separate from remote +//! permissions (domain access, chain submit, ...), so this module exposes two +//! `check_or_prompt` entrypoints that route to the matching platform callback. +//! The cache layer is shared but keys are typed so a device grant cannot +//! authorize a remote operation by accident. Keys are also scoped by product id +//! so one product's authorization never grants another product's request. + +use parity_scale_codec::{Decode, Encode}; + +use truapi::latest::{ + GenericError, HostDevicePermissionRequest, HostDevicePermissionResponse, RemotePermission, + RemotePermissionRequest, RemotePermissionResponse, +}; +use truapi_platform::{ + CoreStorage, CoreStorageKey, PermissionAuthorizationRequest, PermissionAuthorizationStatus, + Permissions, +}; + +/// Persisted answer for a single permission request. Keep `Authorized` at +/// discriminant 0 and `Denied` at 1 to preserve the existing two-variant cache +/// encoding. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +enum StoredAuthorizationStatus { + /// User authorized the permission. + Authorized, + /// User denied the permission. + Denied, +} + +impl From for PermissionAuthorizationStatus { + fn from(status: StoredAuthorizationStatus) -> Self { + match status { + StoredAuthorizationStatus::Authorized => PermissionAuthorizationStatus::Authorized, + StoredAuthorizationStatus::Denied => PermissionAuthorizationStatus::Denied, + } + } +} + +impl From for StoredAuthorizationStatus { + fn from(granted: bool) -> Self { + if granted { + Self::Authorized + } else { + Self::Denied + } + } +} + +/// Coordinator that inspects persisted state first, falls back to the +/// platform's prompt callback, and writes the authorization back so future +/// calls short-circuit. +pub struct PermissionsService<'a, S: CoreStorage + ?Sized, P: Permissions + ?Sized> { + storage: &'a S, + prompt: &'a P, + product_id: &'a str, +} + +impl<'a, S: CoreStorage + ?Sized, P: Permissions + ?Sized> PermissionsService<'a, S, P> { + /// Construct a service backed by the given storage + prompt callbacks. + pub fn new(storage: &'a S, prompt: &'a P, product_id: &'a str) -> Self { + Self { + storage, + prompt, + product_id, + } + } + + /// Returns the stored authorization status for a device permission without prompting. + pub async fn peek_device( + &self, + permission: &HostDevicePermissionRequest, + ) -> Result { + authorization_status( + self.storage, + device_core_storage_key(self.product_id, permission), + ) + .await + } + + /// Returns the stored authorization status for a remote permission without + /// prompting. + pub async fn peek_remote( + &self, + request: &RemotePermissionRequest, + ) -> Result { + authorization_status( + self.storage, + remote_core_storage_key(self.product_id, request), + ) + .await + } + + /// Returns the stored authorization status for a permission request + /// without prompting. + pub async fn authorization_status( + &self, + request: &PermissionAuthorizationRequest, + ) -> Result { + match request { + PermissionAuthorizationRequest::Device(permission) => { + self.peek_device(permission).await + } + PermissionAuthorizationRequest::Remote(request) => self.peek_remote(request).await, + } + } + + /// Returns the stored authorization statuses for permission requests + /// without prompting. Results follow the same order as `requests`. + pub async fn authorization_statuses( + &self, + requests: &[PermissionAuthorizationRequest], + ) -> Result, GenericError> { + let mut statuses = Vec::with_capacity(requests.len()); + for request in requests { + statuses.push(self.authorization_status(request).await?); + } + Ok(statuses) + } + + /// Update the stored authorization status for a permission request. + /// + /// Setting `NotDetermined` clears the stored value so the next product + /// request prompts again. + pub async fn set_authorization_status( + &self, + request: &PermissionAuthorizationRequest, + status: PermissionAuthorizationStatus, + ) -> Result<(), GenericError> { + let key = match request { + PermissionAuthorizationRequest::Device(permission) => { + device_core_storage_key(self.product_id, permission) + } + PermissionAuthorizationRequest::Remote(request) => { + remote_core_storage_key(self.product_id, request) + } + }; + set_authorization_status(self.storage, key, status).await + } + + /// Returns the cached device authorization if any, otherwise prompts the + /// platform's `device_permission` callback and persists the answer. + pub async fn check_or_prompt_device( + &self, + permission: HostDevicePermissionRequest, + ) -> Result { + let key = device_core_storage_key(self.product_id, &permission); + if let Some(cached) = peek_stored(self.storage, key.clone()).await? { + return Ok(cached.into()); + } + // Only a genuine user authorization is persisted. A prompt-callback error is + // transient (UI unavailable, IPC timeout), not a denial, so fail closed + // for this call but do not cache it — the next request re-prompts rather + // than locking the capability out permanently with no revoke path. + let authorization = match self.prompt.device_permission(permission).await { + Ok(HostDevicePermissionResponse { granted }) => granted.into(), + Err(_) => return Ok(PermissionAuthorizationStatus::Denied), + }; + self.persist_decision(key, authorization).await + } + + /// Returns the cached remote authorization if any, otherwise prompts the + /// platform's `remote_permission` callback and persists the answer. + pub async fn check_or_prompt_remote( + &self, + request: RemotePermissionRequest, + ) -> Result { + let key = remote_core_storage_key(self.product_id, &request); + if let Some(cached) = peek_stored(self.storage, key.clone()).await? { + return Ok(cached.into()); + } + // See `check_or_prompt_device`: persist only a genuine user decision; a + // transient callback error fails closed for this call without caching. + let authorization = match self.prompt.remote_permission(request).await { + Ok(RemotePermissionResponse { granted }) => granted.into(), + Err(_) => return Ok(PermissionAuthorizationStatus::Denied), + }; + self.persist_decision(key, authorization).await + } + + /// Persist a fresh user decision and return its public status. + async fn persist_decision( + &self, + key: CoreStorageKey, + authorization: StoredAuthorizationStatus, + ) -> Result { + self.storage + .write_core_storage(key, authorization.encode()) + .await?; + Ok(authorization.into()) + } +} + +async fn authorization_status( + storage: &S, + key: CoreStorageKey, +) -> Result { + Ok(peek_stored(storage, key) + .await? + .map(Into::into) + .unwrap_or(PermissionAuthorizationStatus::NotDetermined)) +} + +async fn peek_stored( + storage: &S, + key: CoreStorageKey, +) -> Result, GenericError> { + let Some(raw) = storage.read_core_storage(key).await? else { + return Ok(None); + }; + Ok(StoredAuthorizationStatus::decode(&mut &*raw).ok()) +} + +async fn set_authorization_status( + storage: &S, + key: CoreStorageKey, + status: PermissionAuthorizationStatus, +) -> Result<(), GenericError> { + match status_into_stored(status) { + Some(stored) => storage.write_core_storage(key, stored.encode()).await, + None => storage.clear_core_storage(key).await, + } +} + +fn status_into_stored(status: PermissionAuthorizationStatus) -> Option { + match status { + PermissionAuthorizationStatus::NotDetermined => None, + PermissionAuthorizationStatus::Denied => Some(StoredAuthorizationStatus::Denied), + PermissionAuthorizationStatus::Authorized => Some(StoredAuthorizationStatus::Authorized), + } +} + +fn device_core_storage_key( + product_id: &str, + permission: &HostDevicePermissionRequest, +) -> CoreStorageKey { + CoreStorageKey::PermissionAuthorization { + product_id: product_id.to_string(), + request: PermissionAuthorizationRequest::Device(*permission), + } +} + +fn remote_core_storage_key(product_id: &str, request: &RemotePermissionRequest) -> CoreStorageKey { + CoreStorageKey::PermissionAuthorization { + product_id: product_id.to_string(), + request: PermissionAuthorizationRequest::Remote(canonical_remote_request(request)), + } +} + +fn canonical_remote_request(request: &RemotePermissionRequest) -> RemotePermissionRequest { + let permission = match &request.permission { + RemotePermission::Remote { domains } => { + // DNS domains are case-insensitive, so a logically-identical bundle + // requested with different casing or duplicate entries must + // canonicalize to one key (no spurious re-prompt). + let mut canonical: Vec = + domains.iter().map(|d| d.to_ascii_lowercase()).collect(); + canonical.sort(); + canonical.dedup(); + RemotePermission::Remote { domains: canonical } + } + other => other.clone(), + }; + RemotePermissionRequest { permission } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::lock::Mutex; + use std::collections::HashMap; + use std::sync::atomic::{AtomicUsize, Ordering}; + use truapi::v01; + use truapi::v01::GenericError; + + #[derive(Default)] + struct MemStorage { + inner: Mutex>>, + } + + #[truapi_platform::async_trait] + impl CoreStorage for MemStorage { + async fn read_core_storage( + &self, + key: CoreStorageKey, + ) -> Result>, v01::GenericError> { + Ok(self.inner.lock().await.get(&test_key(key)).cloned()) + } + async fn write_core_storage( + &self, + key: CoreStorageKey, + value: Vec, + ) -> Result<(), v01::GenericError> { + self.inner.lock().await.insert(test_key(key), value); + Ok(()) + } + async fn clear_core_storage(&self, key: CoreStorageKey) -> Result<(), v01::GenericError> { + self.inner.lock().await.remove(&test_key(key)); + Ok(()) + } + } + + fn test_key(key: CoreStorageKey) -> String { + hex::encode(key.encode()) + } + + struct ScriptedPrompt { + device_answers: Mutex>, + remote_answers: Mutex>, + device_calls: AtomicUsize, + remote_calls: AtomicUsize, + } + + impl ScriptedPrompt { + fn new(device_answers: Vec, remote_answers: Vec) -> Self { + Self { + device_answers: Mutex::new(device_answers), + remote_answers: Mutex::new(remote_answers), + device_calls: AtomicUsize::new(0), + remote_calls: AtomicUsize::new(0), + } + } + } + + #[truapi_platform::async_trait] + impl Permissions for ScriptedPrompt { + async fn device_permission( + &self, + _request: HostDevicePermissionRequest, + ) -> Result { + self.device_calls.fetch_add(1, Ordering::SeqCst); + let granted = self + .device_answers + .lock() + .await + .pop() + .expect("ScriptedPrompt ran out of device answers"); + Ok(v01::HostDevicePermissionResponse { granted }) + } + + async fn remote_permission( + &self, + _request: RemotePermissionRequest, + ) -> Result { + self.remote_calls.fetch_add(1, Ordering::SeqCst); + let granted = self + .remote_answers + .lock() + .await + .pop() + .expect("ScriptedPrompt ran out of remote answers"); + Ok(v01::RemotePermissionResponse { granted }) + } + } + + #[test] + fn core_storage_key_separates_product_device_and_remote_variants() { + let camera = device_core_storage_key("product.dot", &HostDevicePermissionRequest::Camera); + let other_product = + device_core_storage_key("other.dot", &HostDevicePermissionRequest::Camera); + let remote = remote_core_storage_key( + "product.dot", + &RemotePermissionRequest { + permission: RemotePermission::ChainSubmit, + }, + ); + + assert_ne!(camera, other_product); + assert_ne!(camera, remote); + } + + #[test] + fn remote_core_storage_key_canonicalizes_domain_sets() { + let unsorted = RemotePermissionRequest { + permission: RemotePermission::Remote { + domains: vec!["b.example.com".into(), "a.example.com".into()], + }, + }; + let sorted = RemotePermissionRequest { + permission: RemotePermission::Remote { + domains: vec!["a.example.com".into(), "b.example.com".into()], + }, + }; + assert_eq!( + remote_core_storage_key("product.dot", &unsorted), + remote_core_storage_key("product.dot", &sorted) + ); + + let mixed = RemotePermissionRequest { + permission: RemotePermission::Remote { + domains: vec!["Example.COM".into(), "a.com".into(), "a.com".into()], + }, + }; + let canonical = RemotePermissionRequest { + permission: RemotePermission::Remote { + domains: vec!["a.com".into(), "example.com".into()], + }, + }; + assert_eq!( + remote_core_storage_key("product.dot", &mixed), + remote_core_storage_key("product.dot", &canonical) + ); + } + + #[test] + fn remote_core_storage_key_handles_separator_chars_in_domains() { + // Domain strings containing separator-looking text must not be able to + // forge a key that matches an unrelated permission. + let injecting = RemotePermissionRequest { + permission: RemotePermission::Remote { + domains: vec!["a|b".into(), "c,d".into(), "remote:web-rtc".into()], + }, + }; + let benign_same_set = RemotePermissionRequest { + permission: RemotePermission::Remote { + domains: vec!["x".into(), "y".into(), "z".into()], + }, + }; + let injecting_key = remote_core_storage_key("product.dot", &injecting); + let benign_key = remote_core_storage_key("product.dot", &benign_same_set); + assert_ne!(injecting_key, benign_key); + + // The injecting permission must also be distinct from the `WebRtc` + // variant it tries to impersonate via crafted strings. + let webrtc = RemotePermissionRequest { + permission: RemotePermission::WebRtc, + }; + assert_ne!( + injecting_key, + remote_core_storage_key("product.dot", &webrtc) + ); + + // Re-ordering the same domains still collapses to a single key + // (canonicalization is order-independent). + let injecting_reordered = RemotePermissionRequest { + permission: RemotePermission::Remote { + domains: vec!["remote:web-rtc".into(), "c,d".into(), "a|b".into()], + }, + }; + assert_eq!( + injecting_key, + remote_core_storage_key("product.dot", &injecting_reordered) + ); + } + + #[test] + fn check_or_prompt_device_caches_grant() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![true], vec![]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let first = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + let second = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + + assert_eq!(first, PermissionAuthorizationStatus::Authorized); + assert_eq!(second, PermissionAuthorizationStatus::Authorized); + assert_eq!(prompt.device_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn check_or_prompt_remote_caches_denial() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![], vec![false]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let request = RemotePermissionRequest { + permission: RemotePermission::ChainSubmit, + }; + let first = + futures::executor::block_on(service.check_or_prompt_remote(request.clone())).unwrap(); + let second = futures::executor::block_on(service.check_or_prompt_remote(request)).unwrap(); + + assert_eq!(first, PermissionAuthorizationStatus::Denied); + assert_eq!(second, PermissionAuthorizationStatus::Denied); + assert_eq!(prompt.remote_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn device_and_remote_caches_are_independent() { + let storage = MemStorage::default(); + // Device denies, remote grants. If the caches collided we'd see the + // same answer on the second call. + let prompt = ScriptedPrompt::new(vec![false], vec![true]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let device = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + let remote = + futures::executor::block_on(service.check_or_prompt_remote(RemotePermissionRequest { + permission: RemotePermission::ChainSubmit, + })) + .unwrap(); + + assert_eq!(device, PermissionAuthorizationStatus::Denied); + assert_eq!(remote, PermissionAuthorizationStatus::Authorized); + assert_eq!(prompt.device_calls.load(Ordering::SeqCst), 1); + assert_eq!(prompt.remote_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn device_prompt_does_not_invoke_remote_callback() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![true], vec![]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let _ = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + assert_eq!(prompt.device_calls.load(Ordering::SeqCst), 1); + assert_eq!(prompt.remote_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn remote_prompt_does_not_invoke_device_callback() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![], vec![true]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let _ = + futures::executor::block_on(service.check_or_prompt_remote(RemotePermissionRequest { + permission: RemotePermission::WebRtc, + })) + .unwrap(); + assert_eq!(prompt.device_calls.load(Ordering::SeqCst), 0); + assert_eq!(prompt.remote_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn peek_returns_not_determined_until_authorized() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![true], vec![]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let before = + futures::executor::block_on(service.peek_device(&HostDevicePermissionRequest::Camera)) + .unwrap(); + assert_eq!(before, PermissionAuthorizationStatus::NotDetermined); + + futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + + let after = + futures::executor::block_on(service.peek_device(&HostDevicePermissionRequest::Camera)) + .unwrap(); + assert_eq!(after, PermissionAuthorizationStatus::Authorized); + } + + #[test] + fn set_authorization_status_writes_and_clears() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![], vec![]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + let request = PermissionAuthorizationRequest::Device(HostDevicePermissionRequest::Camera); + + futures::executor::block_on( + service.set_authorization_status(&request, PermissionAuthorizationStatus::Authorized), + ) + .unwrap(); + assert_eq!( + futures::executor::block_on(service.authorization_status(&request)).unwrap(), + PermissionAuthorizationStatus::Authorized + ); + + futures::executor::block_on( + service + .set_authorization_status(&request, PermissionAuthorizationStatus::NotDetermined), + ) + .unwrap(); + assert_eq!( + futures::executor::block_on(service.authorization_status(&request)).unwrap(), + PermissionAuthorizationStatus::NotDetermined + ); + } + + /// Prompt callback that always errors, to exercise the transient-failure + /// path (fail closed for the current call, but do not persist the error). + struct FailingPrompt; + + #[truapi_platform::async_trait] + impl Permissions for FailingPrompt { + async fn device_permission( + &self, + _request: HostDevicePermissionRequest, + ) -> Result { + Err(GenericError { + reason: "boom".into(), + }) + } + + async fn remote_permission( + &self, + _request: RemotePermissionRequest, + ) -> Result { + Err(GenericError { + reason: "boom".into(), + }) + } + } + + #[test] + fn prompt_failure_denies_without_persisting() { + let storage = MemStorage::default(); + let prompt = FailingPrompt; + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let decision = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + assert_eq!(decision, PermissionAuthorizationStatus::Denied); + + // A transient callback error is fail-closed for this call but NOT + // cached, so peek still sees no authorization and the next request + // re-prompts rather than permanently locking out the capability. + let cached = + futures::executor::block_on(service.peek_device(&HostDevicePermissionRequest::Camera)) + .unwrap(); + assert_eq!( + cached, + PermissionAuthorizationStatus::NotDetermined, + "a transient prompt error must not be persisted" + ); + } + + /// A corrupt SCALE-encoded cache entry must be treated as "no cache", + /// not panic. The service falls back to prompting. + #[test] + fn corrupt_cache_entry_returns_none() { + let storage = MemStorage::default(); + // Write garbage bytes under the canonical key. + futures::executor::block_on(storage.write_core_storage( + device_core_storage_key("product.dot", &HostDevicePermissionRequest::Camera), + vec![0xff, 0xfe, 0xfd], + )) + .unwrap(); + + let prompt = ScriptedPrompt::new(vec![true], vec![]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let peeked = + futures::executor::block_on(service.peek_device(&HostDevicePermissionRequest::Camera)) + .unwrap(); + assert_eq!( + peeked, + PermissionAuthorizationStatus::NotDetermined, + "corrupt entry must decode as absent" + ); + } + + /// Storage failures must propagate to the caller; the service must not + /// swallow them by silently returning a default authorization. + #[derive(Default)] + struct FailingStorage; + + #[truapi_platform::async_trait] + impl CoreStorage for FailingStorage { + async fn read_core_storage( + &self, + _key: CoreStorageKey, + ) -> Result>, v01::GenericError> { + Err(v01::GenericError { + reason: "read failed".into(), + }) + } + async fn write_core_storage( + &self, + _key: CoreStorageKey, + _value: Vec, + ) -> Result<(), v01::GenericError> { + Err(v01::GenericError { + reason: "write failed".into(), + }) + } + async fn clear_core_storage(&self, _key: CoreStorageKey) -> Result<(), v01::GenericError> { + Err(v01::GenericError { + reason: "clear failed".into(), + }) + } + } + + #[test] + fn storage_read_error_propagates() { + let storage = FailingStorage; + let prompt = ScriptedPrompt::new(vec![], vec![]); + let service = PermissionsService::new(&storage, &prompt, "product.dot"); + + let err = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .expect_err("read failure must surface"); + assert!(matches!(err, v01::GenericError { .. })); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/product_account.rs b/rust/crates/truapi-server/src/host_logic/product_account.rs new file mode 100644 index 00000000..c4b16ec0 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/product_account.rs @@ -0,0 +1,175 @@ +//! Product account derivation shared by all hosts. +//! +//! Mirrors dotli's `packages/auth/src/account.ts`: derive an sr25519 public +//! key through soft HDKD junctions `["product", product_id, derivation_index]`. + +use blake2_rfc::blake2b::blake2b; +use parity_scale_codec::Encode; +use schnorrkel::PublicKey; +use schnorrkel::derive::{ChainCode, Derivation}; +use thiserror::Error; +use unicode_normalization::UnicodeNormalization; + +const JUNCTION_ID_LEN: usize = 32; +const PRODUCT_JUNCTION: &str = "product"; +const SS58_PREFIX: &[u8] = b"SS58PRE"; +const SUBSTRATE_GENERIC_SS58_PREFIX: u8 = 42; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ProductAccountError { + #[error("invalid sr25519 root public key")] + InvalidRootPublicKey, + #[error("numeric derivation junction is outside u64 range")] + NumericJunctionOutOfRange, +} + +/// Whether `identifier` is a product scope the core is allowed to derive for. +pub fn is_product_identifier(identifier: &str) -> bool { + let normalized = normalize_product_identifier(identifier); + normalized.ends_with(".dot") + || normalized == "localhost" + || normalized.starts_with("localhost:") +} + +/// Normalize product identifiers before derivation and policy checks. +pub fn normalize_product_identifier(identifier: &str) -> String { + identifier.nfc().collect::().to_lowercase() +} + +/// Derive a product account public key from the paired root public key. +pub fn derive_product_public_key( + root_public_key: [u8; 32], + product_id: &str, + derivation_index: u32, +) -> Result<[u8; 32], ProductAccountError> { + let mut public_key = PublicKey::from_bytes(&root_public_key) + .map_err(|_| ProductAccountError::InvalidRootPublicKey)?; + + for junction in [ + PRODUCT_JUNCTION.to_string(), + product_id.to_string(), + derivation_index.to_string(), + ] { + let chain_code = ChainCode(create_chain_code(&junction)?); + let (derived, _) = public_key.derived_key_simple(chain_code, []); + public_key = derived; + } + + Ok(public_key.to_bytes()) +} + +/// Encode a product account public key as a generic Substrate SS58 address. +pub fn product_public_key_to_address(public_key: [u8; 32]) -> String { + let mut payload = Vec::with_capacity(35); + payload.push(SUBSTRATE_GENERIC_SS58_PREFIX); + payload.extend_from_slice(&public_key); + + let mut checksum_input = Vec::with_capacity(SS58_PREFIX.len() + payload.len()); + checksum_input.extend_from_slice(SS58_PREFIX); + checksum_input.extend_from_slice(&payload); + let checksum = blake2b(64, &[], &checksum_input); + payload.extend_from_slice(&checksum.as_bytes()[..2]); + + bs58::encode(payload).into_string() +} + +fn create_chain_code(code: &str) -> Result<[u8; 32], ProductAccountError> { + let encoded = if is_numeric_junction(code) { + code.parse::() + .map_err(|_| ProductAccountError::NumericJunctionOutOfRange)? + .encode() + } else { + code.encode() + }; + + let mut chain_code = [0u8; JUNCTION_ID_LEN]; + if encoded.len() > JUNCTION_ID_LEN { + let hash = blake2b(JUNCTION_ID_LEN, &[], &encoded); + chain_code.copy_from_slice(hash.as_bytes()); + } else { + chain_code[..encoded.len()].copy_from_slice(&encoded); + } + Ok(chain_code) +} + +fn is_numeric_junction(code: &str) -> bool { + !code.is_empty() && code.bytes().all(|byte| byte.is_ascii_digit()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const ROOT_PUBLIC_KEY: [u8; 32] = [ + 0x80, 0x05, 0x28, 0xc9, 0x55, 0x87, 0x3e, 0x4c, 0x78, 0xb7, 0xdf, 0x24, 0xf7, 0x1d, 0xb8, + 0xf5, 0x81, 0xaa, 0x99, 0xe3, 0x49, 0x3b, 0xf4, 0x96, 0xed, 0xf1, 0x51, 0xab, 0xc1, 0xd7, + 0x20, 0x23, + ]; + + #[test] + fn derives_dotli_product_account_vector() { + let derived = derive_product_public_key(ROOT_PUBLIC_KEY, "myapp.dot", 0).unwrap(); + assert_eq!( + hex::encode(derived), + "281489e3dd1c4dbe88cd670a59edcc9c44d64f510d302bd527ec306f10292f08" + ); + } + + #[test] + fn derives_different_index_vector() { + let derived = derive_product_public_key(ROOT_PUBLIC_KEY, "myapp.dot", 1).unwrap(); + assert_eq!( + hex::encode(derived), + "ec8a80808b46e44c1351b68e295eb975c55bda4855e5ea9fc1325be7296a2a4e" + ); + } + + #[test] + fn derives_long_product_id_vector() { + let derived = derive_product_public_key( + ROOT_PUBLIC_KEY, + "w-credentialless-staticblitz-com.local-credentialless.webcontainer-api.io", + 0, + ) + .unwrap(); + assert_eq!( + hex::encode(derived), + "56769a234038defb62a7ad42f251091cc24846c2473a31b5bdd17d366c38c211" + ); + } + + #[test] + fn ss58_address_matches_dotli_vector() { + let derived = derive_product_public_key(ROOT_PUBLIC_KEY, "myapp.dot", 0).unwrap(); + assert_eq!( + product_public_key_to_address(derived), + "5CyFsdhwjXy7wWpDEM6isungQ3LfGnu9UXkt7paBQ6DYRxk1" + ); + } + + #[test] + fn accepts_dot_and_localhost_product_identifiers() { + assert!(is_product_identifier("Example.DOT")); + assert!(is_product_identifier("localhost")); + assert!(is_product_identifier("localhost:3000")); + assert!(!is_product_identifier("example.com")); + } + + #[test] + fn chain_code_matches_dotli_encoding_rules() { + let product = create_chain_code("product").unwrap(); + assert_eq!( + &product[..8], + &[0x1c, b'p', b'r', b'o', b'd', b'u', b'c', b't'] + ); + + let zero = create_chain_code("0").unwrap(); + assert_eq!(&zero[..8], &[0; 8]); + + let long = create_chain_code( + "w-credentialless-staticblitz-com.local-credentialless.webcontainer-api.io", + ) + .unwrap(); + assert_ne!(&long[..8], &[0; 8]); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/session.rs b/rust/crates/truapi-server/src/host_logic/session.rs new file mode 100644 index 00000000..e2b1eeb3 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/session.rs @@ -0,0 +1,416 @@ +//! Core-owned active-session state. Platform entrypoints notify the core when +//! pairing or unpairing changes the session, and account-management methods +//! read this state instead of round-tripping a host callback on every product +//! call. + +use futures::channel::mpsc; +use futures::stream::{self, BoxStream, StreamExt}; +use parity_scale_codec::{Decode, Encode}; +use std::sync::{Arc, Mutex}; + +use truapi::v01::HostAccountConnectionStatusSubscribeItem; +use truapi::versioned::account::HostAccountConnectionStatusSubscribeItem as VersionedItem; + +/// Session info pushed by the host. The 32-byte sr25519 public key plus +/// optional usernames sourced from the People-Chain identity record. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct SessionInfo { + /// 32-byte sr25519 root public key of the paired session. + pub public_key: [u8; 32], + /// Core-owned SSO channel state. Core-run pairing fills this; unavailable + /// sessions restored from older test fixtures may leave it empty. + pub sso: Option, + /// Wallet-provided source for deterministic product entropy. + pub root_entropy_source: Option<[u8; 32]>, + /// Wallet identity account id used for People-chain username lookup. + pub identity_account_id: Option<[u8; 32]>, + /// Short username (e.g. `alice`). + pub lite_username: Option, + /// Fully qualified username (e.g. `Alice Smith`). + pub full_username: Option, +} + +/// Core-owned SSO session material negotiated with the wallet during pairing. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct SsoSessionInfo { + /// Core's own 64-byte expanded sr25519 statement-store secret. + pub ss_secret: [u8; 64], + /// Core's own session sr25519 statement-store public key. + pub ss_public_key: [u8; 32], + /// Core's P-256 ECDH private key. + pub enc_secret: [u8; 32], + /// Wallet persistent P-256 public key. + pub peer_enc_pubkey: [u8; 65], + /// Wallet identity sr25519 account id. + pub identity_account_id: [u8; 32], + /// Core -> wallet topic id. + pub session_id_own: [u8; 32], + /// Wallet -> core topic id. + pub session_id_peer: [u8; 32], + /// Statement channel for core requests. + pub request_channel: [u8; 32], + /// Statement channel for wallet responses to core requests. + pub response_channel: [u8; 32], + /// Statement channel for wallet-initiated requests. + pub peer_request_channel: [u8; 32], +} + +const PERSISTED_SESSION_VERSION: u8 = 3; + +/// Encode the active-session fields the core currently understands into an +/// opaque host-global session blob. Later SSO channel state should bump +/// `PERSISTED_SESSION_VERSION` instead of extending this layout silently. +pub fn encode_persisted_session(info: &SessionInfo) -> Vec { + (PERSISTED_SESSION_VERSION, info).encode() +} + +/// Decode a core-owned persisted session blob. +pub fn decode_persisted_session(blob: &[u8]) -> Result { + let mut input = blob; + let version = u8::decode(&mut input).map_err(|err| format!("invalid session blob: {err}"))?; + let info = match version { + PERSISTED_SESSION_VERSION => { + SessionInfo::decode(&mut input).map_err(|err| format!("invalid session blob: {err}"))? + } + _ => return Err(format!("unsupported session blob version {version}")), + }; + if !input.is_empty() { + return Err("invalid session blob: trailing bytes".to_string()); + } + Ok(info) +} + +/// Holds the currently-active session and broadcasts connection-status +/// transitions to subscribers. Cheap to clone via `Arc`. +#[derive(Default)] +pub struct SessionState { + inner: Mutex, +} + +#[derive(Default)] +struct Inner { + current: Option, + subscribers: Vec>, +} + +impl SessionState { + /// Construct a fresh session holder, starting in the `Disconnected` state. + pub fn new() -> Arc { + Arc::new(Self::default()) + } + + /// Replace the active session with `info`. Emits a `Connected` event to + /// every live subscriber if this is a transition from no-session or an + /// actual session replacement. + pub fn set_session(&self, info: SessionInfo) { + let mut inner = self.inner.lock().expect("session-state mutex poisoned"); + let should_broadcast = inner.current.as_ref() != Some(&info); + inner.current = Some(info); + if should_broadcast { + broadcast( + &mut inner.subscribers, + HostAccountConnectionStatusSubscribeItem::Connected, + ); + } + } + + /// Drop the active session. Emits a `Disconnected` event to every live + /// subscriber if there was a session to clear. + pub fn clear_session(&self) { + let mut inner = self.inner.lock().expect("session-state mutex poisoned"); + if inner.current.take().is_some() { + broadcast( + &mut inner.subscribers, + HostAccountConnectionStatusSubscribeItem::Disconnected, + ); + } + } + + /// Snapshot of the current session, or `None` when nothing is paired. + pub fn current(&self) -> Option { + self.inner + .lock() + .expect("session-state mutex poisoned") + .current + .clone() + } + + /// Stream of connection-status events. The first item emitted is the + /// current state (so subscribers don't have to read it separately); + /// subsequent items reflect every `set_session` / `clear_session` + /// transition. + pub fn subscribe(&self) -> BoxStream<'static, VersionedItem> { + let (tx, rx) = mpsc::unbounded(); + let mut inner = self.inner.lock().expect("session-state mutex poisoned"); + let initial = match inner.current { + Some(_) => HostAccountConnectionStatusSubscribeItem::Connected, + None => HostAccountConnectionStatusSubscribeItem::Disconnected, + }; + inner.subscribers.push(tx); + let initial_item = VersionedItem::V1(initial); + Box::pin(stream::once(async move { initial_item }).chain(rx)) + } +} + +fn broadcast( + subscribers: &mut Vec>, + status: HostAccountConnectionStatusSubscribeItem, +) { + let item = VersionedItem::V1(status); + // `retain` drops senders whose receiver has been dropped, so the + // subscriber list self-prunes on the next broadcast after a reader + // unsubscribes. + subscribers.retain(|tx| tx.unbounded_send(item.clone()).is_ok()); +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::executor::block_on; + use futures::{FutureExt, StreamExt}; + + fn info(pubkey_byte: u8) -> SessionInfo { + SessionInfo { + public_key: [pubkey_byte; 32], + sso: None, + root_entropy_source: None, + identity_account_id: None, + lite_username: Some("alice".to_string()), + full_username: None, + } + } + + #[test] + fn current_starts_empty() { + let state = SessionState::new(); + assert!(state.current().is_none()); + } + + #[test] + fn set_then_current_returns_session() { + let state = SessionState::new(); + state.set_session(info(0x42)); + let got = state.current().expect("session should be present"); + assert_eq!(got.public_key, [0x42; 32]); + assert_eq!(got.lite_username.as_deref(), Some("alice")); + } + + #[test] + fn persisted_session_round_trips() { + let mut session = info(0x42); + session.root_entropy_source = Some([1; 32]); + session.full_username = Some("Alice Smith".to_string()); + + let blob = encode_persisted_session(&session); + let decoded = decode_persisted_session(&blob).expect("session should decode"); + + assert_eq!(decoded, session); + } + + #[test] + fn persisted_sso_session_round_trips() { + let mut session = info(0x42); + session.sso = Some(SsoSessionInfo { + ss_secret: [1; 64], + ss_public_key: [2; 32], + enc_secret: [3; 32], + peer_enc_pubkey: [4; 65], + identity_account_id: [5; 32], + session_id_own: [6; 32], + session_id_peer: [7; 32], + request_channel: [8; 32], + response_channel: [9; 32], + peer_request_channel: [10; 32], + }); + + let blob = encode_persisted_session(&session); + let decoded = decode_persisted_session(&blob).expect("session should decode"); + + assert_eq!(decoded, session); + } + + #[test] + fn persisted_session_rejects_unknown_version() { + let mut blob = encode_persisted_session(&info(0x42)); + blob[0] = 0xff; + + let err = decode_persisted_session(&blob).unwrap_err(); + + assert_eq!(err, "unsupported session blob version 255"); + } + + #[test] + fn persisted_session_rejects_legacy_v2() { + let blob = vec![2]; + + let err = decode_persisted_session(&blob).unwrap_err(); + + assert_eq!(err, "unsupported session blob version 2"); + } + + #[test] + fn persisted_session_rejects_trailing_bytes() { + let mut blob = encode_persisted_session(&info(0x42)); + blob.push(0); + + let err = decode_persisted_session(&blob).unwrap_err(); + + assert_eq!(err, "invalid session blob: trailing bytes"); + } + + #[test] + fn clear_returns_to_empty() { + let state = SessionState::new(); + state.set_session(info(0x01)); + state.clear_session(); + assert!(state.current().is_none()); + } + + #[test] + fn subscribe_emits_current_state_first() { + let state = SessionState::new(); + state.set_session(info(0x01)); + let mut stream = state.subscribe(); + let first = block_on(stream.next()).expect("expected initial item"); + assert_eq!( + first, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + } + + #[test] + fn subscribe_emits_disconnected_when_no_session() { + let state = SessionState::new(); + let mut stream = state.subscribe(); + let first = block_on(stream.next()).expect("expected initial item"); + assert_eq!( + first, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Disconnected) + ); + } + + #[test] + fn set_session_broadcasts_connected_to_existing_subscribers() { + let state = SessionState::new(); + let mut stream = state.subscribe(); + let _ = block_on(stream.next()); + + state.set_session(info(0x01)); + let next = block_on(stream.next()).expect("expected Connected event"); + assert_eq!( + next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + } + + #[test] + fn clear_session_broadcasts_disconnected_to_existing_subscribers() { + let state = SessionState::new(); + state.set_session(info(0x01)); + let mut stream = state.subscribe(); + let _ = block_on(stream.next()); + + state.clear_session(); + let next = block_on(stream.next()).expect("expected Disconnected event"); + assert_eq!( + next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Disconnected) + ); + } + + #[test] + fn set_session_with_same_info_does_not_re_emit_connected() { + let state = SessionState::new(); + state.set_session(info(0x01)); + let mut stream = state.subscribe(); + let _ = block_on(stream.next()); + + state.set_session(info(0x01)); + + let pending = stream.next().now_or_never(); + assert!( + pending.is_none(), + "no transition event expected for equivalent session" + ); + } + + #[test] + fn set_session_with_replacement_re_emits_connected() { + let state = SessionState::new(); + state.set_session(info(0x01)); + let mut stream = state.subscribe(); + let _ = block_on(stream.next()); + + state.set_session(info(0x02)); + + let next = block_on(stream.next()).expect("expected replacement Connected event"); + assert_eq!( + next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + } + + #[test] + fn multi_subscriber_broadcast() { + let state = SessionState::new(); + let mut a = state.subscribe(); + let mut b = state.subscribe(); + // Drain initial Disconnected from both. + let _ = block_on(a.next()); + let _ = block_on(b.next()); + + state.set_session(info(0x77)); + let a_next = block_on(a.next()).expect("a should receive Connected"); + let b_next = block_on(b.next()).expect("b should receive Connected"); + assert_eq!( + a_next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + assert_eq!( + b_next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + } + + /// Clearing a never-set session is a no-op and must not synthesize a + /// spurious `Disconnected` event for live subscribers. + #[test] + fn clear_when_empty_is_silent_no_op() { + let state = SessionState::new(); + let mut stream = state.subscribe(); + // Drain the initial Disconnected. + let _ = block_on(stream.next()); + + state.clear_session(); + + let pending = stream.next().now_or_never(); + assert!(pending.is_none(), "no event expected when clear is a no-op",); + } + + /// Dropping a subscriber's stream must remove that sender from the + /// broadcast list. The next broadcast prunes it; the surviving stream + /// still receives the event. + #[test] + fn dropped_subscriber_is_pruned() { + let state = SessionState::new(); + let mut survivor = state.subscribe(); + let dropping = state.subscribe(); + let _ = block_on(survivor.next()); + // Drain the initial item from the dropping stream too so we don't + // accidentally test buffered-but-undelivered. + drop(dropping); + + state.set_session(info(0x33)); + let next = block_on(survivor.next()).expect("survivor must receive Connected"); + assert_eq!( + next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected), + ); + + // Internally, `set_session`'s broadcast call `retain`-prunes any + // dropped senders. After the call the subscribers list should have + // exactly one entry (the survivor). + let inner = state.inner.lock().unwrap(); + assert_eq!(inner.subscribers.len(), 1, "dropped subscriber not pruned"); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/session_store.rs b/rust/crates/truapi-server/src/host_logic/session_store.rs new file mode 100644 index 00000000..45cc0edb --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/session_store.rs @@ -0,0 +1,97 @@ +//! Core-side invalidation signal for host-global session storage. +//! +//! The host owns persistence; the core owns decoding and projecting the +//! current blob into `SessionState` and `AuthState`. This notifier is just the +//! "the backing store may have changed" signal that drives a re-read. + +use std::sync::{Arc, Mutex}; + +use futures::channel::mpsc; +use futures::stream::{self, BoxStream, StreamExt}; + +#[derive(Default)] +pub struct SessionStoreChangeNotifier { + subscribers: Mutex>>, +} + +impl SessionStoreChangeNotifier { + /// Create a notifier with no subscribers. + pub fn new() -> Arc { + Arc::new(Self::default()) + } + + /// Broadcast a storage-change tick to current subscribers. + pub fn notify(&self) { + let mut subscribers = self + .subscribers + .lock() + .expect("session-store notifier mutex poisoned"); + subscribers.retain(|tx| tx.unbounded_send(()).is_ok()); + } + + /// Subscribe to storage-change ticks, including one initial tick. + pub fn subscribe(&self) -> BoxStream<'static, ()> { + let (tx, rx) = mpsc::unbounded(); + self.subscribers + .lock() + .expect("session-store notifier mutex poisoned") + .push(tx); + Box::pin(stream::once(async {}).chain(rx)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::executor::block_on; + use futures::{FutureExt, StreamExt}; + + #[test] + fn subscribe_emits_initial_tick() { + let notifier = SessionStoreChangeNotifier::new(); + let mut ticks = notifier.subscribe(); + + assert!(block_on(ticks.next()).is_some()); + } + + #[test] + fn notify_broadcasts_to_subscribers() { + let notifier = SessionStoreChangeNotifier::new(); + let mut first = notifier.subscribe(); + let mut second = notifier.subscribe(); + let _ = block_on(first.next()); + let _ = block_on(second.next()); + + notifier.notify(); + + assert!(block_on(first.next()).is_some()); + assert!(block_on(second.next()).is_some()); + } + + #[test] + fn dropped_subscriber_is_pruned_on_next_notify() { + let notifier = SessionStoreChangeNotifier::new(); + let dropped = notifier.subscribe(); + drop(dropped); + + notifier.notify(); + + assert_eq!( + notifier + .subscribers + .lock() + .expect("session-store notifier mutex poisoned") + .len(), + 0 + ); + } + + #[test] + fn no_tick_without_notify_after_initial() { + let notifier = SessionStoreChangeNotifier::new(); + let mut ticks = notifier.subscribe(); + let _ = block_on(ticks.next()); + + assert!(ticks.next().now_or_never().is_none()); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/sso.rs b/rust/crates/truapi-server/src/host_logic/sso.rs new file mode 100644 index 00000000..05da0780 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/sso.rs @@ -0,0 +1,2 @@ +pub mod messages; +pub mod pairing; diff --git a/rust/crates/truapi-server/src/host_logic/sso/messages.rs b/rust/crates/truapi-server/src/host_logic/sso/messages.rs new file mode 100644 index 00000000..6d782242 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/sso/messages.rs @@ -0,0 +1,1029 @@ +//! SCALE codecs for host-papp SSO session-channel messages. +//! +//! These are the encrypted payloads carried inside statement-store +//! `SsoStatementData::Request` / `Response` frames. +//! The runtime builds them when forwarding TrUAPI account, signing, resource +//! allocation, and transaction requests to the paired wallet, then decodes the +//! wallet's responses while waiting on the SSO statement-store channels. +//! Field order and enum variant order are kept wire-compatible with host-papp: +//! +//! +//! +//! + +use parity_scale_codec::{Decode, Encode, OptionBool}; +use truapi::latest::{ + AccountId, AllocatableResource, HostAccountGetAliasResponse, HostSignPayloadRequest, + HostSignRawRequest, LegacyAccountTxPayload, ProductAccountId, ProductAccountTxPayload, + RawPayload, +}; + +use crate::host_logic::session::SsoSessionInfo; +use crate::host_logic::sso::pairing::{ + AES_GCM_NONCE_LEN, SsoStatementData, decrypt_session_statement_data, + encrypt_session_statement_data, encrypt_session_statement_data_with_nonce, +}; +use crate::host_logic::statement_store::{ + build_signed_session_request_statement, current_unix_secs, decode_verified_statement_data, + statement_expiry_elapsed, +}; + +const SSO_RESPONSE_CODE_SUCCESS: u8 = 0; + +/// Top-level wallet remote message sent over the encrypted SSO channel. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct RemoteMessage { + /// Correlation id used to match wallet responses to host requests. + pub message_id: String, + /// Versioned remote message body. + pub data: RemoteMessageData, +} + +/// Versioned remote message body. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum RemoteMessageData { + V1(RemoteMessageV1), +} + +/// v1 messages exchanged with the paired wallet over the encrypted SSO channel. +/// +/// The variant order is part of the SCALE wire protocol used inside +/// statement-store session statements. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum RemoteMessageV1 { + Disconnected, + SignRequest(Box), + SignResponse(SigningResponse), + RingVrfAliasRequest(RingVrfAliasRequest), + RingVrfAliasResponse(RingVrfAliasResponse), + ResourceAllocationRequest(ResourceAllocationRequest), + ResourceAllocationResponse(ResourceAllocationResponse), + CreateTransactionRequest(CreateTransactionRequest), + CreateTransactionResponse(CreateTransactionResponse), + CreateTransactionLegacyRequest(CreateTransactionLegacyRequest), + SignRawLegacyRequest(SignRawLegacyRequest), + SignRawLegacyResponse(SignRawLegacyResponse), +} + +/// Signing request flavor sent to the wallet. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum SigningRequest { + Payload(Box), + Raw(SigningRawRequest), +} + +/// Request sent when a product asks the paired wallet to sign a Substrate +/// payload with a product-derived account. +/// +/// Built from [`HostSignPayloadRequest`] and wrapped in +/// [`RemoteMessageV1::SignRequest`] before being encrypted into an SSO session +/// statement. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct SigningPayloadRequest { + pub product_account_id: ProductAccountId, + pub block_hash: Vec, + pub block_number: Vec, + pub era: Vec, + pub genesis_hash: Vec, + pub method: Vec, + pub nonce: Vec, + pub spec_version: Vec, + pub tip: Vec, + pub transaction_version: Vec, + pub signed_extensions: Vec, + pub version: u32, + pub asset_id: Option>, + pub metadata_hash: Option>, + pub mode: Option, + pub with_signed_transaction: OptionBool, +} + +fn signing_payload_request_from(value: HostSignPayloadRequest) -> SigningPayloadRequest { + let payload = value.payload; + SigningPayloadRequest { + product_account_id: value.account, + block_hash: payload.block_hash, + block_number: payload.block_number, + era: payload.era, + genesis_hash: payload.genesis_hash, + method: payload.method, + nonce: payload.nonce, + spec_version: payload.spec_version, + tip: payload.tip, + transaction_version: payload.transaction_version, + signed_extensions: payload.signed_extensions, + version: payload.version, + asset_id: payload.asset_id, + metadata_hash: payload.metadata_hash, + mode: payload.mode, + with_signed_transaction: OptionBool(payload.with_signed_transaction), + } +} + +/// Request sent when a product asks the paired wallet to sign raw bytes or a +/// string message with a product-derived account. +/// +/// Built from [`HostSignRawRequest`] and wrapped in +/// [`RemoteMessageV1::SignRequest`] before being encrypted into an SSO session +/// statement. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct SigningRawRequest { + pub product_account_id: ProductAccountId, + pub data: SigningRawPayload, +} + +fn signing_raw_request_from(value: HostSignRawRequest) -> SigningRawRequest { + SigningRawRequest { + product_account_id: value.account, + data: value.payload.into(), + } +} + +/// Request sent when a product asks the paired wallet to sign raw data with a +/// user-imported legacy account. +/// +/// Unlike product-account signing, the signer is the raw account id selected +/// from the user's legacy accounts. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct SignRawLegacyRequest { + pub account: AccountId, + pub data: SigningRawPayload, +} + +/// Raw data accepted by SSO signing requests. +/// +/// Used by both product-account raw signing and legacy-account raw signing to +/// distinguish binary payloads from string messages on the session-channel +/// wire. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum SigningRawPayload { + Bytes(Vec), + Payload(String), +} + +impl From for SigningRawPayload { + fn from(value: RawPayload) -> Self { + match value { + RawPayload::Bytes { bytes } => Self::Bytes(bytes), + RawPayload::Payload { payload } => Self::Payload(payload), + } + } +} + +/// Response returned by the wallet for a product-account signing request. +/// +/// Decoded from [`RemoteMessageV1::SignResponse`] while the runtime is waiting +/// for a matching SSO remote message id. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct SigningResponse { + pub responding_to: String, + pub payload: Result, +} + +/// Successful product-account signing result returned by the wallet. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct SigningPayloadResponseData { + pub signature: Vec, + pub signed_transaction: Option>, +} + +/// Response returned by the wallet for a legacy-account raw signing request. +/// +/// Decoded from [`RemoteMessageV1::SignRawLegacyResponse`] and mapped back to +/// the public raw-signing response shape. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct SignRawLegacyResponse { + pub responding_to: String, + pub signature: Result, String>, +} + +/// Request sent when a product asks the wallet for a ring-VRF alias. +/// +/// Used by `Account::get_account_alias`; the product account identifies the +/// alias target, while `product_id` identifies the caller that the wallet is +/// authorizing over the SSO channel. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct RingVrfAliasRequest { + pub product_account_id: ProductAccountId, + pub product_id: String, +} + +/// Response returned by the wallet for a ring-VRF alias request. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct RingVrfAliasResponse { + pub responding_to: String, + pub payload: Result, +} + +/// Request sent when a product asks the wallet to allocate SSO-backed +/// resources. +/// +/// Used by `ResourceAllocation::request` for capabilities such as statement +/// store allowance and auto-signing material. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct ResourceAllocationRequest { + pub calling_product_id: String, + pub resources: Vec, + pub on_existing: OnExistingAllowancePolicy, +} + +/// Resources the wallet may allocate for the calling product. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum SsoAllocatableResource { + StatementStoreAllowance, + BulletInAllowance, + SmartContractAllowance(u32), + AutoSigning, +} + +impl From for SsoAllocatableResource { + fn from(value: AllocatableResource) -> Self { + match value { + AllocatableResource::StatementStoreAllowance => Self::StatementStoreAllowance, + AllocatableResource::BulletinAllowance => Self::BulletInAllowance, + AllocatableResource::SmartContractAllowance(index) => { + Self::SmartContractAllowance(index) + } + AllocatableResource::AutoSigning => Self::AutoSigning, + } + } +} + +/// Wallet policy for already-existing resource allowance. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +pub enum OnExistingAllowancePolicy { + Ignore, + Increase, +} + +/// Response returned by the wallet for a resource-allocation request. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct ResourceAllocationResponse { + pub responding_to: String, + pub payload: Result, String>, +} + +/// Per-resource allocation result from the wallet. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum SsoAllocationOutcome { + Allocated(SsoAllocatedResource), + Rejected, + NotAvailable, +} + +/// Resource material allocated by the wallet. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum SsoAllocatedResource { + StatementStoreAllowance { + slot_account_key: Vec, + }, + BulletInAllowance { + slot_account_key: Vec, + }, + SmartContractAllowance, + AutoSigning { + product_derivation_secret: String, + product_root_private_key: Vec, + }, +} + +/// Request sent when a product asks the wallet to create a signed transaction +/// for a product-derived account. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct CreateTransactionRequest { + pub payload: CreateTransactionPayload, +} + +/// Versioned transaction-creation payload. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum CreateTransactionPayload { + V1(ProductAccountTxPayload), +} + +/// Request sent when a product asks the wallet to create a signed transaction +/// for a user-imported legacy account. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct CreateTransactionLegacyRequest { + pub payload: CreateTransactionLegacyPayload, +} + +/// Versioned legacy transaction-creation payload. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum CreateTransactionLegacyPayload { + V1(LegacyAccountTxPayload), +} + +/// Response returned by the wallet for either product-account or legacy-account +/// transaction creation. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct CreateTransactionResponse { + pub responding_to: String, + pub signed_transaction: Result, String>, +} + +/// Decoded inbound statement-channel outcome. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SsoSessionStatement { + RequestAccepted, + RemoteResponse(SsoRemoteResponse), + Disconnected, +} + +/// Wallet response variants that can satisfy a pending remote request. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SsoRemoteResponse { + Sign(SigningResponse), + SignRawLegacy(SignRawLegacyResponse), + RingVrfAlias(RingVrfAliasResponse), + ResourceAllocation(ResourceAllocationResponse), + CreateTransaction(CreateTransactionResponse), +} + +/// Decode and classify an inbound encrypted SSO session statement. +pub fn decode_sso_session_statement( + session: &SsoSessionInfo, + statement: &[u8], + expected_statement_request_id: &str, + expected_remote_message_id: &str, +) -> Result, String> { + let verified = + decode_verified_statement_data(statement, None).map_err(|err| err.to_string())?; + // Freshness gate against replay: a statement whose expiry is in the past + // is ignored. Trusts the local clock. + if verified + .expiry + .is_some_and(|expiry| statement_expiry_elapsed(expiry, current_unix_secs())) + { + return Ok(None); + } + let encrypted = verified.data; + let data = decrypt_session_statement_data(session, &encrypted)?; + if verified.signer == session.ss_public_key { + return match data { + SsoStatementData::Response { + request_id, + response_code, + } if request_id == expected_statement_request_id => { + classify_response_ack(request_id, response_code).map(Some) + } + _ => Ok(None), + }; + } + if verified.signer != session.identity_account_id { + return Err("statement proof signer does not match expected peer".to_string()); + } + match data { + SsoStatementData::Response { + request_id, + response_code, + } if request_id == expected_statement_request_id => { + classify_response_ack(request_id, response_code).map(Some) + } + SsoStatementData::Response { .. } => Ok(None), + SsoStatementData::Request { data, .. } => { + for message in data { + let message = RemoteMessage::decode(&mut message.as_slice()) + .map_err(|err| format!("invalid SSO remote message: {err}"))?; + if matches!( + &message.data, + RemoteMessageData::V1(RemoteMessageV1::Disconnected) + ) { + return Ok(Some(SsoSessionStatement::Disconnected)); + } + if let Some(response) = + remote_response_for_message(message, expected_remote_message_id) + { + return Ok(Some(SsoSessionStatement::RemoteResponse(response))); + } + } + Ok(None) + } + } +} + +fn classify_response_ack( + request_id: String, + response_code: u8, +) -> Result { + if response_code == SSO_RESPONSE_CODE_SUCCESS { + Ok(SsoSessionStatement::RequestAccepted) + } else { + Err(format!( + "SSO request {request_id} was rejected: {}", + sso_response_code_name(response_code) + )) + } +} + +fn remote_response_for_message( + message: RemoteMessage, + expected_remote_message_id: &str, +) -> Option { + let RemoteMessageData::V1(data) = message.data; + match data { + RemoteMessageV1::SignResponse(response) + if response.responding_to == expected_remote_message_id => + { + Some(SsoRemoteResponse::Sign(response)) + } + RemoteMessageV1::RingVrfAliasResponse(response) + if response.responding_to == expected_remote_message_id => + { + Some(SsoRemoteResponse::RingVrfAlias(response)) + } + RemoteMessageV1::SignRawLegacyResponse(response) + if response.responding_to == expected_remote_message_id => + { + Some(SsoRemoteResponse::SignRawLegacy(response)) + } + RemoteMessageV1::ResourceAllocationResponse(response) + if response.responding_to == expected_remote_message_id => + { + Some(SsoRemoteResponse::ResourceAllocation(response)) + } + RemoteMessageV1::CreateTransactionResponse(response) + if response.responding_to == expected_remote_message_id => + { + Some(SsoRemoteResponse::CreateTransaction(response)) + } + _ => None, + } +} + +fn sso_response_code_name(code: u8) -> &'static str { + match code { + 1 => "decryptionFailed", + 2 => "decodingFailed", + _ => "unknown", + } +} + +/// Build a wallet payload-signing request message. +pub fn sign_payload_message(message_id: String, request: HostSignPayloadRequest) -> RemoteMessage { + RemoteMessage { + message_id, + data: RemoteMessageData::V1(RemoteMessageV1::SignRequest(Box::new( + SigningRequest::Payload(Box::new(signing_payload_request_from(request))), + ))), + } +} + +/// Build a wallet raw-signing request message. +pub fn sign_raw_message(message_id: String, request: HostSignRawRequest) -> RemoteMessage { + RemoteMessage { + message_id, + data: RemoteMessageData::V1(RemoteMessageV1::SignRequest(Box::new(SigningRequest::Raw( + signing_raw_request_from(request), + )))), + } +} + +/// Build a wallet legacy raw-signing request message. +pub fn sign_raw_legacy_message( + message_id: String, + account: AccountId, + payload: RawPayload, +) -> RemoteMessage { + RemoteMessage { + message_id, + data: RemoteMessageData::V1(RemoteMessageV1::SignRawLegacyRequest( + SignRawLegacyRequest { + account, + data: payload.into(), + }, + )), + } +} + +/// Build a wallet account-alias request message. +pub fn alias_request_message( + message_id: String, + product_account_id: ProductAccountId, + product_id: String, +) -> RemoteMessage { + RemoteMessage { + message_id, + data: RemoteMessageData::V1(RemoteMessageV1::RingVrfAliasRequest(RingVrfAliasRequest { + product_account_id, + product_id, + })), + } +} + +/// Build a wallet resource-allocation request message. +pub fn resource_allocation_message( + message_id: String, + calling_product_id: String, + resources: Vec, + on_existing: OnExistingAllowancePolicy, +) -> RemoteMessage { + RemoteMessage { + message_id, + data: RemoteMessageData::V1(RemoteMessageV1::ResourceAllocationRequest( + ResourceAllocationRequest { + calling_product_id, + resources: resources.into_iter().map(Into::into).collect(), + on_existing, + }, + )), + } +} + +/// Build a wallet transaction-creation request message. +pub fn create_transaction_message( + message_id: String, + payload: ProductAccountTxPayload, +) -> RemoteMessage { + RemoteMessage { + message_id, + data: RemoteMessageData::V1(RemoteMessageV1::CreateTransactionRequest( + CreateTransactionRequest { + payload: CreateTransactionPayload::V1(payload), + }, + )), + } +} + +/// Build a wallet legacy-account transaction-creation request message. +pub fn create_transaction_legacy_message( + message_id: String, + payload: LegacyAccountTxPayload, +) -> RemoteMessage { + RemoteMessage { + message_id, + data: RemoteMessageData::V1(RemoteMessageV1::CreateTransactionLegacyRequest( + CreateTransactionLegacyRequest { + payload: CreateTransactionLegacyPayload::V1(payload), + }, + )), + } +} + +/// Build a signed outbound SSO request statement with a random nonce. +pub fn build_outgoing_request_statement( + session: &SsoSessionInfo, + statement_request_id: String, + messages: Vec, + expiry: u64, +) -> Result, String> { + let encrypted = encrypt_outgoing_request_data(session, statement_request_id, messages)?; + build_signed_session_request_statement(session, encrypted, expiry) +} + +/// Build a signed outbound SSO request statement with a caller-supplied nonce. +pub fn build_outgoing_request_statement_with_nonce( + session: &SsoSessionInfo, + statement_request_id: String, + messages: Vec, + expiry: u64, + nonce: [u8; AES_GCM_NONCE_LEN], +) -> Result, String> { + let encrypted = + encrypt_outgoing_request_data_with_nonce(session, statement_request_id, messages, nonce)?; + build_signed_session_request_statement(session, encrypted, expiry) +} + +fn encrypt_outgoing_request_data( + session: &SsoSessionInfo, + statement_request_id: String, + messages: Vec, +) -> Result, String> { + encrypt_session_statement_data( + session, + &outgoing_request_data(statement_request_id, messages), + ) +} + +fn encrypt_outgoing_request_data_with_nonce( + session: &SsoSessionInfo, + statement_request_id: String, + messages: Vec, + nonce: [u8; AES_GCM_NONCE_LEN], +) -> Result, String> { + encrypt_session_statement_data_with_nonce( + session, + &outgoing_request_data(statement_request_id, messages), + nonce, + ) +} + +fn outgoing_request_data( + statement_request_id: String, + messages: Vec, +) -> SsoStatementData { + SsoStatementData::Request { + request_id: statement_request_id, + data: messages + .into_iter() + .map(|message| message.encode()) + .collect(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::host_logic::sso::pairing::decrypt_session_statement_data; + use crate::host_logic::statement_store::{ + StatementField, build_signed_statement, decode_statement_data, + }; + use p256::SecretKey as P256SecretKey; + use p256::elliptic_curve::sec1::ToEncodedPoint; + use schnorrkel::{ExpansionMode, MiniSecretKey}; + use truapi::latest::HostSignPayloadData; + use truapi::v01; + + fn account() -> ProductAccountId { + ProductAccountId { + dot_ns_identifier: "myapp.dot".to_string(), + derivation_index: 7, + } + } + + fn fresh_expiry() -> u64 { + (current_unix_secs() + 60) << 32 + } + + fn elapsed_expiry() -> u64 { + (current_unix_secs() - 60) << 32 + } + + fn session() -> SsoSessionInfo { + let mini_secret = MiniSecretKey::from_bytes(&[7; 32]).unwrap(); + let keypair = mini_secret.expand_to_keypair(ExpansionMode::Ed25519); + let core_secret = P256SecretKey::from_slice(&[1; 32]).unwrap(); + let peer_secret = P256SecretKey::from_slice(&[2; 32]).unwrap(); + SsoSessionInfo { + ss_secret: keypair.secret.to_bytes(), + ss_public_key: keypair.public.to_bytes(), + enc_secret: core_secret.to_bytes().into(), + peer_enc_pubkey: peer_secret + .public_key() + .to_encoded_point(false) + .as_bytes() + .try_into() + .unwrap(), + identity_account_id: [3; 32], + session_id_own: [4; 32], + session_id_peer: [5; 32], + request_channel: [6; 32], + response_channel: [7; 32], + peer_request_channel: [8; 32], + } + } + + #[test] + fn disconnected_message_matches_host_papp_variant_order() { + let message = RemoteMessage { + message_id: String::new(), + data: RemoteMessageData::V1(RemoteMessageV1::Disconnected), + }; + + assert_eq!(message.encode(), vec![0, 0, 0]); + } + + #[test] + fn raw_sign_request_uses_remote_message_variant_indices() { + let message = sign_raw_message( + "m1".to_string(), + HostSignRawRequest { + account: account(), + payload: RawPayload::Bytes { + bytes: vec![0xde, 0xad], + }, + }, + ); + let encoded = message.encode(); + + assert_eq!(&encoded[..3], &[8, b'm', b'1']); + assert_eq!(encoded[3], 0); + assert_eq!(encoded[4], 1); + assert_eq!(encoded[5], 1); + } + + #[test] + fn late_remote_message_variants_match_host_papp_order() { + let legacy_tx = create_transaction_legacy_message( + String::new(), + v01::LegacyAccountTxPayload { + signer: [1; 32], + genesis_hash: [2; 32], + call_data: Vec::new(), + extensions: Vec::new(), + tx_ext_version: 0, + }, + ) + .encode(); + let legacy_raw = + sign_raw_legacy_message(String::new(), [1; 32], RawPayload::Bytes { bytes: vec![] }) + .encode(); + + assert_eq!(legacy_tx[..3], [0, 0, 9]); + assert_eq!(legacy_raw[..3], [0, 0, 10]); + } + + fn sequential_bytes(start: u8) -> [u8; N] { + std::array::from_fn(|index| start.wrapping_add(index as u8)) + } + + fn assert_host_papp_0_8_8_fixture(message: RemoteMessage, expected: &str) { + assert_eq!( + hex::encode(message.encode()), + expected.trim_start_matches("0x") + ); + } + + #[test] + fn resource_allocation_message_matches_host_papp_0_8_8_fixture() { + let message = resource_allocation_message( + "m-resource".to_string(), + "truapi-playground.dot".to_string(), + vec![ + AllocatableResource::StatementStoreAllowance, + AllocatableResource::BulletinAllowance, + AllocatableResource::SmartContractAllowance(9), + AllocatableResource::AutoSigning, + ], + OnExistingAllowancePolicy::Increase, + ); + + assert_host_papp_0_8_8_fixture( + message, + "0x286d2d7265736f757263650005547472756170692d706c617967726f756e642e646f7410000102090000000301", + ); + } + + #[test] + fn create_transaction_message_matches_host_papp_0_8_8_fixture() { + let message = create_transaction_message( + "m-product-tx".to_string(), + v01::ProductAccountTxPayload { + signer: v01::ProductAccountId { + dot_ns_identifier: "truapi-playground.dot".to_string(), + derivation_index: 0, + }, + genesis_hash: sequential_bytes(32), + call_data: vec![0, 0], + extensions: vec![v01::TxPayloadExtension { + id: "CheckNonce".to_string(), + extra: vec![1], + additional_signed: vec![2, 3], + }], + tx_ext_version: 0, + }, + ); + + assert_host_papp_0_8_8_fixture( + message, + "0x306d2d70726f647563742d7478000700547472756170692d706c617967726f756e642e646f7400000000202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f0800000428436865636b4e6f6e6365040108020300", + ); + } + + #[test] + fn playground_create_transaction_message_matches_host_papp_0_8_8_fixture() { + let message = create_transaction_message( + "create-transaction-1".to_string(), + v01::ProductAccountTxPayload { + signer: v01::ProductAccountId { + dot_ns_identifier: "truapi-playground.dot".to_string(), + derivation_index: 0, + }, + genesis_hash: [ + 0xbf, 0x04, 0x88, 0xdb, 0xe9, 0xda, 0xa1, 0xde, 0x1c, 0x08, 0xc5, 0xf7, 0x43, + 0xe2, 0x6f, 0xdc, 0x2a, 0x4e, 0xcd, 0x74, 0xcf, 0x87, 0xdd, 0x1b, 0x4b, 0x1e, + 0xeb, 0x99, 0xae, 0x4e, 0xf1, 0x9f, + ], + call_data: vec![0, 0], + extensions: vec![], + tx_ext_version: 0, + }, + ); + + assert_host_papp_0_8_8_fixture( + message, + "0x506372656174652d7472616e73616374696f6e2d31000700547472756170692d706c617967726f756e642e646f7400000000bf0488dbe9daa1de1c08c5f743e26fdc2a4ecd74cf87dd1b4b1eeb99ae4ef19f0800000000", + ); + } + + #[test] + fn create_transaction_legacy_message_matches_host_papp_0_8_8_fixture() { + let message = create_transaction_legacy_message( + "m-legacy-tx".to_string(), + v01::LegacyAccountTxPayload { + signer: sequential_bytes(0), + genesis_hash: sequential_bytes(32), + call_data: vec![0, 0], + extensions: vec![v01::TxPayloadExtension { + id: "CheckNonce".to_string(), + extra: vec![1], + additional_signed: vec![2, 3], + }], + tx_ext_version: 0, + }, + ); + + assert_host_papp_0_8_8_fixture( + message, + "0x2c6d2d6c65676163792d7478000900000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f0800000428436865636b4e6f6e6365040108020300", + ); + } + + #[test] + fn sign_raw_legacy_messages_match_host_papp_0_8_8_fixtures() { + assert_host_papp_0_8_8_fixture( + sign_raw_legacy_message( + "m-legacy-raw".to_string(), + sequential_bytes(0), + RawPayload::Bytes { + bytes: b"Hi".to_vec(), + }, + ), + "0x306d2d6c65676163792d726177000a000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f00084869", + ); + assert_host_papp_0_8_8_fixture( + sign_raw_legacy_message( + "m-legacy-raw-payload".to_string(), + sequential_bytes(0), + RawPayload::Payload { + payload: "Hi".to_string(), + }, + ), + "0x506d2d6c65676163792d7261772d7061796c6f6164000a000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f01443c42797465733e48693c2f42797465733e", + ); + } + + #[test] + fn option_bool_matches_host_papp_option_bool_encoding() { + let mut request = HostSignPayloadRequest { + account: account(), + payload: HostSignPayloadData { + block_hash: vec![], + block_number: vec![], + era: vec![], + genesis_hash: vec![], + method: vec![], + nonce: vec![], + spec_version: vec![], + tip: vec![], + transaction_version: vec![], + signed_extensions: vec![], + version: 4, + asset_id: None, + metadata_hash: None, + mode: None, + with_signed_transaction: Some(true), + }, + }; + let true_encoded = signing_payload_request_from(request.clone()).encode(); + request.payload.with_signed_transaction = Some(false); + let false_encoded = signing_payload_request_from(request.clone()).encode(); + request.payload.with_signed_transaction = None; + let none_encoded = signing_payload_request_from(request).encode(); + + assert_eq!(true_encoded.last(), Some(&1)); + assert_eq!(false_encoded.last(), Some(&2)); + assert_eq!(none_encoded.last(), Some(&0)); + } + + #[test] + fn maps_public_resource_names_to_sso_dialect() { + let message = resource_allocation_message( + "alloc".to_string(), + "myapp.dot".to_string(), + vec![ + AllocatableResource::StatementStoreAllowance, + AllocatableResource::BulletinAllowance, + AllocatableResource::SmartContractAllowance(9), + AllocatableResource::AutoSigning, + ], + OnExistingAllowancePolicy::Increase, + ); + let RemoteMessageData::V1(RemoteMessageV1::ResourceAllocationRequest(request)) = + message.data + else { + panic!("expected resource allocation request"); + }; + + assert_eq!( + request.resources, + vec![ + SsoAllocatableResource::StatementStoreAllowance, + SsoAllocatableResource::BulletInAllowance, + SsoAllocatableResource::SmartContractAllowance(9), + SsoAllocatableResource::AutoSigning, + ] + ); + assert_eq!(request.on_existing, OnExistingAllowancePolicy::Increase); + } + + #[test] + fn builds_signed_encrypted_outgoing_request_statement() { + let session = session(); + let remote_message = sign_raw_message( + "remote-1".to_string(), + HostSignRawRequest { + account: account(), + payload: RawPayload::Payload { + payload: "hello".to_string(), + }, + }, + ); + + let statement = build_outgoing_request_statement_with_nonce( + &session, + "statement-1".to_string(), + vec![remote_message.clone()], + 99, + [9; AES_GCM_NONCE_LEN], + ) + .unwrap(); + let encrypted = decode_statement_data(&statement).unwrap(); + let decrypted = decrypt_session_statement_data(&session, &encrypted).unwrap(); + + let SsoStatementData::Request { request_id, data } = decrypted else { + panic!("expected request statement data"); + }; + assert_eq!(request_id, "statement-1"); + assert_eq!(data.len(), 1); + assert_eq!( + RemoteMessage::decode(&mut data[0].as_slice()).unwrap(), + remote_message + ); + + let fields = Vec::::decode(&mut statement.as_slice()).unwrap(); + assert_eq!(fields[1], StatementField::Expiry(99)); + assert_eq!(fields[2], StatementField::Channel(session.request_channel)); + assert_eq!(fields[3], StatementField::Topic1(session.session_id_own)); + } + + #[test] + fn ignores_own_echoed_session_request_statement() { + let session = session(); + let remote_message = sign_raw_message( + "remote-1".to_string(), + HostSignRawRequest { + account: account(), + payload: RawPayload::Payload { + payload: "hello".to_string(), + }, + }, + ); + let statement = build_outgoing_request_statement_with_nonce( + &session, + "statement-1".to_string(), + vec![remote_message], + fresh_expiry(), + [9; AES_GCM_NONCE_LEN], + ) + .unwrap(); + + let decoded = + decode_sso_session_statement(&session, &statement, "statement-1", "remote-1").unwrap(); + + assert_eq!(decoded, None); + } + + fn response_ack_statement(session: &SsoSessionInfo, expiry: u64) -> Vec { + let encrypted = encrypt_session_statement_data_with_nonce( + session, + &SsoStatementData::Response { + request_id: "statement-1".to_string(), + response_code: SSO_RESPONSE_CODE_SUCCESS, + }, + [9; AES_GCM_NONCE_LEN], + ) + .unwrap(); + build_signed_statement( + session, + session.response_channel, + session.session_id_own, + encrypted, + expiry, + ) + .unwrap() + } + + #[test] + fn accepts_own_echoed_session_response_ack() { + let session = session(); + let statement = response_ack_statement(&session, fresh_expiry()); + + let decoded = + decode_sso_session_statement(&session, &statement, "statement-1", "remote-1").unwrap(); + + assert_eq!(decoded, Some(SsoSessionStatement::RequestAccepted)); + } + + /// A statement whose expiry is in the past must be ignored even when it + /// would otherwise match the pending request (replay protection). + #[test] + fn ignores_expired_session_response_ack() { + let session = session(); + let statement = response_ack_statement(&session, elapsed_expiry()); + + let decoded = + decode_sso_session_statement(&session, &statement, "statement-1", "remote-1").unwrap(); + + assert_eq!(decoded, None); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/sso/pairing.rs b/rust/crates/truapi-server/src/host_logic/sso/pairing.rs new file mode 100644 index 00000000..73e7d402 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/sso/pairing.rs @@ -0,0 +1,760 @@ +//! SSO pairing bootstrap helpers. +//! +//! This module owns the byte shape of the QR/deeplink payload described in +//! `docs/design/host-contract-and-core-impl/H - sso-pairing-protocol.md`. +//! The SCALE handshake codecs are kept wire-compatible with host-papp's v2 +//! handshake codec: +//! + +use aes_gcm::aead::{Aead, KeyInit}; +use aes_gcm::{Aes256Gcm, Nonce}; +use blake2_rfc::blake2b::blake2b; +use hkdf::Hkdf; +use p256::ecdh::diffie_hellman; +use p256::elliptic_curve::sec1::ToEncodedPoint; +use p256::{PublicKey, SecretKey}; +use parity_scale_codec::{Decode, Encode}; +use schnorrkel::{ExpansionMode, MiniSecretKey}; +use sha2::Sha256; +use thiserror::Error; +use truapi_platform::RuntimeConfig; +#[cfg(test)] +use truapi_platform::{HostInfo, PlatformInfo}; + +use crate::host_logic::session::SsoSessionInfo; + +const HANDSHAKE_TOPIC_SUFFIX: &[u8] = b"topic"; +const MAX_P256_SECRET_ATTEMPTS: usize = 64; +/// Byte length of the AES-GCM nonce prepended to encrypted SSO payloads. +pub const AES_GCM_NONCE_LEN: usize = 12; +const SESSION_PREFIX: &[u8] = b"session"; +const PIN_SEPARATOR: &[u8] = b"/"; +const REQUEST_CHANNEL_SUFFIX: &[u8] = b"request"; +const RESPONSE_CHANNEL_SUFFIX: &[u8] = b"response"; + +/// QR/deeplink bootstrap material generated by the host for one pairing flow. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PairingBootstrap { + pub deeplink: String, + pub topic: [u8; 32], + pub statement_store_public_key: [u8; 32], + pub statement_store_secret: [u8; 64], + pub encryption_public_key: [u8; 65], + pub encryption_secret_key: [u8; 32], +} + +/// Persistable device identity reused across pairing attempts. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct PairingDeviceIdentity { + pub statement_store_secret: [u8; 64], + pub statement_store_public_key: [u8; 32], + pub encryption_secret_key: [u8; 32], + pub encryption_public_key: [u8; 65], +} + +/// Errors that can occur while generating pairing bootstrap material. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum PairingBootstrapError { + #[error("failed to generate random pairing material: {0}")] + Random(String), + #[error("failed to generate P-256 pairing key")] + InvalidP256Secret, +} + +/// Versioned SCALE payload embedded in the pairing deeplink. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum VersionedHandshakeProposal { + #[codec(index = 1)] + V2(HandshakeProposalV2), +} + +/// Host-papp v2 handshake proposal sent by the host. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct HandshakeProposalV2 { + pub device: HandshakeDevice, + pub metadata: Vec, +} + +/// Device keys advertised in the v2 handshake proposal. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct HandshakeDevice { + pub statement_account_id: [u8; 32], + pub encryption_public_key: [u8; 65], +} + +/// Metadata key/value entry attached to a v2 handshake proposal. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct HandshakeMetadataEntry(pub HandshakeMetadataKey, pub String); + +/// Metadata keys understood by the mobile SSO pairing flow. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum HandshakeMetadataKey { + Custom(String), + HostName, + HostVersion, + HostIcon, + PlatformType, + PlatformVersion, +} + +/// Versioned encrypted response posted by the wallet to the pairing topic. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum VersionedHandshakeResponse { + #[codec(index = 1)] + V2 { + encrypted_message: Vec, + public_key: [u8; 65], + }, +} + +/// Plaintext v2 wallet response after decrypting the pairing statement. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum EncryptedHandshakeResponseV2 { + Pending(HandshakeStatusV2), + Success(Box), + Failed(String), +} + +/// Intermediate v2 handshake status emitted before success/failure. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum HandshakeStatusV2 { + AllowanceAllocation, +} + +/// Successful v2 handshake payload used to establish the SSO session. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct HandshakeSuccessV2 { + pub identity_account_id: [u8; 32], + pub root_account_id: [u8; 32], + pub identity_chat_private_key: [u8; 32], + pub sso_enc_pub_key: [u8; 65], + pub device_enc_pub_key: [u8; 65], + pub root_entropy_source: [u8; 32], +} + +/// Encrypted statement-channel envelope shared with the wallet. +/// +/// Mirrors `@novasamatech/statement-store` session statement data: +/// +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum SsoStatementData { + Request { + request_id: String, + data: Vec>, + }, + Response { + request_id: String, + response_code: u8, + }, +} + +/// Decode wallet-posted pairing handshake data from SCALE bytes. +pub fn decode_app_handshake_data(blob: &[u8]) -> Result { + let mut input = blob; + let value: VersionedHandshakeResponse = + Decode::decode(&mut input).map_err(|err| format!("invalid app handshake data: {err}"))?; + if !input.is_empty() { + return Err("invalid app handshake data: trailing bytes".to_string()); + } + Ok(value) +} + +/// Decrypt a v2 handshake response. +pub fn decrypt_v2_handshake_response( + core_encryption_secret_key: [u8; 32], + wallet_ephemeral_public_key: [u8; 65], + encrypted_message: &[u8], +) -> Result { + let plaintext = decrypt_p256_hkdf_aes_gcm( + core_encryption_secret_key, + wallet_ephemeral_public_key, + encrypted_message, + )?; + let mut input = plaintext.as_slice(); + let value = EncryptedHandshakeResponseV2::decode(&mut input) + .map_err(|err| format!("invalid SSO V2 handshake response: {err}"))?; + if !input.is_empty() { + return Err("invalid SSO V2 handshake response: trailing bytes".to_string()); + } + Ok(value) +} + +/// Derive the persistent SSO session channels from a successful handshake. +pub fn establish_sso_session_info( + bootstrap: &PairingBootstrap, + peer_statement_account_id: [u8; 32], + peer_sso_enc_pub_key: [u8; 65], +) -> Result { + let shared_secret = shared_secret(bootstrap.encryption_secret_key, peer_sso_enc_pub_key)?; + let shared_secret_bytes: [u8; 32] = (*shared_secret.raw_secret_bytes()).into(); + let session_id_own = create_session_id( + shared_secret_bytes, + bootstrap.statement_store_public_key, + peer_statement_account_id, + ); + let session_id_peer = create_session_id( + shared_secret_bytes, + peer_statement_account_id, + bootstrap.statement_store_public_key, + ); + + Ok(SsoSessionInfo { + ss_secret: bootstrap.statement_store_secret, + ss_public_key: bootstrap.statement_store_public_key, + enc_secret: bootstrap.encryption_secret_key, + peer_enc_pubkey: peer_sso_enc_pub_key, + identity_account_id: peer_statement_account_id, + session_id_own, + session_id_peer, + request_channel: keyed_hash(session_id_own, REQUEST_CHANNEL_SUFFIX), + response_channel: keyed_hash(session_id_own, RESPONSE_CHANNEL_SUFFIX), + peer_request_channel: keyed_hash(session_id_peer, REQUEST_CHANNEL_SUFFIX), + }) +} + +/// Encrypt session-channel statement data with a random nonce. +pub fn encrypt_session_statement_data( + session: &SsoSessionInfo, + data: &SsoStatementData, +) -> Result, String> { + let mut nonce = [0u8; AES_GCM_NONCE_LEN]; + getrandom::getrandom(&mut nonce) + .map_err(|err| format!("failed to generate AES-GCM nonce: {err}"))?; + encrypt_session_statement_data_with_nonce(session, data, nonce) +} + +/// Encrypt session-channel statement data with a caller-supplied nonce. +pub fn encrypt_session_statement_data_with_nonce( + session: &SsoSessionInfo, + data: &SsoStatementData, + nonce: [u8; AES_GCM_NONCE_LEN], +) -> Result, String> { + let aes_key = session_aes_key(session)?; + let cipher = Aes256Gcm::new_from_slice(&aes_key) + .map_err(|err| format!("failed to initialize AES-GCM: {err}"))?; + let mut encrypted = nonce.to_vec(); + encrypted.extend( + cipher + .encrypt(Nonce::from_slice(&nonce), data.encode().as_slice()) + .map_err(|err| format!("failed to encrypt SSO statement data: {err}"))?, + ); + Ok(encrypted) +} + +/// Decrypt session-channel statement data. +pub fn decrypt_session_statement_data( + session: &SsoSessionInfo, + encrypted_message: &[u8], +) -> Result { + let plaintext = decrypt_session_message(session, encrypted_message)?; + let mut input = plaintext.as_slice(); + let data = SsoStatementData::decode(&mut input) + .map_err(|err| format!("invalid SSO statement data: {err}"))?; + if !input.is_empty() { + return Err("invalid SSO statement data: trailing bytes".to_string()); + } + Ok(data) +} + +fn decrypt_p256_hkdf_aes_gcm( + own_secret_key: [u8; 32], + peer_public_key: [u8; 65], + encrypted_message: &[u8], +) -> Result, String> { + if encrypted_message.len() < AES_GCM_NONCE_LEN { + return Err("encrypted SSO handshake answer is too short".to_string()); + } + let shared_secret = shared_secret(own_secret_key, peer_public_key)?; + let aes_key = aes_key_from_shared_secret(&shared_secret)?; + + decrypt_aes_gcm_with_key(aes_key, encrypted_message, "handshake answer") +} + +fn decrypt_session_message( + session: &SsoSessionInfo, + encrypted_message: &[u8], +) -> Result, String> { + decrypt_aes_gcm_with_key( + session_aes_key(session)?, + encrypted_message, + "statement data", + ) +} + +fn decrypt_aes_gcm_with_key( + aes_key: [u8; 32], + encrypted_message: &[u8], + label: &str, +) -> Result, String> { + if encrypted_message.len() < AES_GCM_NONCE_LEN { + return Err(format!("encrypted SSO {label} is too short")); + } + let (nonce, ciphertext) = encrypted_message.split_at(AES_GCM_NONCE_LEN); + let cipher = Aes256Gcm::new_from_slice(&aes_key) + .map_err(|err| format!("failed to initialize AES-GCM: {err}"))?; + cipher + .decrypt(Nonce::from_slice(nonce), ciphertext) + .map_err(|err| format!("failed to decrypt SSO {label}: {err}")) +} + +fn session_aes_key(session: &SsoSessionInfo) -> Result<[u8; 32], String> { + let shared_secret = shared_secret(session.enc_secret, session.peer_enc_pubkey)?; + aes_key_from_shared_secret(&shared_secret) +} + +fn aes_key_from_shared_secret( + shared_secret: &p256::ecdh::SharedSecret, +) -> Result<[u8; 32], String> { + let hkdf = Hkdf::::new(None, shared_secret.raw_secret_bytes()); + let mut aes_key = [0u8; 32]; + hkdf.expand(&[], &mut aes_key) + .map_err(|err| format!("failed to derive AES key: {err}"))?; + Ok(aes_key) +} + +fn shared_secret( + own_secret_key: [u8; 32], + peer_public_key: [u8; 65], +) -> Result { + let secret = SecretKey::from_slice(&own_secret_key) + .map_err(|err| format!("invalid P-256 secret key: {err}"))?; + let peer_public = PublicKey::from_sec1_bytes(&peer_public_key) + .map_err(|err| format!("invalid P-256 public key: {err}"))?; + Ok(diffie_hellman( + secret.to_nonzero_scalar(), + peer_public.as_affine(), + )) +} + +fn create_session_id( + shared_secret: [u8; 32], + account_a: [u8; 32], + account_b: [u8; 32], +) -> [u8; 32] { + let mut message = Vec::with_capacity(SESSION_PREFIX.len() + 32 + 32 + 2); + message.extend_from_slice(SESSION_PREFIX); + message.extend_from_slice(&account_a); + message.extend_from_slice(&account_b); + message.extend_from_slice(PIN_SEPARATOR); + message.extend_from_slice(PIN_SEPARATOR); + keyed_hash(shared_secret, &message) +} + +fn keyed_hash(key: [u8; 32], message: &[u8]) -> [u8; 32] { + let digest = blake2b(32, &key, message); + let mut output = [0u8; 32]; + output.copy_from_slice(digest.as_bytes()); + output +} + +/// Create one-shot pairing bootstrap material from runtime config. +pub fn create_pairing_bootstrap( + config: &RuntimeConfig, +) -> Result { + create_pairing_bootstrap_from_identity(config, generate_pairing_device_identity()?) +} + +/// Generate a fresh persistable pairing device identity. +pub fn generate_pairing_device_identity() -> Result { + let (statement_store_secret, statement_store_public_key) = generate_statement_store_keypair()?; + let (encryption_secret_key, encryption_public_key) = generate_p256_keypair()?; + + Ok(PairingDeviceIdentity { + statement_store_secret, + statement_store_public_key, + encryption_secret_key, + encryption_public_key, + }) +} + +/// Create pairing bootstrap material from an existing device identity. +pub fn create_pairing_bootstrap_from_identity( + config: &RuntimeConfig, + identity: PairingDeviceIdentity, +) -> Result { + let deeplink = build_pairing_deeplink( + &config.pairing_deeplink_scheme, + identity.statement_store_public_key, + identity.encryption_public_key, + config, + ); + let topic = bootstrap_topic( + identity.statement_store_public_key, + identity.encryption_public_key, + ); + + Ok(PairingBootstrap { + deeplink, + topic, + statement_store_public_key: identity.statement_store_public_key, + statement_store_secret: identity.statement_store_secret, + encryption_public_key: identity.encryption_public_key, + encryption_secret_key: identity.encryption_secret_key, + }) +} + +/// Build the wallet deeplink that carries the v2 handshake proposal. +pub fn build_pairing_deeplink( + scheme: &str, + statement_store_public_key: [u8; 32], + encryption_public_key: [u8; 65], + config: &RuntimeConfig, +) -> String { + let handshake = VersionedHandshakeProposal::V2(HandshakeProposalV2 { + device: HandshakeDevice { + statement_account_id: statement_store_public_key, + encryption_public_key, + }, + metadata: handshake_metadata(config), + }); + format!( + "{scheme}://pair?handshake={}", + hex::encode(handshake.encode()) + ) +} + +fn handshake_metadata(config: &RuntimeConfig) -> Vec { + let mut entries = vec![HandshakeMetadataEntry( + HandshakeMetadataKey::HostName, + config.host_info.name.clone(), + )]; + if let Some(value) = &config.host_info.version { + entries.push(HandshakeMetadataEntry( + HandshakeMetadataKey::HostVersion, + value.clone(), + )); + } + if let Some(value) = &config.host_info.icon { + entries.push(HandshakeMetadataEntry( + HandshakeMetadataKey::HostIcon, + value.clone(), + )); + } + if let Some(value) = &config.platform_info.kind { + entries.push(HandshakeMetadataEntry( + HandshakeMetadataKey::PlatformType, + value.clone(), + )); + } + if let Some(value) = &config.platform_info.version { + entries.push(HandshakeMetadataEntry( + HandshakeMetadataKey::PlatformVersion, + value.clone(), + )); + } + entries +} + +/// Derive the statement-store pairing topic from advertised host keys. +pub fn bootstrap_topic( + statement_store_public_key: [u8; 32], + encryption_public_key: [u8; 65], +) -> [u8; 32] { + let mut message = + Vec::with_capacity(encryption_public_key.len() + HANDSHAKE_TOPIC_SUFFIX.len()); + message.extend_from_slice(&encryption_public_key); + message.extend_from_slice(HANDSHAKE_TOPIC_SUFFIX); + + keyed_hash(statement_store_public_key, &message) +} + +fn generate_statement_store_keypair() -> Result<([u8; 64], [u8; 32]), PairingBootstrapError> { + let mut seed = [0u8; 32]; + getrandom::getrandom(&mut seed) + .map_err(|err| PairingBootstrapError::Random(err.to_string()))?; + let mini_secret = MiniSecretKey::from_bytes(&seed) + .map_err(|err| PairingBootstrapError::Random(err.to_string()))?; + let keypair = mini_secret.expand_to_keypair(ExpansionMode::Ed25519); + Ok((keypair.secret.to_bytes(), keypair.public.to_bytes())) +} + +fn generate_p256_keypair() -> Result<([u8; 32], [u8; 65]), PairingBootstrapError> { + for _ in 0..MAX_P256_SECRET_ATTEMPTS { + let mut candidate = [0u8; 32]; + getrandom::getrandom(&mut candidate) + .map_err(|err| PairingBootstrapError::Random(err.to_string()))?; + let Ok(secret) = SecretKey::from_slice(&candidate) else { + continue; + }; + let public = secret.public_key().to_encoded_point(false); + let public = public.as_bytes(); + if public.len() != 65 { + return Err(PairingBootstrapError::InvalidP256Secret); + } + let mut encryption_public_key = [0u8; 65]; + encryption_public_key.copy_from_slice(public); + let mut encryption_secret_key = [0u8; 32]; + encryption_secret_key.copy_from_slice(secret.to_bytes().as_slice()); + return Ok((encryption_secret_key, encryption_public_key)); + } + + Err(PairingBootstrapError::InvalidP256Secret) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SS_PUBLIC: [u8; 32] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]; + const ENC_PUBLIC: [u8; 65] = [ + 0x04, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, + 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + ]; + + fn runtime_config() -> RuntimeConfig { + RuntimeConfig::new( + "myapp.dot".to_string(), + HostInfo { + name: "Polkadot Web".to_string(), + icon: Some("https://example.invalid/dotli.png".to_string()), + version: Some("1.2.3".to_string()), + }, + PlatformInfo { + kind: Some("Firefox".to_string()), + version: Some("192.32".to_string()), + }, + [0; 32], + "polkadotapp".to_string(), + ) + .expect("test runtime config is valid") + } + + #[test] + fn builds_v2_pairing_deeplink() { + let config = runtime_config(); + let deeplink = build_pairing_deeplink("polkadotapp", SS_PUBLIC, ENC_PUBLIC, &config); + + assert!(deeplink.starts_with("polkadotapp://pair?handshake=01")); + let encoded = hex::decode(deeplink.split("handshake=").nth(1).unwrap()).unwrap(); + let decoded = ::decode(&mut &encoded[..]).unwrap(); + let VersionedHandshakeProposal::V2(proposal) = decoded; + assert_eq!(proposal.device.statement_account_id, SS_PUBLIC); + assert_eq!(proposal.device.encryption_public_key, ENC_PUBLIC); + assert!(proposal.metadata.contains(&HandshakeMetadataEntry( + HandshakeMetadataKey::HostName, + "Polkadot Web".to_string() + ))); + } + + #[test] + fn builds_dev_pairing_deeplink() { + let deeplink = + build_pairing_deeplink("polkadotappdev", SS_PUBLIC, ENC_PUBLIC, &runtime_config()); + + assert!(deeplink.starts_with("polkadotappdev://pair?handshake=")); + } + + #[test] + fn derives_bootstrap_topic_vector() { + assert_eq!( + hex::encode(bootstrap_topic(SS_PUBLIC, ENC_PUBLIC)), + "031c589833c39b1dfbe3c1304ced75fa7b0d841035db008e5b407bfadd2779a4" + ); + } + + #[test] + fn generated_bootstrap_uses_real_key_shapes() { + let config = runtime_config(); + + let bootstrap = create_pairing_bootstrap(&config).unwrap(); + + assert!( + bootstrap + .deeplink + .starts_with("polkadotapp://pair?handshake=") + ); + assert_eq!(bootstrap.encryption_public_key[0], 0x04); + assert_eq!( + bootstrap.topic, + bootstrap_topic( + bootstrap.statement_store_public_key, + bootstrap.encryption_public_key + ) + ); + } + + #[test] + fn decodes_app_handshake_response() { + let answer = VersionedHandshakeResponse::V2 { + encrypted_message: vec![0xde, 0xad], + public_key: ENC_PUBLIC, + }; + + assert_eq!(decode_app_handshake_data(&answer.encode()).unwrap(), answer); + } + + #[test] + fn rejects_app_handshake_trailing_bytes() { + let mut encoded = VersionedHandshakeResponse::V2 { + encrypted_message: vec![0xde, 0xad], + public_key: ENC_PUBLIC, + } + .encode(); + encoded.push(0); + + assert_eq!( + decode_app_handshake_data(&encoded).unwrap_err(), + "invalid app handshake data: trailing bytes" + ); + } + + #[test] + fn decrypts_v2_handshake_response() { + let core_secret = SecretKey::from_slice(&[1; 32]).unwrap(); + let wallet_ephemeral_secret = SecretKey::from_slice(&[2; 32]).unwrap(); + let wallet_ephemeral_public = wallet_ephemeral_secret.public_key().to_encoded_point(false); + let mut wallet_ephemeral_public_bytes = [0u8; 65]; + wallet_ephemeral_public_bytes.copy_from_slice(wallet_ephemeral_public.as_bytes()); + + let shared_secret = diffie_hellman( + wallet_ephemeral_secret.to_nonzero_scalar(), + core_secret.public_key().as_affine(), + ); + let hkdf = Hkdf::::new(None, shared_secret.raw_secret_bytes()); + let mut aes_key = [0u8; 32]; + hkdf.expand(&[], &mut aes_key).unwrap(); + + let sensitive = EncryptedHandshakeResponseV2::Success(Box::new(HandshakeSuccessV2 { + identity_account_id: [8; 32], + root_account_id: [7; 32], + identity_chat_private_key: [6; 32], + sso_enc_pub_key: ENC_PUBLIC, + device_enc_pub_key: ENC_PUBLIC, + root_entropy_source: [5; 32], + })); + let nonce = [9u8; AES_GCM_NONCE_LEN]; + let cipher = Aes256Gcm::new_from_slice(&aes_key).unwrap(); + let mut encrypted = nonce.to_vec(); + encrypted.extend( + cipher + .encrypt(Nonce::from_slice(&nonce), sensitive.encode().as_slice()) + .unwrap(), + ); + + assert_eq!( + decrypt_v2_handshake_response( + core_secret.to_bytes().into(), + wallet_ephemeral_public_bytes, + &encrypted + ) + .unwrap(), + sensitive + ); + } + + #[test] + fn rejects_short_handshake_ciphertext() { + assert_eq!( + decrypt_v2_handshake_response([1; 32], ENC_PUBLIC, &[0; AES_GCM_NONCE_LEN - 1]) + .unwrap_err(), + "encrypted SSO handshake answer is too short" + ); + } + + #[test] + fn establishes_session_ids_and_channels() { + let core_secret = SecretKey::from_slice(&[1; 32]).unwrap(); + let core_public = core_secret.public_key().to_encoded_point(false); + let mut core_public_bytes = [0u8; 65]; + core_public_bytes.copy_from_slice(core_public.as_bytes()); + let bootstrap = PairingBootstrap { + deeplink: "polkadotapp://pair?handshake=00".to_string(), + topic: [0x11; 32], + statement_store_public_key: [0x22; 32], + statement_store_secret: [0x33; 64], + encryption_public_key: core_public_bytes, + encryption_secret_key: [1; 32], + }; + let peer_secret = SecretKey::from_slice(&[2; 32]).unwrap(); + let peer_public = peer_secret.public_key().to_encoded_point(false); + let peer_public: [u8; 65] = peer_public.as_bytes().try_into().unwrap(); + + let info = establish_sso_session_info(&bootstrap, [0x55; 32], peer_public).unwrap(); + + assert_eq!(info.ss_secret, [0x33; 64]); + assert_eq!(info.ss_public_key, [0x22; 32]); + assert_eq!(info.enc_secret, [1; 32]); + assert_eq!(info.peer_enc_pubkey, peer_public); + assert_eq!(info.identity_account_id, [0x55; 32]); + assert_ne!(info.session_id_own, info.session_id_peer); + assert_eq!( + info.request_channel, + keyed_hash(info.session_id_own, b"request") + ); + assert_eq!( + info.response_channel, + keyed_hash(info.session_id_own, b"response") + ); + assert_eq!( + info.peer_request_channel, + keyed_hash(info.session_id_peer, b"request") + ); + } + + #[test] + fn statement_data_codec_round_trips_request_and_response() { + let request = SsoStatementData::Request { + request_id: "req-1".to_string(), + data: vec![vec![0xde, 0xad], vec![0xbe, 0xef]], + }; + let response = SsoStatementData::Response { + request_id: "req-1".to_string(), + response_code: 0, + }; + + assert_eq!( + SsoStatementData::decode(&mut &request.encode()[..]).unwrap(), + request + ); + assert_eq!( + SsoStatementData::decode(&mut &response.encode()[..]).unwrap(), + response + ); + assert_eq!(request.encode()[0], 0); + assert_eq!(response.encode()[0], 1); + } + + #[test] + fn encrypts_and_decrypts_session_statement_data() { + let core_secret = SecretKey::from_slice(&[1; 32]).unwrap(); + let core_public = core_secret.public_key().to_encoded_point(false); + let mut core_public_bytes = [0u8; 65]; + core_public_bytes.copy_from_slice(core_public.as_bytes()); + let bootstrap = PairingBootstrap { + deeplink: "polkadotapp://pair?handshake=00".to_string(), + topic: [0x11; 32], + statement_store_public_key: [0x22; 32], + statement_store_secret: [0x33; 64], + encryption_public_key: core_public_bytes, + encryption_secret_key: [1; 32], + }; + let peer_secret = SecretKey::from_slice(&[2; 32]).unwrap(); + let peer_public = peer_secret + .public_key() + .to_encoded_point(false) + .as_bytes() + .try_into() + .unwrap(); + let session = establish_sso_session_info(&bootstrap, [0x55; 32], peer_public).unwrap(); + let data = SsoStatementData::Request { + request_id: "req-1".to_string(), + data: vec![vec![0xde, 0xad]], + }; + let nonce = [9u8; AES_GCM_NONCE_LEN]; + + let encrypted = encrypt_session_statement_data_with_nonce(&session, &data, nonce).unwrap(); + + assert_eq!(&encrypted[..AES_GCM_NONCE_LEN], nonce); + assert_eq!( + decrypt_session_statement_data(&session, &encrypted).unwrap(), + data + ); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/statement_store.rs b/rust/crates/truapi-server/src/host_logic/statement_store.rs new file mode 100644 index 00000000..06980545 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/statement_store.rs @@ -0,0 +1,38 @@ +//! People-chain statement-store helpers. +//! +//! The core talks to the statement-store pallet through the host-provided +//! `ChainProvider` JSON-RPC connection. Transport mechanics live in +//! `HostRpcClient`; this module owns statement-store payload encoding, +//! proof verification, and subscription-result parsing. + +use thiserror::Error; + +mod rpc; +mod statement; + +pub use rpc::{ + MAX_MATCH_ALL_TOPICS, MAX_MATCH_ANY_TOPICS, NewStatements, SUBMIT_STATEMENT_METHOD, + SUBSCRIBE_STATEMENT_METHOD, TopicFilterKind, UNSUBSCRIBE_STATEMENT_METHOD, + parse_new_statements_result, +}; +pub(crate) use statement::current_unix_secs; +pub use statement::{ + StatementField, StatementProof, VerifiedStatementData, build_signed_session_request_statement, + build_signed_statement, decode_signed_statement, decode_statement_data, + decode_verified_statement_data, hex_topic, sign_statement_fields, signed_statement_to_scale, + statement_expiry_elapsed, statement_fields_from_v01, statement_proof_to_v01, + statement_signing_payload, +}; + +/// Error while parsing statement-store JSON-RPC or SCALE statement payloads. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum StatementStoreParseError { + #[error("invalid statement hex: {0}")] + InvalidStatementHex(String), + #[error("invalid statement scale: {0}")] + InvalidStatementScale(String), + #[error("malformed statement-store frame: {0}")] + Malformed(String), + #[error("invalid statement proof: {0}")] + InvalidStatementProof(String), +} diff --git a/rust/crates/truapi-server/src/host_logic/statement_store/rpc.rs b/rust/crates/truapi-server/src/host_logic/statement_store/rpc.rs new file mode 100644 index 00000000..bedbce5d --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/statement_store/rpc.rs @@ -0,0 +1,115 @@ +//! Statement-store JSON-RPC shapes mirrored from `sp_statement_store`. +//! +//! See the upstream RPC methods plus `TopicFilter` / `StatementEvent` types: +//! +//! +//! + +use serde_json::Value; + +use super::StatementStoreParseError; + +/// Statement-store RPC method used to open a topic subscription. +pub const SUBSCRIBE_STATEMENT_METHOD: &str = "statement_subscribeStatement"; +/// Statement-store RPC method used to close a topic subscription. +pub const UNSUBSCRIBE_STATEMENT_METHOD: &str = "statement_unsubscribeStatement"; +/// Statement-store RPC method used to submit a signed statement. +pub const SUBMIT_STATEMENT_METHOD: &str = "statement_submit"; +/// Maximum `matchAll` topic count accepted by the statement-store RPC. +pub const MAX_MATCH_ALL_TOPICS: usize = 4; +/// Maximum `matchAny` topic count accepted by the statement-store RPC. +pub const MAX_MATCH_ANY_TOPICS: usize = 128; + +/// Decoded `newStatements` subscription notification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NewStatements { + /// Remote subscription id included in the notification. + pub remote_subscription_id: String, + /// SCALE-encoded signed statements carried by the notification. + pub statements: Vec>, + /// Optional server-side backlog count. + pub remaining: Option, +} + +/// Topic filter flavor used by statement-store subscribe requests. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TopicFilterKind { + /// Require every listed topic to match. + MatchAll, + /// Accept any listed topic match. + MatchAny, +} + +/// Parse a statement-store subscription result value. +pub fn parse_new_statements_result( + remote_subscription_id: String, + result: &Value, +) -> Result { + if result.get("event").and_then(Value::as_str) != Some("newStatements") { + return Err(StatementStoreParseError::Malformed( + "result is not a newStatements event".to_string(), + )); + } + let data = result + .get("data") + .ok_or_else(|| StatementStoreParseError::Malformed("missing data".to_string()))?; + let statement_values = data + .get("statements") + .and_then(Value::as_array) + .ok_or_else(|| StatementStoreParseError::Malformed("missing statements".to_string()))?; + let statements = statement_values + .iter() + .map(|value| { + let Some(hex) = value.as_str() else { + return Err(StatementStoreParseError::Malformed( + "statement is not a hex string".to_string(), + )); + }; + decode_hex(hex) + }) + .collect::, _>>()?; + let remaining = data + .get("remaining") + .map(|value| { + value.as_u64().ok_or_else(|| { + StatementStoreParseError::Malformed("remaining is not an integer".to_string()) + }) + }) + .transpose()?; + + Ok(NewStatements { + remote_subscription_id, + statements, + remaining, + }) +} + +fn decode_hex(value: &str) -> Result, StatementStoreParseError> { + hex::decode(value.strip_prefix("0x").unwrap_or(value)) + .map_err(|error| StatementStoreParseError::InvalidStatementHex(error.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_dotli_sdk_new_statements_result() { + let result = serde_json::json!({ + "event": "newStatements", + "data": { + "statements": ["0xdeadbeef", "0xcafe"], + "remaining": 0, + }, + }); + + assert_eq!( + parse_new_statements_result("remote-sub".to_string(), &result).unwrap(), + NewStatements { + remote_subscription_id: "remote-sub".to_string(), + statements: vec![vec![0xde, 0xad, 0xbe, 0xef], vec![0xca, 0xfe]], + remaining: Some(0), + } + ); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/statement_store/statement.rs b/rust/crates/truapi-server/src/host_logic/statement_store/statement.rs new file mode 100644 index 00000000..b4f29302 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/statement_store/statement.rs @@ -0,0 +1,675 @@ +use parity_scale_codec::{Compact, Decode, Encode}; +use schnorrkel::{PublicKey, SecretKey, Signature}; +use truapi::v01; + +use super::StatementStoreParseError; +use crate::host_logic::session::SsoSessionInfo; + +const SR25519_SIGNING_CONTEXT: &[u8] = b"substrate"; + +/// Verified statement payload plus the sr25519 signer recovered from proof. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerifiedStatementData { + /// Raw statement data field. + pub data: Vec, + /// Sr25519 signer recovered from the proof. + pub signer: [u8; 32], + /// Raw `Expiry` field, if present: unix seconds in the upper 32 bits. + pub expiry: Option, +} + +/// SCALE statement proof variants mirrored from `sp_statement_store::Proof`. +/// +/// See the current upstream `Proof` codec: +/// +/// +/// `OnChain` is retained for v01 wire compatibility with older +/// statement-store bytes: +/// +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum StatementProof { + Sr25519 { + signature: [u8; 64], + signer: [u8; 32], + }, + Ed25519 { + signature: [u8; 64], + signer: [u8; 32], + }, + Ecdsa { + signature: [u8; 65], + signer: [u8; 33], + }, + OnChain { + who: [u8; 32], + block_hash: [u8; 32], + event: u64, + }, +} + +/// SCALE statement field variants mirrored from `sp_statement_store::Field`. +/// +/// See the upstream statement field vector codec: +/// +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum StatementField { + Proof(StatementProof), + DecryptionKey([u8; 32]), + Expiry(u64), + Channel([u8; 32]), + Topic1([u8; 32]), + Topic2([u8; 32]), + Topic3([u8; 32]), + Topic4([u8; 32]), + Data(Vec), +} + +/// Extract the raw `Data` field from a SCALE-encoded statement. +pub fn decode_statement_data(statement: &[u8]) -> Result, StatementStoreParseError> { + statement_data_from_fields(decode_statement_fields(statement)?) +} + +/// Verify statement proof and extract signer, expiry, and raw `Data` field. +pub fn decode_verified_statement_data( + statement: &[u8], + expected_signer: Option<[u8; 32]>, +) -> Result { + let fields = decode_statement_fields(statement)?; + let signer = verify_statement_proof(&fields, expected_signer)?; + let expiry = fields.iter().find_map(|field| match field { + StatementField::Expiry(value) => Some(*value), + _ => None, + }); + let data = statement_data_from_fields(fields)?; + Ok(VerifiedStatementData { + data, + signer, + expiry, + }) +} + +/// Whether a statement `Expiry` field (unix seconds in the upper 32 bits) is +/// in the past relative to `now_unix_secs`. +pub fn statement_expiry_elapsed(expiry: u64, now_unix_secs: u64) -> bool { + (expiry >> 32) < now_unix_secs +} + +/// Current unix time in seconds, used to stamp outgoing statement expiries +/// and to gate inbound statement freshness. Trusts the local clock on both +/// native and wasm targets. +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn current_unix_secs() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +/// Current unix time in seconds on wasm32, sourced from the JS clock. +#[cfg(target_arch = "wasm32")] +pub(crate) fn current_unix_secs() -> u64 { + (js_sys::Date::now() / 1000.0) as u64 +} + +/// Decode a SCALE signed statement into the public v01 statement shape. +pub fn decode_signed_statement( + statement: &[u8], +) -> Result { + signed_statement_from_fields(decode_statement_fields(statement)?) +} + +/// Build a signed statement on the active SSO request channel. +pub fn build_signed_session_request_statement( + session: &SsoSessionInfo, + encrypted_data: Vec, + expiry: u64, +) -> Result, String> { + build_signed_statement( + session, + session.request_channel, + session.session_id_own, + encrypted_data, + expiry, + ) +} + +/// Build a signed statement for an arbitrary channel/topic pair. +pub fn build_signed_statement( + session: &SsoSessionInfo, + channel: [u8; 32], + topic1: [u8; 32], + data: Vec, + expiry: u64, +) -> Result, String> { + let fields = vec![ + StatementField::Expiry(expiry), + StatementField::Channel(channel), + StatementField::Topic1(topic1), + StatementField::Data(data), + ]; + sign_statement_fields(session.ss_secret, session.ss_public_key, fields) + .map(|fields| fields.encode()) +} + +/// Sort fields, insert an sr25519 proof, and return signed fields. +pub fn sign_statement_fields( + ss_secret: [u8; 64], + expected_public_key: [u8; 32], + mut fields: Vec, +) -> Result, String> { + if fields + .iter() + .any(|field| matches!(field, StatementField::Proof(_))) + { + return Err("statement is already signed".to_string()); + } + fields.sort_by_key(statement_field_sort_index); + + let secret = + SecretKey::from_bytes(&ss_secret).map_err(|err| format!("invalid ss_secret: {err}"))?; + let public = secret.to_public(); + if public.to_bytes() != expected_public_key { + return Err("ss_secret does not match session statement public key".to_string()); + } + + let signing_payload = statement_signing_payload(&fields)?; + let signature = secret + .sign_simple(SR25519_SIGNING_CONTEXT, &signing_payload, &public) + .to_bytes(); + + let mut signed = Vec::with_capacity(fields.len() + 1); + signed.push(StatementField::Proof(StatementProof::Sr25519 { + signature, + signer: expected_public_key, + })); + signed.extend(fields); + Ok(signed) +} + +/// Build the statement signing payload from sorted fields. +pub fn statement_signing_payload(fields: &[StatementField]) -> Result, String> { + let encoded = fields.to_vec().encode(); + let mut input = encoded.as_slice(); + let _: Compact = + Decode::decode(&mut input).map_err(|err| format!("invalid statement vector: {err}"))?; + let compact_len = encoded.len() - input.len(); + Ok(encoded[compact_len..].to_vec()) +} + +fn decode_statement_fields( + statement: &[u8], +) -> Result, StatementStoreParseError> { + let mut input = statement; + let fields: Vec = Decode::decode(&mut input) + .map_err(|err| StatementStoreParseError::InvalidStatementScale(err.to_string()))?; + if !input.is_empty() { + return Err(StatementStoreParseError::Malformed( + "statement has trailing bytes".to_string(), + )); + } + Ok(fields) +} + +fn statement_data_from_fields( + fields: Vec, +) -> Result, StatementStoreParseError> { + fields + .into_iter() + .find_map(|field| match field { + StatementField::Data(value) => Some(value), + _ => None, + }) + .ok_or_else(|| StatementStoreParseError::Malformed("statement has no data".to_string())) +} + +fn verify_statement_proof( + fields: &[StatementField], + expected_signer: Option<[u8; 32]>, +) -> Result<[u8; 32], StatementStoreParseError> { + let mut proof = None; + let mut unsigned_fields = Vec::with_capacity(fields.len().saturating_sub(1)); + for field in fields { + match field { + StatementField::Proof(StatementProof::Sr25519 { signature, signer }) => { + if proof.replace((*signature, *signer)).is_some() { + return Err(StatementStoreParseError::InvalidStatementProof( + "statement has duplicate proof".to_string(), + )); + } + } + StatementField::Proof(_) => { + return Err(StatementStoreParseError::InvalidStatementProof( + "statement proof is not sr25519".to_string(), + )); + } + field => unsigned_fields.push(field.clone()), + } + } + let (signature, signer) = proof.ok_or_else(|| { + StatementStoreParseError::InvalidStatementProof("statement has no proof".to_string()) + })?; + if let Some(expected) = expected_signer + && signer != expected + { + return Err(StatementStoreParseError::InvalidStatementProof( + "statement proof signer does not match expected peer".to_string(), + )); + } + + unsigned_fields.sort_by_key(statement_field_sort_index); + let payload = + statement_signing_payload(&unsigned_fields).map_err(StatementStoreParseError::Malformed)?; + let public = PublicKey::from_bytes(&signer).map_err(|err| { + StatementStoreParseError::InvalidStatementProof(format!("invalid sr25519 signer: {err}")) + })?; + let signature = Signature::from_bytes(&signature).map_err(|err| { + StatementStoreParseError::InvalidStatementProof(format!("invalid sr25519 signature: {err}")) + })?; + public + .verify_simple(SR25519_SIGNING_CONTEXT, &payload, &signature) + .map_err(|err| { + StatementStoreParseError::InvalidStatementProof(format!( + "sr25519 signature verification failed: {err}" + )) + })?; + Ok(signer) +} + +/// Convert a public v01 statement into SCALE statement fields. +pub fn statement_fields_from_v01(statement: v01::Statement) -> Result, String> { + let mut fields = Vec::new(); + if let Some(proof) = statement.proof { + fields.push(StatementField::Proof(statement_proof_from_v01(proof))); + } + if let Some(decryption_key) = statement.decryption_key { + fields.push(StatementField::DecryptionKey(decryption_key)); + } + if let Some(expiry) = statement.expiry { + fields.push(StatementField::Expiry(expiry)); + } + if let Some(channel) = statement.channel { + fields.push(StatementField::Channel(channel)); + } + push_statement_topics(&mut fields, statement.topics)?; + if let Some(data) = statement.data { + fields.push(StatementField::Data(data)); + } + Ok(fields) +} + +/// Convert a public v01 signed statement into SCALE bytes. +pub fn signed_statement_to_scale(statement: v01::SignedStatement) -> Result, String> { + Ok(signed_statement_fields(statement)?.encode()) +} + +fn signed_statement_fields(statement: v01::SignedStatement) -> Result, String> { + let mut fields = vec![StatementField::Proof(statement_proof_from_v01( + statement.proof, + ))]; + if let Some(decryption_key) = statement.decryption_key { + fields.push(StatementField::DecryptionKey(decryption_key)); + } + if let Some(expiry) = statement.expiry { + fields.push(StatementField::Expiry(expiry)); + } + if let Some(channel) = statement.channel { + fields.push(StatementField::Channel(channel)); + } + push_statement_topics(&mut fields, statement.topics)?; + if let Some(data) = statement.data { + fields.push(StatementField::Data(data)); + } + fields.sort_by_key(statement_field_sort_index); + Ok(fields) +} + +fn signed_statement_from_fields( + fields: Vec, +) -> Result { + let mut proof = None; + let mut decryption_key = None; + let mut expiry = None; + let mut channel = None; + let mut topics = Vec::new(); + let mut data = None; + + for field in fields { + match field { + StatementField::Proof(value) => { + if proof.replace(statement_proof_to_v01(value)).is_some() { + return Err(StatementStoreParseError::Malformed( + "statement has duplicate proof".to_string(), + )); + } + } + StatementField::DecryptionKey(value) => { + if decryption_key.replace(value).is_some() { + return Err(StatementStoreParseError::Malformed( + "statement has duplicate decryption key".to_string(), + )); + } + } + StatementField::Expiry(value) => { + if expiry.replace(value).is_some() { + return Err(StatementStoreParseError::Malformed( + "statement has duplicate expiry".to_string(), + )); + } + } + StatementField::Channel(value) => { + if channel.replace(value).is_some() { + return Err(StatementStoreParseError::Malformed( + "statement has duplicate channel".to_string(), + )); + } + } + StatementField::Topic1(value) + | StatementField::Topic2(value) + | StatementField::Topic3(value) + | StatementField::Topic4(value) => topics.push(value), + StatementField::Data(value) => { + if data.replace(value).is_some() { + return Err(StatementStoreParseError::Malformed( + "statement has duplicate data".to_string(), + )); + } + } + } + } + + let proof = proof + .ok_or_else(|| StatementStoreParseError::Malformed("statement has no proof".to_string()))?; + Ok(v01::SignedStatement { + proof, + decryption_key, + expiry, + channel, + topics, + data, + }) +} + +/// Convert an internal proof into the public v01 proof shape. +pub fn statement_proof_to_v01(proof: StatementProof) -> v01::StatementProof { + match proof { + StatementProof::Sr25519 { signature, signer } => { + v01::StatementProof::Sr25519 { signature, signer } + } + StatementProof::Ed25519 { signature, signer } => { + v01::StatementProof::Ed25519 { signature, signer } + } + StatementProof::Ecdsa { signature, signer } => { + v01::StatementProof::Ecdsa { signature, signer } + } + StatementProof::OnChain { + who, + block_hash, + event, + } => v01::StatementProof::OnChain { + who, + block_hash, + event, + }, + } +} + +fn statement_proof_from_v01(proof: v01::StatementProof) -> StatementProof { + match proof { + v01::StatementProof::Sr25519 { signature, signer } => { + StatementProof::Sr25519 { signature, signer } + } + v01::StatementProof::Ed25519 { signature, signer } => { + StatementProof::Ed25519 { signature, signer } + } + v01::StatementProof::Ecdsa { signature, signer } => { + StatementProof::Ecdsa { signature, signer } + } + v01::StatementProof::OnChain { + who, + block_hash, + event, + } => StatementProof::OnChain { + who, + block_hash, + event, + }, + } +} + +fn push_statement_topics( + fields: &mut Vec, + topics: Vec<[u8; 32]>, +) -> Result<(), String> { + if topics.len() > 4 { + return Err(format!( + "statement has {} topics, maximum is 4", + topics.len() + )); + } + for (index, topic) in topics.into_iter().enumerate() { + fields.push(match index { + 0 => StatementField::Topic1(topic), + 1 => StatementField::Topic2(topic), + 2 => StatementField::Topic3(topic), + 3 => StatementField::Topic4(topic), + _ => unreachable!("topic count checked above"), + }); + } + Ok(()) +} + +fn statement_field_sort_index(field: &StatementField) -> u8 { + // Keep in sync with upstream `sp_statement_store::Field` discriminants: + // https://github.com/paritytech/polkadot-sdk/blob/f2f3aa6a8fda8ea52282da9711b3c5da4ba82529/substrate/primitives/statement-store/src/lib.rs#L314-L337 + match field { + StatementField::Proof(_) => 0, + StatementField::DecryptionKey(_) => 1, + StatementField::Expiry(_) => 2, + StatementField::Channel(_) => 3, + StatementField::Topic1(_) => 4, + StatementField::Topic2(_) => 5, + StatementField::Topic3(_) => 6, + StatementField::Topic4(_) => 7, + StatementField::Data(_) => 8, + } +} + +/// Format a 32-byte statement-store topic as `0x`-prefixed hex. +pub fn hex_topic(topic: &[u8; 32]) -> String { + format!("0x{}", hex::encode(topic)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::host_logic::session::SsoSessionInfo; + use schnorrkel::{ExpansionMode, MiniSecretKey, PublicKey, Signature}; + + fn test_session() -> SsoSessionInfo { + let mini_secret = MiniSecretKey::from_bytes(&[7; 32]).unwrap(); + let keypair = mini_secret.expand_to_keypair(ExpansionMode::Ed25519); + SsoSessionInfo { + ss_secret: keypair.secret.to_bytes(), + ss_public_key: keypair.public.to_bytes(), + enc_secret: [1; 32], + peer_enc_pubkey: [2; 65], + identity_account_id: [3; 32], + session_id_own: [4; 32], + session_id_peer: [5; 32], + request_channel: [6; 32], + response_channel: [7; 32], + peer_request_channel: [8; 32], + } + } + + #[test] + fn decodes_statement_data_field() { + let statement = vec![ + StatementField::Proof(StatementProof::Sr25519 { + signature: [1; 64], + signer: [2; 32], + }), + StatementField::Expiry(42), + StatementField::Channel([3; 32]), + StatementField::Topic1([4; 32]), + StatementField::Data(vec![0xde, 0xad, 0xbe, 0xef]), + ] + .encode(); + + assert_eq!( + decode_statement_data(&statement).unwrap(), + vec![0xde, 0xad, 0xbe, 0xef] + ); + } + + #[test] + fn signed_statement_scale_round_trips_public_shape() { + let signed = v01::SignedStatement { + proof: v01::StatementProof::Sr25519 { + signature: [9; 64], + signer: [8; 32], + }, + decryption_key: Some([7; 32]), + expiry: Some(99), + channel: Some([6; 32]), + topics: vec![[1; 32], [2; 32]], + data: Some(vec![3, 4, 5]), + }; + + let encoded = signed_statement_to_scale(signed.clone()).unwrap(); + + assert_eq!(decode_signed_statement(&encoded).unwrap(), signed); + } + + #[test] + fn signing_payload_strips_scale_vec_compact_len() { + let fields = vec![ + StatementField::Expiry(42), + StatementField::Channel([3; 32]), + StatementField::Topic1([4; 32]), + StatementField::Data(vec![0xde, 0xad, 0xbe, 0xef]), + ]; + let encoded = fields.encode(); + + assert_eq!(encoded[0], 16); + assert_eq!(statement_signing_payload(&fields).unwrap(), encoded[1..]); + } + + #[test] + fn builds_signed_session_request_statement() { + let session = test_session(); + + let statement = + build_signed_session_request_statement(&session, vec![0xde, 0xad], 42).unwrap(); + let mut input = statement.as_slice(); + let fields = Vec::::decode(&mut input).unwrap(); + + assert!(input.is_empty()); + assert_eq!(fields.len(), 5); + let StatementField::Proof(StatementProof::Sr25519 { signature, signer }) = fields[0] else { + panic!("expected sr25519 proof"); + }; + assert_eq!(signer, session.ss_public_key); + assert_eq!(fields[1], StatementField::Expiry(42)); + assert_eq!(fields[2], StatementField::Channel(session.request_channel)); + assert_eq!(fields[3], StatementField::Topic1(session.session_id_own)); + assert_eq!(fields[4], StatementField::Data(vec![0xde, 0xad])); + + let payload = statement_signing_payload(&fields[1..]).unwrap(); + let public = PublicKey::from_bytes(&signer).unwrap(); + let signature = Signature::from_bytes(&signature).unwrap(); + public + .verify_simple(SR25519_SIGNING_CONTEXT, &payload, &signature) + .unwrap(); + } + + #[test] + fn verified_statement_data_accepts_valid_sr25519_proof() { + let session = test_session(); + let statement = + build_signed_session_request_statement(&session, vec![0xde, 0xad], 42).unwrap(); + + let verified = + decode_verified_statement_data(&statement, Some(session.ss_public_key)).unwrap(); + + assert_eq!( + verified, + VerifiedStatementData { + data: vec![0xde, 0xad], + signer: session.ss_public_key, + expiry: Some(42), + } + ); + } + + #[test] + fn verified_statement_data_rejects_tampered_signature() { + let session = test_session(); + let statement = + build_signed_session_request_statement(&session, vec![0xde, 0xad], 42).unwrap(); + let mut fields = Vec::::decode(&mut statement.as_slice()).unwrap(); + let StatementField::Proof(StatementProof::Sr25519 { signature, .. }) = &mut fields[0] + else { + panic!("expected sr25519 proof"); + }; + signature[0] ^= 0xff; + + let err = decode_verified_statement_data(&fields.encode(), Some(session.ss_public_key)) + .unwrap_err(); + + assert!( + matches!(err, StatementStoreParseError::InvalidStatementProof(reason) if reason.contains("signature verification failed")) + ); + } + + #[test] + fn verified_statement_data_rejects_wrong_expected_signer() { + let session = test_session(); + let statement = + build_signed_session_request_statement(&session, vec![0xde, 0xad], 42).unwrap(); + + assert_eq!( + decode_verified_statement_data(&statement, Some([0xaa; 32])).unwrap_err(), + StatementStoreParseError::InvalidStatementProof( + "statement proof signer does not match expected peer".to_string() + ) + ); + } + + #[test] + fn signing_rejects_mismatched_session_key_material() { + let mut session = test_session(); + session.ss_public_key = [0xff; 32]; + + assert_eq!( + build_signed_session_request_statement(&session, vec![0xde], 42).unwrap_err(), + "ss_secret does not match session statement public key" + ); + } + + #[test] + fn signing_rejects_already_signed_statements() { + let session = test_session(); + let fields = vec![StatementField::Proof(StatementProof::Sr25519 { + signature: [1; 64], + signer: session.ss_public_key, + })]; + + assert_eq!( + sign_statement_fields(session.ss_secret, session.ss_public_key, fields).unwrap_err(), + "statement is already signed" + ); + } + + #[test] + fn rejects_statement_without_data_field() { + let statement = vec![StatementField::Expiry(42)].encode(); + + assert_eq!( + decode_statement_data(&statement).unwrap_err(), + StatementStoreParseError::Malformed("statement has no data".to_string()) + ); + } +} diff --git a/rust/crates/truapi-server/src/lib.rs b/rust/crates/truapi-server/src/lib.rs new file mode 100644 index 00000000..8b56ec56 --- /dev/null +++ b/rust/crates/truapi-server/src/lib.rs @@ -0,0 +1,9 @@ +//! TrUAPI server runtime support. +//! +//! This layer contains host-agnostic logic shared by the runtime and target +//! adapters. Wire dispatch and platform runtime wiring are added by later stack +//! layers. + +#![forbid(unsafe_code)] + +pub mod host_logic;