iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit b1da9f35fe36b6c97b458908a69cc0b6d45a136b
parent 71e0562269049fdef3159f1dddd462a90e343019
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Thu, 10 Sep 2026 14:11:48 +0200

feat: add signed automatic updates

Diffstat:
M.gitignore | 1+
MCargo.lock | 649+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MCargo.toml | 15+++++++++++++++
MDockerfile | 2+-
MREADME.md | 22++++++++++++++++++++++
Aconfig/update-signing.key.pub | 2++
Mdeployment.sh | 133++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mfuzz/Cargo.lock | 18+++++++++---------
Mfuzz/Cargo.toml | 2+-
Mnginx.conf | 6++++++
Msrc-tauri/Cargo.lock | 324++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc-tauri/Cargo.toml | 2++
Msrc-tauri/src/main.rs | 53++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc-tauri/tauri.conf.json | 13+++++++++++++
Msrc/adapters/http/index_html.rs | 21++++++++++++++++++++-
Msrc/cli.rs | 5++++-
Msrc/main.rs | 6++++++
Msrc/main_tests.rs | 11+++++++++++
Asrc/updater.rs | 292+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwww/assets/iuna-ui.js | 39+++++++++++++++++++++++++++++++++++++++
20 files changed, 1583 insertions(+), 33 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -11,6 +11,7 @@ /downloads/* !/downloads/.gitkeep /config/admin-ip-allowlist.local +/config/update-signing.key __pycache__/ *.py[cod] .scratch diff --git a/Cargo.lock b/Cargo.lock @@ -3,6 +3,12 @@ version = 4 [[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -101,6 +107,12 @@ dependencies = [ ] [[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" @@ -226,6 +238,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -235,6 +263,15 @@ dependencies = [ ] [[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -294,6 +331,17 @@ dependencies = [ ] [[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -352,18 +400,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] name = "find-msvc-tools" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -548,6 +632,23 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -556,13 +657,125 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -575,6 +788,12 @@ dependencies = [ ] [[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -586,27 +805,44 @@ version = "0.4.29" dependencies = [ "anyhow", "axum", + "base64", "bip39", "chacha20poly1305", "ed25519-dalek", + "flate2", "getrandom 0.2.17", "kyn-vdf", + "minisign-verify", "num-bigint", "num-integer", "num-traits", "pbkdf2", "proptest", + "reqwest", "rusqlite", "secrecy", + "semver", "serde", "serde_json", "sha2", + "tar", "tempfile", "tokio", "tower", ] [[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] name = "kyn-vdf" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -645,6 +881,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -678,6 +920,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] name = "mio" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -689,6 +947,23 @@ dependencies = [ ] [[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -729,6 +1004,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -801,6 +1119,15 @@ dependencies = [ ] [[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -927,6 +1254,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] name = "rusqlite" version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -963,6 +1326,15 @@ dependencies = [ ] [[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -987,6 +1359,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1003,6 +1384,29 @@ dependencies = [ ] [[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" @@ -1111,6 +1515,12 @@ dependencies = [ ] [[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1143,6 +1553,12 @@ dependencies = [ ] [[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1175,6 +1591,31 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] [[package]] name = "tempfile" @@ -1210,6 +1651,16 @@ dependencies = [ ] [[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] name = "tinyvec" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1253,6 +1704,16 @@ dependencies = [ ] [[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] name = "tower" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1269,6 +1730,24 @@ dependencies = [ ] [[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1301,6 +1780,12 @@ dependencies = [ ] [[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1338,6 +1823,24 @@ dependencies = [ ] [[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 = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1359,6 +1862,15 @@ dependencies = [ ] [[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1381,9 +1893,9 @@ checksum = "e59fe57342dd136b22e8c7d1856e276b69e07a8f88153d218d2e54f19991a3eb" [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -1393,10 +1905,20 @@ dependencies = [ ] [[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1404,27 +1926,37 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1446,6 +1978,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] name = "zerocopy" version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1466,12 +2037,72 @@ dependencies = [ ] [[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -8,6 +8,7 @@ license = "Apache-2.0" [dependencies] anyhow = "1.0.98" axum = "0.8.4" +base64 = { version = "0.22", optional = true } bip39 = "2.2.2" chacha20poly1305 = "0.10.1" ed25519-dalek = "2.2.0" @@ -23,8 +24,22 @@ rusqlite = { version = "0.32.1", features = ["bundled"] } tokio = { version = "1.45.1", features = ["full"] } kyn-vdf = "=0.1.1" secrecy = { version = "0.10.3", default-features = false, features = ["serde"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"], optional = true } +semver = { version = "1", optional = true } +flate2 = { version = "1", optional = true } +tar = { version = "0.4", optional = true } +minisign-verify = { version = "0.2.5", optional = true } [features] +default = ["cli-updater"] +cli-updater = [ + "dep:base64", + "dep:flate2", + "dep:minisign-verify", + "dep:reqwest", + "dep:semver", + "dep:tar", +] e2e = [] fuzzing = [] diff --git a/Dockerfile b/Dockerfile @@ -70,7 +70,7 @@ RUN set -eux; \ mkdir -p /site/downloads; \ cp /site/downloads.html /site/downloads/index.html; \ sed -i "s|\${IUNA_VERSION}|${version}|g" /site/downloads/index.html; \ - printf '{"tag":"v%s","version":"%s","url":"https://getiuna.org/downloads/"}\n' "$version" "$version" > /site/downloads/latest.json; \ + if [ ! -f /site/downloads/latest.json ]; then printf '{"tag":"v%s","version":"%s","url":"https://getiuna.org/downloads/"}\n' "$version" "$version" > /site/downloads/latest.json; fi; \ rm -f /site/downloads.html; \ python3 /src/iuna-work/scripts/inject_cloudflare_analytics.py /site diff --git a/README.md b/README.md @@ -66,12 +66,22 @@ On macOS and Windows, closing the desktop window keeps the node running from the system tray. Choose **Open iuna** to reopen the window, or **Quit iuna** to stop the node. On Windows, a left click on the tray icon also reopens the window directly. +The desktop app checks for signed updates. When a new release is available, click the version +badge and choose **Install and restart**. The bundled node is stopped before installation; wallet, +settings, and chain data are not part of the app bundle and remain in place. + Release and deploy with: ```sh ./deployment.sh 0.4.7 ``` +Updater artifacts are signed with the private key at `config/update-signing.key` by default. This +file is ignored by Git and must be backed up separately; losing it prevents existing installations +from accepting future updates. Set `IUNA_UPDATE_SIGNING_KEY` to use a securely stored copy, and +`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` when that key is password-protected. The matching public key +is committed at `config/update-signing.key.pub`. + Releases regenerate [`CHANGELOG.md`](CHANGELOG.md) automatically from the full tagged Git history and commit titles. All new commits must use a Conventional Commit prefix such as `feat:`, `fix(wallet):`, or `docs:`. Enable the repository @@ -160,6 +170,18 @@ On Windows PowerShell: The binary prints a local management URL. Open it and follow setup. +On supported Linux releases, check or install a signed CLI update with: + +```sh +iuna --version +iuna update --check +iuna update +``` + +The updater replaces only the running executable. If it is installed in a system-owned directory, +run the command with suitable permissions or move Iuna to a user-writable binary directory. Restart +any long-running service after updating. + ## Optional: Local Docker Testnet For local P2P and consensus testing, start a six-node testnet with Docker diff --git a/config/update-signing.key.pub b/config/update-signing.key.pub @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEEzMEJDQjQ4RDVDQUY1MTMKUldRVDljclZTTXNMbzgvU2tIenpMQjdWbnNhbE9NV1RjZGthQVNYdEhiUkcvdEl1TlZCZVBmYUwK+ \ No newline at end of file diff --git a/deployment.sh b/deployment.sh @@ -109,6 +109,27 @@ ensure_tauri_cli() { fi } +update_signing_key() { + local key="${IUNA_UPDATE_SIGNING_KEY:-config/update-signing.key}" + [ -f "$key" ] || die "missing update signing key: ${key}; restore it from the secure release-key backup" + ( + cd "$(dirname "$key")" + printf '%s/%s\n' "$(pwd)" "$(basename "$key")" + ) +} + +validate_update_public_key() { + local configured_key + local committed_key + + require_command jq + configured_key="$(jq -r '.plugins.updater.pubkey' src-tauri/tauri.conf.json)" + committed_key="$(tr -d '\r\n' < config/update-signing.key.pub)" + [ -n "$configured_key" ] || die "desktop updater public key is empty" + [ "$configured_key" = "$committed_key" ] || \ + die "src-tauri/tauri.conf.json updater key does not match config/update-signing.key.pub" +} + clear_nsis_installers() { local nsis_dir="$1" @@ -348,7 +369,10 @@ build_macos_desktop_if_possible() { local version="$1" local artifact="downloads/iuna-v${version}-macos-aarch64-desktop.app.zip" - [ -f "$artifact" ] && return 0 + [ -f "$artifact" ] \ + && [ -f "downloads/iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz" ] \ + && [ -f "downloads/iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz.sig" ] \ + && return 0 [ "$(uname -s)" = "Darwin" ] || return 0 is_apple_silicon_macos || die "macOS desktop artifact requires Apple silicon; expected ${artifact}" @@ -356,40 +380,56 @@ build_macos_desktop_if_possible() { require_command ditto require_command rustup ensure_tauri_cli + local signing_key + signing_key="$(update_signing_key)" rustup target add aarch64-apple-darwin cargo build --release --locked --target aarch64-apple-darwin mkdir -p src-tauri/binaries downloads cp target/aarch64-apple-darwin/release/iuna src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin chmod +x src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin - (cd src-tauri && cargo tauri build --target aarch64-apple-darwin --bundles app) + (cd src-tauri && \ + TAURI_SIGNING_PRIVATE_KEY="$(cat "$signing_key")" \ + TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" \ + cargo tauri build --target aarch64-apple-darwin --bundles app) local app="src-tauri/target/aarch64-apple-darwin/release/bundle/macos/iuna.app" - codesign --force --deep --sign - --options runtime "$app" + local updater_archive="${app}.tar.gz" codesign --verify --deep --strict --verbose=4 "$app" + [ -f "$updater_archive" ] || die "missing macOS updater archive: ${updater_archive}" + [ -f "${updater_archive}.sig" ] || die "missing macOS updater signature: ${updater_archive}.sig" ditto -c -k --keepParent "$app" "$artifact" + cp "$updater_archive" "downloads/iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz" + cp "${updater_archive}.sig" "downloads/iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz.sig" } build_windows_desktop_if_possible() { local version="$1" local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe" - [ -f "$artifact" ] && return 0 + [ -f "$artifact" ] && [ -f "${artifact}.sig" ] && return 0 case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) ;; *) return 0 ;; esac ensure_tauri_cli + local signing_key + signing_key="$(update_signing_key)" cargo build --release --locked mkdir -p src-tauri/binaries downloads cp target/release/iuna.exe src-tauri/binaries/iuna-sidecar-x86_64-pc-windows-msvc.exe local nsis_dir="src-tauri/target/release/bundle/nsis" clear_nsis_installers "$nsis_dir" - (cd src-tauri && cargo tauri build --bundles nsis) + (cd src-tauri && \ + TAURI_SIGNING_PRIVATE_KEY="$(cat "$signing_key")" \ + TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" \ + cargo tauri build --bundles nsis) local installer installer="$(versioned_nsis_installer "$nsis_dir" "$version")" cp "$installer" "$artifact" + [ -f "${installer}.sig" ] || die "missing Windows updater signature: ${installer}.sig" + cp "${installer}.sig" "${artifact}.sig" } build_windows_desktop_in_docker_if_possible() { @@ -397,18 +437,21 @@ build_windows_desktop_in_docker_if_possible() { local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe" local builder_platform local builder_arch + local signing_key - [ -f "$artifact" ] && return 0 + [ -f "$artifact" ] && [ -f "${artifact}.sig" ] && return 0 command -v docker >/dev/null 2>&1 || return 0 builder_platform="$(docker_native_linux_platform)" builder_arch="${builder_platform#linux/}" + signing_key="$(update_signing_key)" mkdir -p downloads docker run --rm --pull=always --platform="$builder_platform" \ -e "IUNA_VERSION=${version}" \ -e "HOST_UID=$(id -u)" \ -e "HOST_GID=$(id -g)" \ + -e "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" \ -v iuna-windows-cargo-registry:/usr/local/cargo/registry \ -v iuna-windows-cargo-git:/usr/local/cargo/git \ -v iuna-windows-root-cache:/root/.cache \ @@ -416,10 +459,13 @@ build_windows_desktop_in_docker_if_possible() { -v "iuna-windows-${builder_arch}-tauri-target:/work/iuna/src-tauri/target" \ -v "$(pwd):/src/iuna:ro" \ -v "$(pwd)/downloads:/out" \ + -v "${signing_key}:/run/secrets/iuna-update.key:ro" \ rust:1.88-bookworm \ bash -c ' set -euo pipefail + export TAURI_SIGNING_PRIVATE_KEY="$(cat /run/secrets/iuna-update.key)" + apt-get update # The Linux-hosted Tauri CLI inspects enabled tray features while preparing # bundle settings, even when cargo-xwin targets a Windows NSIS installer. @@ -467,7 +513,9 @@ build_windows_desktop_in_docker_if_possible() { installer="${nsis_dir}/iuna_${IUNA_VERSION}_x64-setup.exe" [ -f "$installer" ] || { echo "Windows installer for version ${IUNA_VERSION} was not produced at ${installer}" >&2; exit 1; } cp "$installer" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe" - chown "${HOST_UID}:${HOST_GID}" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe" + test -f "${installer}.sig" || { echo "missing Windows updater signature: ${installer}.sig" >&2; exit 1; } + cp "${installer}.sig" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe.sig" + chown "${HOST_UID}:${HOST_GID}" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe.sig" ' } @@ -558,6 +606,72 @@ build_linux_cli_archives() { ' } +sign_cli_archives() { + local version="$1" + local signing_key + local artifact + + ensure_tauri_cli + signing_key="$(update_signing_key)" + for artifact in \ + "downloads/iuna-v${version}-linux-x86_64.tar.gz" \ + "downloads/iuna-v${version}-linux-aarch64.tar.gz"; do + [ -f "$artifact" ] || die "missing CLI update artifact: ${artifact}" + cargo tauri signer sign -f "$signing_key" -p "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" "$artifact" + done +} + +file_sha256() { + local file="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + else + shasum -a 256 "$file" | awk '{print $1}' + fi +} + +write_release_metadata() { + local version="$1" + local base="https://getiuna.org/downloads" + local linux_x86="iuna-v${version}-linux-x86_64.tar.gz" + local linux_arm="iuna-v${version}-linux-aarch64.tar.gz" + local mac="iuna-v${version}-macos-aarch64-desktop-update.app.tar.gz" + local windows="iuna-v${version}-windows-x86_64-desktop-setup.exe" + + require_command jq + for file in "$linux_x86" "$linux_arm" "$mac" "$windows"; do + [ -f "downloads/$file" ] || die "missing release artifact: downloads/${file}" + [ -f "downloads/${file}.sig" ] || die "missing release signature: downloads/${file}.sig" + done + + jq -n \ + --arg tag "v${version}" \ + --arg version "$version" \ + --arg url "${base}/" \ + --arg linux_x86_url "${base}/${linux_x86}" \ + --arg linux_x86_sha "$(file_sha256 "downloads/$linux_x86")" \ + --rawfile linux_x86_sig "downloads/${linux_x86}.sig" \ + --arg linux_arm_url "${base}/${linux_arm}" \ + --arg linux_arm_sha "$(file_sha256 "downloads/$linux_arm")" \ + --rawfile linux_arm_sig "downloads/${linux_arm}.sig" \ + '{tag: $tag, version: $version, url: $url, artifacts: { + "linux-x86_64": {url: $linux_x86_url, sha256: $linux_x86_sha, signature: $linux_x86_sig}, + "linux-aarch64": {url: $linux_arm_url, sha256: $linux_arm_sha, signature: $linux_arm_sig} + }}' > downloads/latest.json + + mkdir -p downloads/desktop + jq -n \ + --arg version "$version" \ + --arg mac_url "${base}/${mac}" \ + --rawfile mac_sig "downloads/${mac}.sig" \ + --arg windows_url "${base}/${windows}" \ + --rawfile windows_sig "downloads/${windows}.sig" \ + '{version: $version, platforms: { + "darwin-aarch64": {url: $mac_url, signature: $mac_sig}, + "windows-x86_64": {url: $windows_url, signature: $windows_sig} + }}' > downloads/desktop/latest.json +} + write_download_checksums() { ( cd downloads @@ -588,11 +702,14 @@ build_versions() { local version="$1" mkdir -p downloads + validate_update_public_key build_linux_cli_archives "$version" build_macos_desktop_if_possible "$version" build_windows_desktop_if_possible "$version" build_windows_desktop_in_docker_if_possible "$version" require_desktop_artifacts "$version" + sign_cli_archives "$version" + write_release_metadata "$version" write_download_checksums } @@ -841,7 +958,7 @@ main() { exit 1 fi run_release_tests "$skip_long_tests" - build_linux_cli_archives "$version" + build_versions "$version" build_docker_image "$version" deploy_docker_image "$version" "$genesis" exit 0 diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock @@ -1224,9 +1224,9 @@ checksum = "e59fe57342dd136b22e8c7d1856e276b69e07a8f88153d218d2e54f19991a3eb" [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -1237,9 +1237,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1247,22 +1247,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml @@ -10,7 +10,7 @@ publish = false cargo-fuzz = true [dependencies] -iuna = { path = "..", features = ["fuzzing"] } +iuna = { path = "..", default-features = false, features = ["fuzzing"] } libfuzzer-sys = "0.4" serde_json = "1" diff --git a/nginx.conf b/nginx.conf @@ -23,6 +23,12 @@ server { try_files $uri =404; } + location = /downloads/desktop/latest.json { + add_header Access-Control-Allow-Origin "*" always; + add_header Cache-Control "no-cache" always; + try_files $uri =404; + } + location /downloads/ { autoindex on; try_files $uri $uri/ =404; diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock @@ -48,6 +48,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] name = "atk" version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -357,7 +366,7 @@ dependencies = [ "bitflags 2.13.1", "core-foundation", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -511,6 +520,17 @@ dependencies = [ ] [[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] name = "derive_more" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -756,6 +776,16 @@ dependencies = [ ] [[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -785,12 +815,21 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -806,6 +845,12 @@ dependencies = [ [[package]] name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" @@ -1281,6 +1326,22 @@ dependencies = [ ] [[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] name = "hyper-util" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1513,9 +1574,11 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" name = "iuna-desktop" version = "0.4.29" dependencies = [ + "serde_json", "tauri", "tauri-build", "tauri-plugin-shell", + "tauri-plugin-updater", ] [[package]] @@ -1688,6 +1751,12 @@ dependencies = [ ] [[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" @@ -1741,6 +1810,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + +[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1783,6 +1858,23 @@ dependencies = [ ] [[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] name = "ndk" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1972,6 +2064,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.1", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -1988,6 +2081,18 @@ dependencies = [ ] [[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] name = "objc2-quartz-core" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2062,6 +2167,49 @@ dependencies = [ ] [[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2078,6 +2226,20 @@ dependencies = [ ] [[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] name = "pango" version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2437,15 +2599,19 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-tls", "hyper-util", "js-sys", "log", + "native-tls", "percent-encoding", "pin-project-lite", + "rustls-pki-types", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-util", "tower", "tower-http", @@ -2473,6 +2639,28 @@ dependencies = [ ] [[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2488,6 +2676,15 @@ dependencies = [ ] [[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 = "schemars" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2545,6 +2742,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "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 = "selectors" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3035,6 +3255,17 @@ dependencies = [ ] [[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] name = "target-lexicon" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3191,6 +3422,38 @@ dependencies = [ ] [[package]] +name = "tauri-plugin-updater" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28d8cabdeb0564f03ae261963de4bc3d98321cd3d213e76a81b7d344e5df606" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.19", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + +[[package]] name = "tauri-runtime" version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3291,6 +3554,19 @@ dependencies = [ ] [[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] name = "tendril" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3395,6 +3671,16 @@ dependencies = [ ] [[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] name = "tokio-util" version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3728,6 +4014,12 @@ dependencies = [ ] [[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] name = "version-compare" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4500,6 +4792,16 @@ dependencies = [ ] [[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] name = "yoke" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4544,6 +4846,12 @@ dependencies = [ ] [[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] name = "zerotrie" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4577,6 +4885,18 @@ dependencies = [ ] [[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + +[[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml @@ -10,5 +10,7 @@ publish = false tauri-build = { version = "2", features = [] } [dependencies] +serde_json = "1" tauri = { version = "2", features = ["tray-icon"] } tauri-plugin-shell = "2" +tauri-plugin-updater = { version = "2", default-features = false, features = ["native-tls", "zip"] } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs @@ -4,8 +4,9 @@ use std::{ sync::{Mutex, OnceLock}, }; -use tauri::WindowEvent; +use tauri::{AppHandle, State, WindowEvent}; use tauri_plugin_shell::{ShellExt, process::CommandChild}; +use tauri_plugin_updater::{Update, UpdaterExt}; #[cfg(any(target_os = "macos", target_os = "windows"))] use tauri::{ @@ -16,13 +17,22 @@ use tauri::{ struct IunaSidecar(Mutex<Option<CommandChild>>); struct IunaSleepInhibitor(Mutex<Option<SleepInhibitor>>); +struct PendingDesktopUpdate(Mutex<Option<Update>>); static SIDECAR: OnceLock<IunaSidecar> = OnceLock::new(); static SLEEP_INHIBITOR: OnceLock<IunaSleepInhibitor> = OnceLock::new(); fn main() { + let updater = tauri_plugin_updater::Builder::new() + .pubkey(include_str!("../../config/update-signing.key.pub").trim()); tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(updater.build()) + .manage(PendingDesktopUpdate(Mutex::new(None))) + .invoke_handler(tauri::generate_handler![ + check_desktop_update, + install_desktop_update + ]) .setup(|app| { #[cfg(any(target_os = "macos", target_os = "windows"))] setup_desktop_tray(app)?; @@ -80,6 +90,47 @@ fn main() { }); } +#[tauri::command] +async fn check_desktop_update( + app: AppHandle, + pending: State<'_, PendingDesktopUpdate>, +) -> Result<Option<String>, String> { + let update = app + .updater() + .map_err(|error| error.to_string())? + .check() + .await + .map_err(|error| error.to_string())?; + let version = update.as_ref().map(|update| update.version.clone()); + *pending.0.lock().map_err(|_| "updater mutex poisoned")? = update; + Ok(version) +} + +#[tauri::command] +async fn install_desktop_update( + app: AppHandle, + pending: State<'_, PendingDesktopUpdate>, +) -> Result<(), String> { + let update = pending + .0 + .lock() + .map_err(|_| "updater mutex poisoned")? + .take() + .ok_or_else(|| "no verified desktop update is ready".to_string())?; + let bytes = update + .download(|_, _| {}, || {}) + .await + .map_err(|error| error.to_string())?; + + stop_sidecar(); + update.install(bytes).map_err(|error| error.to_string())?; + + #[cfg(not(target_os = "windows"))] + app.restart(); + #[cfg(target_os = "windows")] + Ok(()) +} + #[cfg(any(target_os = "macos", target_os = "windows"))] fn setup_desktop_tray(app: &tauri::App) -> tauri::Result<()> { const OPEN_MENU_ID: &str = "open-iuna"; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json @@ -26,6 +26,7 @@ }, "bundle": { "active": true, + "createUpdaterArtifacts": true, "targets": "all", "externalBin": [ "binaries/iuna-sidecar" @@ -46,7 +47,19 @@ } }, "macOS": { + "signingIdentity": "-", "minimumSystemVersion": "11.0" } + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEEzMEJDQjQ4RDVDQUY1MTMKUldRVDljclZTTXNMbzgvU2tIenpMQjdWbnNhbE9NV1RjZGthQVNYdEhiUkcvdEl1TlZCZVBmYUwK", + "endpoints": [ + "https://getiuna.org/downloads/desktop/latest.json" + ], + "windows": { + "installMode": "passive" + } + } } } diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs @@ -620,7 +620,7 @@ pub(super) const INDEX_HTML: &str = concat!( <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.7 3.2 9.2 5.5a7.2 7.2 0 0 0-1.4.8L5.6 5.6 3.2 9.8l1.7 1.6a7.8 7.8 0 0 0 0 1.6l-1.7 1.6 2.4 4.2 2.2-.7a7.2 7.2 0 0 0 1.4.8l.5 2.3h4.8l.5-2.3a7.2 7.2 0 0 0 1.4-.8l2.2.7 2.4-4.2-1.7-1.6a7.8 7.8 0 0 0 0-1.6L21 9.8l-2.4-4.2-2.2.7a7.2 7.2 0 0 0-1.4-.8l-.5-2.3H9.7Z"></path><circle cx="12" cy="12.2" r="3.1"></circle></svg> <span>Settings</span> </button> - <button class="version-panel" type="button" :class="{ update: updateAvailable(), checking: releaseCheckState === 'checking', failed: releaseCheckState === 'failed' }" :title="versionPanelTitle()" @click="openLatestRelease"> + <button class="version-panel" type="button" :disabled="desktopUpdateBusy" :class="{ update: updateAvailable(), checking: releaseCheckState === 'checking', failed: releaseCheckState === 'failed' }" :title="versionPanelTitle()" @click="openLatestRelease"> <span class="version-dot" aria-hidden="true"></span> <span class="version-label" x-text="appVersionLabel()"></span> <span class="version-update" x-show="updateAvailable()">Update</span> @@ -1578,6 +1578,25 @@ pub(super) const INDEX_HTML: &str = concat!( </div> </section> </div> + <div class="setup-overlay transaction-overlay" x-show="desktopUpdateModalOpen" x-transition.opacity @click.self="closeDesktopUpdateModal()" role="dialog" aria-modal="true" aria-labelledby="desktop-update-title"> + <section class="tx-modal"> + <div class="tx-modal-head"> + <div class="tx-modal-title"> + <span class="pill transfer">Update</span> + <h2 id="desktop-update-title">Install <span x-text="latestReleaseLabel()"></span></h2> + </div> + <button type="button" @click="closeDesktopUpdateModal" :disabled="desktopUpdateBusy">Close</button> + </div> + <div class="info-copy"> + <p>Iuna will download and verify the signed update, stop the local node, install it, and restart the desktop app.</p> + <p>Your wallet, settings, and local chain data stay on this device.</p> + </div> + <div class="danger-actions"> + <button class="subtle" type="button" @click="closeDesktopUpdateModal" :disabled="desktopUpdateBusy">Later</button> + <button class="primary" type="button" @click="installDesktopUpdate" :disabled="desktopUpdateBusy" x-text="desktopUpdateBusy ? 'Installing...' : 'Install and restart'"></button> + </div> + </section> + </div> <div class="setup-overlay transaction-overlay" x-show="chainResetModalOpen" x-transition.opacity @click.self="closeChainResetModal()" role="dialog" aria-modal="true" aria-labelledby="chain-reset-title"> <section class="tx-modal"> <div class="tx-modal-head"> diff --git a/src/cli.rs b/src/cli.rs @@ -313,8 +313,11 @@ pub(crate) fn help_text() -> &'static str { Usage:\n\ iuna [options]\n\ iuna --genesis [options]\n\ - iuna --join <addr:port> [options]\n\n\ + iuna --join <addr:port> [options]\n\ + iuna update [--check]\n\n\ Options:\n\ + --version Print the installed iuna version\n\ + update [--check] Install or only check for a signed CLI update\n\ --genesis Create a new chain with a fresh setup wallet\n\ --wallet <path> Wallet file (default <data-dir>/wallet.json)\n\ --chain-db <path> Chain SQLite database (default <data-dir>/chain.sqlite3)\n\ diff --git a/src/main.rs b/src/main.rs @@ -29,6 +29,8 @@ use secrecy::{ExposeSecret, SecretString}; use tokio::sync::Mutex; mod cli; +#[cfg(feature = "cli-updater")] +mod updater; use cli::{ ChainMode, CliOptions, apply_cli_p2p_config_overrides, apply_cli_stratum_config_overrides, apply_cli_wallet_endpoint_config_overrides, configured_p2p_announce_addr, @@ -58,6 +60,10 @@ const WALLET_ENDPOINT_PORT_ENV: &str = "IUNA_WALLET_ENDPOINT_PORT"; #[tokio::main] async fn main() -> Result<()> { + #[cfg(feature = "cli-updater")] + if updater::handle_cli_command().await? { + return Ok(()); + } let Some(opts) = CliOptions::parse()? else { return Ok(()); }; diff --git a/src/main_tests.rs b/src/main_tests.rs @@ -222,6 +222,17 @@ fn transaction_filters_default_to_every_transaction_type() { } #[test] +fn management_ui_uses_the_native_desktop_updater_when_available() { + let html = include_str!("adapters/http/index_html.rs"); + let javascript = include_str!("../www/assets/iuna-ui.js"); + + assert!(javascript.contains("invoke(\"check_desktop_update\")")); + assert!(javascript.contains("invoke(\"install_desktop_update\")")); + assert!(html.contains("Install and restart")); + assert!(!javascript.contains("window.confirm")); +} + +#[test] fn lightweight_wallet_recent_activity_does_not_use_view_all_filters() { let javascript = include_str!("../wallet/app.js"); diff --git a/src/updater.rs b/src/updater.rs @@ -0,0 +1,292 @@ +use std::{ + collections::BTreeMap, + fs::{self, OpenOptions}, + io::{Read, Write}, + path::Path, +}; + +use anyhow::{Context, Result, bail}; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +use flate2::read::GzDecoder; +use minisign_verify::{PublicKey, Signature}; +use semver::Version; +use serde::Deserialize; +use sha2::{Digest, Sha256}; + +const RELEASE_METADATA_URL: &str = "https://getiuna.org/downloads/latest.json"; +const UPDATE_PUBLIC_KEY: &str = include_str!("../config/update-signing.key.pub"); +const MAX_UPDATE_BYTES: u64 = 256 * 1024 * 1024; + +#[derive(Debug, Deserialize)] +struct ReleaseMetadata { + version: String, + #[serde(default)] + artifacts: BTreeMap<String, ReleaseArtifact>, +} + +#[derive(Debug, Deserialize)] +struct ReleaseArtifact { + url: String, + sha256: String, + signature: String, +} + +pub(crate) async fn handle_cli_command() -> Result<bool> { + let mut args = std::env::args().skip(1); + let Some(command) = args.next() else { + return Ok(false); + }; + + if matches!(command.as_str(), "--version" | "-V") { + if args.next().is_some() { + bail!("--version does not accept additional arguments"); + } + println!("iuna {}", env!("CARGO_PKG_VERSION")); + return Ok(true); + } + + if command != "update" { + return Ok(false); + } + + let check_only = match args.next().as_deref() { + None => false, + Some("--check") => true, + Some(other) => bail!("unknown update option {other}; use `iuna update [--check]`"), + }; + if let Some(other) = args.next() { + bail!("unexpected update argument {other}; use `iuna update [--check]`"); + } + + let release = fetch_release_metadata().await?; + let current = Version::parse(env!("CARGO_PKG_VERSION")).context("invalid built-in version")?; + let available = Version::parse(release.version.trim_start_matches('v')) + .context("release metadata contains an invalid version")?; + + if available <= current { + println!("iuna v{current} is up to date"); + return Ok(true); + } + + println!("iuna v{available} is available (currently v{current})"); + if check_only { + return Ok(true); + } + + install_update(&release, &available).await?; + println!("updated iuna to v{available}; restart any running iuna service"); + Ok(true) +} + +async fn fetch_release_metadata() -> Result<ReleaseMetadata> { + let response = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build()? + .get(RELEASE_METADATA_URL) + .header(reqwest::header::ACCEPT, "application/json") + .send() + .await + .context("could not fetch iuna release metadata")? + .error_for_status() + .context("iuna release metadata request failed")?; + + response + .json() + .await + .context("could not decode iuna release metadata") +} + +async fn install_update(release: &ReleaseMetadata, version: &Version) -> Result<()> { + let target = update_target()?; + let artifact = release + .artifacts + .get(target) + .with_context(|| format!("release v{version} has no artifact for {target}"))?; + let archive = download_artifact(artifact).await?; + verify_artifact(&archive, artifact)?; + + let current_exe = + std::env::current_exe().context("could not locate the running iuna binary")?; + replace_executable(&current_exe, &archive, version) +} + +async fn download_artifact(artifact: &ReleaseArtifact) -> Result<Vec<u8>> { + let response = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(300)) + .build()? + .get(&artifact.url) + .send() + .await + .context("could not download the iuna update")? + .error_for_status() + .context("iuna update download failed")?; + + if response + .content_length() + .is_some_and(|size| size > MAX_UPDATE_BYTES) + { + bail!("iuna update is larger than the allowed 256 MiB"); + } + let bytes = response + .bytes() + .await + .context("could not read the iuna update")?; + if bytes.len() as u64 > MAX_UPDATE_BYTES { + bail!("iuna update is larger than the allowed 256 MiB"); + } + Ok(bytes.to_vec()) +} + +fn verify_artifact(bytes: &[u8], artifact: &ReleaseArtifact) -> Result<()> { + let actual_hash = format!("{:x}", Sha256::digest(bytes)); + if !actual_hash.eq_ignore_ascii_case(&artifact.sha256) { + bail!("iuna update checksum verification failed"); + } + + let public_key_text = decode_tauri_signature(UPDATE_PUBLIC_KEY) + .context("the embedded iuna update public key is invalid")?; + let public_key = PublicKey::decode(&public_key_text) + .context("the embedded iuna update public key is invalid")?; + let signature_text = decode_tauri_signature(&artifact.signature) + .context("the iuna update signature is invalid")?; + let signature = + Signature::decode(&signature_text).context("the iuna update signature is invalid")?; + public_key + .verify(bytes, &signature, true) + .context("iuna update signature verification failed") +} + +fn decode_tauri_signature(encoded: &str) -> Result<String> { + let decoded = BASE64 + .decode(encoded.trim()) + .context("invalid base64 in Tauri signature")?; + String::from_utf8(decoded).context("Tauri signature is not UTF-8") +} + +fn replace_executable(current_exe: &Path, archive: &[u8], version: &Version) -> Result<()> { + let parent = current_exe + .parent() + .context("the running iuna binary has no parent directory")?; + let staged = parent.join(format!(".iuna-update-{version}-{}", std::process::id())); + let result = stage_binary(&staged, archive).and_then(|_| { + fs::rename(&staged, current_exe).with_context(|| { + format!( + "cannot replace {}; install iuna in a writable directory or run the update with sufficient permissions", + current_exe.display() + ) + }) + }); + if result.is_err() { + let _ = fs::remove_file(&staged); + } + result +} + +fn stage_binary(destination: &Path, archive: &[u8]) -> Result<()> { + let decoder = GzDecoder::new(archive); + let mut tar = tar::Archive::new(decoder); + let mut found = false; + + for entry in tar + .entries() + .context("could not read the iuna update archive")? + { + let mut entry = entry.context("could not read an iuna update archive entry")?; + let path = entry + .path() + .context("invalid path in iuna update archive")?; + if path.file_name().and_then(|name| name.to_str()) != Some("iuna") + || !entry.header().entry_type().is_file() + { + continue; + } + if found { + bail!("iuna update archive contains multiple binaries"); + } + + let mut output = OpenOptions::new() + .create_new(true) + .write(true) + .open(destination) + .with_context(|| format!("cannot stage update at {}", destination.display()))?; + let mut buffer = [0_u8; 64 * 1024]; + loop { + let count = entry.read(&mut buffer)?; + if count == 0 { + break; + } + output.write_all(&buffer[..count])?; + } + output.sync_all()?; + found = true; + } + + if !found { + bail!("iuna update archive does not contain an iuna binary"); + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(destination, fs::Permissions::from_mode(0o755))?; + } + Ok(()) +} + +fn update_target() -> Result<&'static str> { + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + return Ok("linux-x86_64"); + #[cfg(all(target_os = "linux", target_arch = "aarch64"))] + return Ok("linux-aarch64"); + #[cfg(not(any( + all(target_os = "linux", target_arch = "x86_64"), + all(target_os = "linux", target_arch = "aarch64") + )))] + bail!("automatic CLI updates are not available for this platform") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn release_metadata_remains_compatible_with_original_shape() { + let metadata: ReleaseMetadata = serde_json::from_str( + r#"{"tag":"v0.4.29","version":"0.4.29","url":"https://getiuna.org/downloads/"}"#, + ) + .unwrap(); + assert_eq!(metadata.version, "0.4.29"); + assert!(metadata.artifacts.is_empty()); + } + + #[test] + fn archive_without_binary_is_rejected() { + let mut encoded = Vec::new(); + { + let encoder = + flate2::write::GzEncoder::new(&mut encoded, flate2::Compression::default()); + let mut archive = tar::Builder::new(encoder); + let mut header = tar::Header::new_gnu(); + header.set_size(4); + header.set_cksum(); + archive + .append_data(&mut header, "README.md", &b"test"[..]) + .unwrap(); + archive.into_inner().unwrap().finish().unwrap(); + } + let temp = tempfile::tempdir().unwrap(); + let error = stage_binary(&temp.path().join("iuna"), &encoded).unwrap_err(); + assert!(error.to_string().contains("does not contain")); + } + + #[test] + fn embedded_public_key_verifies_tauri_signature_format() { + const SIGNATURE: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVRVDljclZTTXNMbzZUQ3cwMUNibXF4bHhBZ0Vka0pCREhXS1VFWmNsbVc4ejVsUzZaN2ZNQ1VoVkwyV05nMGF2dzMrNjNMUlB5Wm1WS2JnbG4xVXhjV2s0N3dsMWx5aGdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg5MDQwNjQ0CWZpbGU6dG1wLklaQThVTDRXOEEKRldRWG9UaXplbFAwVk5CUllNSHJDSSsyM2dLaXBPTXA0UWNOc0xtLyt2d2lLdTgvSmM3dWRZZmpqTkl2Q3RCMTlEWEx5dVNUK0gxeHlJQXY2VWhNQWc9PQo="; + let artifact = ReleaseArtifact { + url: "https://getiuna.org/test".to_string(), + sha256: format!("{:x}", Sha256::digest(b"iuna updater test vector")), + signature: SIGNATURE.to_string(), + }; + verify_artifact(b"iuna updater test vector", &artifact).unwrap(); + } +} diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -47,6 +47,8 @@ window.iunaApp = function iunaApp() { latestRelease: null, releaseCheckState: "idle", releaseCheckError: null, + desktopUpdateModalOpen: false, + desktopUpdateBusy: false, config: { setup_complete: false }, auth: { configured: false, authenticated: false }, authLoaded: false, @@ -303,6 +305,7 @@ window.iunaApp = function iunaApp() { }, versionPanelTitle() { + if (this.desktopUpdateBusy) return "Installing update"; if (this.updateAvailable()) return `Update available: ${this.latestReleaseLabel()}`; if (this.releaseCheckState === "failed") return this.releaseCheckError || "Could not check latest release"; if (this.releaseCheckState === "checking") return "Checking latest release"; @@ -311,6 +314,15 @@ window.iunaApp = function iunaApp() { async openLatestRelease() { const url = this.latestRelease?.url || IUNA_DOWNLOADS_URL; + const invoke = window.__TAURI__?.core?.invoke; + if ( + this.updateAvailable() + && this.latestRelease?.desktopReady === true + && typeof invoke === "function" + ) { + this.desktopUpdateModalOpen = true; + return; + } try { const tauriOpen = window.__TAURI__?.shell?.open; if (typeof tauriOpen === "function") { @@ -321,6 +333,24 @@ window.iunaApp = function iunaApp() { window.open(url, "_blank", "noopener,noreferrer"); }, + closeDesktopUpdateModal() { + if (this.desktopUpdateBusy) return; + this.desktopUpdateModalOpen = false; + }, + + async installDesktopUpdate() { + const invoke = window.__TAURI__?.core?.invoke; + if (typeof invoke !== "function") return; + this.desktopUpdateBusy = true; + try { + await invoke("install_desktop_update"); + } catch (error) { + this.desktopUpdateBusy = false; + this.desktopUpdateModalOpen = false; + this.showFlash(error?.message || String(error) || "Desktop update failed", "error"); + } + }, + showingSetup() { return this.authLoaded && !this.showingAuth() && !this.showingNetworkMigration() && !this.config.setup_complete; }, @@ -1177,9 +1207,17 @@ window.iunaApp = function iunaApp() { if (!version) { throw new Error("Release metadata is missing a version"); } + let desktopVersion = null; + const invoke = window.__TAURI__?.core?.invoke; + if (typeof invoke === "function") { + try { + desktopVersion = this.normalizeVersion(await invoke("check_desktop_update")); + } catch {} + } this.latestRelease = { tag: `v${version}`, url: release.url || IUNA_DOWNLOADS_URL, + desktopReady: desktopVersion === version, }; this.releaseCheckState = "done"; } catch (error) { @@ -1368,6 +1406,7 @@ window.iunaApp = function iunaApp() { }, closeModals() { + this.closeDesktopUpdateModal(); this.closeOptimizeWallet(); this.closeSendConfirmModal(); this.closeTransactionModal();