diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-02-07 10:24:06 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-03-05 23:31:55 +0100 |
| commit | 5f24b9e9ce4161ac0cefb05bfc19388554acb357 (patch) | |
| tree | 83381ca8b478eac94d2f775dd33be5b03a3e76d8 | |
| parent | 60d23ed40849680f351b9b1c0aeff1d0fdd769f0 (diff) | |
| download | mullvadvpn-5f24b9e9ce4161ac0cefb05bfc19388554acb357.tar.xz mullvadvpn-5f24b9e9ce4161ac0cefb05bfc19388554acb357.zip | |
Implement version provider for API responses
This also replaces the PGP verifier with a SHA256 checksum verifier
| -rw-r--r-- | Cargo.lock | 524 | ||||
| -rw-r--r-- | installer-downloader/src/controller.rs | 28 | ||||
| -rw-r--r-- | installer-downloader/src/ui_downloader.rs | 3 | ||||
| -rw-r--r-- | installer-downloader/tests/controller.rs | 45 | ||||
| -rw-r--r-- | installer-downloader/tests/snapshots/controller__download-3.snap | 4 | ||||
| -rw-r--r-- | installer-downloader/tests/snapshots/controller__failed_verification.snap | 4 | ||||
| -rw-r--r-- | mullvad-update/Cargo.toml | 8 | ||||
| -rw-r--r-- | mullvad-update/src/api.rs | 157 | ||||
| -rw-r--r-- | mullvad-update/src/app.rs | 75 | ||||
| -rw-r--r-- | mullvad-update/src/deserializer.rs | 16 | ||||
| -rw-r--r-- | mullvad-update/src/format/mod.rs | 91 | ||||
| -rw-r--r-- | mullvad-update/src/verify.rs | 135 |
12 files changed, 394 insertions, 696 deletions
diff --git a/Cargo.lock b/Cargo.lock index 9ba618673a..2f59ea0fbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,15 +65,6 @@ dependencies = [ ] [[package]] -name = "aes-kw" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" -dependencies = [ - "aes", -] - -[[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -176,19 +167,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", - "zeroize", -] - -[[package]] name = "arrayref" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -369,12 +347,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] -name = "bitfield" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" - -[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -387,15 +359,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] name = "blake3" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -424,15 +387,6 @@ dependencies = [ ] [[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -442,35 +396,6 @@ dependencies = [ ] [[package]] -name = "blowfish" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" -dependencies = [ - "byteorder", - "cipher", -] - -[[package]] -name = "bstr" -version = "1.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "buffer-redux" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8acf87c5b9f5897cd3ebb9a327f420e0cae9dd4e5c1d2e36f2c84c571a58f1" -dependencies = [ - "memchr", -] - -[[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -523,15 +448,6 @@ dependencies = [ ] [[package]] -name = "cast5" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" -dependencies = [ - "cipher", -] - -[[package]] name = "cbindgen" version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -583,15 +499,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] -name = "cfb-mode" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" -dependencies = [ - "cipher", -] - -[[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -718,17 +625,6 @@ dependencies = [ ] [[package]] -name = "cmac" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" -dependencies = [ - "cipher", - "dbl", - "digest", -] - -[[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -845,12 +741,6 @@ dependencies = [ ] [[package]] -name = "crc24" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" - -[[package]] name = "crc32fast" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -945,7 +835,7 @@ dependencies = [ "cpufeatures", "curve25519-dalek-derive", "digest", - "fiat-crypto 0.2.8", + "fiat-crypto", "rustc_version", "subtle", "zeroize", @@ -1017,15 +907,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] -name = "dbl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" -dependencies = [ - "generic-array", -] - -[[package]] name = "dbus" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1053,7 +934,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", - "pem-rfc7468", "zeroize", ] @@ -1109,43 +989,12 @@ dependencies = [ ] [[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2 1.0.92", - "quote 1.0.36", - "syn 2.0.89", - "unicode-xid 0.2.6", -] - -[[package]] -name = "des" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" -dependencies = [ - "cipher", -] - -[[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] @@ -1189,22 +1038,6 @@ dependencies = [ ] [[package]] -name = "dsa" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" -dependencies = [ - "digest", - "num-bigint-dig", - "num-traits", - "pkcs8", - "rfc6979", - "sha2", - "signature", - "zeroize", -] - -[[package]] name = "duct" version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1217,30 +1050,14 @@ dependencies = [ ] [[package]] -name = "eax" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" -dependencies = [ - "aead", - "cipher", - "cmac", - "ctr", - "subtle", -] - -[[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", "elliptic-curve", - "rfc6979", "signature", - "spki", ] [[package]] @@ -1268,17 +1085,6 @@ dependencies = [ ] [[package]] -name = "ed448-goldilocks" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" -dependencies = [ - "fiat-crypto 0.1.20", - "hex", - "subtle", -] - -[[package]] name = "either" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1296,9 +1102,6 @@ dependencies = [ "ff", "generic-array", "group", - "hkdf", - "pem-rfc7468", - "pkcs8", "rand_core 0.6.4", "sec1", "subtle", @@ -1446,12 +1249,6 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - -[[package]] -name = "fiat-crypto" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" @@ -2192,15 +1989,6 @@ dependencies = [ ] [[package]] -name = "idea" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" -dependencies = [ - "cipher", -] - -[[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2423,12 +2211,6 @@ dependencies = [ ] [[package]] -name = "iter-read" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" - -[[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2537,20 +2319,6 @@ dependencies = [ ] [[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2", - "signature", -] - -[[package]] name = "keccak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2594,9 +2362,6 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin 0.5.2", -] [[package]] name = "libc" @@ -2799,12 +2564,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3236,11 +2995,11 @@ dependencies = [ "json-canon", "mockito", "mullvad-version", - "pgp", "rand 0.8.5", "reqwest", "serde", "serde_json", + "sha2", "tokio", ] @@ -3477,16 +3236,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" [[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] name = "notify" version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3525,24 +3274,6 @@ dependencies = [ ] [[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm 0.2.8", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "serde", - "smallvec", - "zeroize", -] - -[[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3600,27 +3331,6 @@ dependencies = [ ] [[package]] -name = "num_enum" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" -dependencies = [ - "proc-macro-crate", - "proc-macro2 1.0.92", - "quote 1.0.36", - "syn 2.0.89", -] - -[[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3750,18 +3460,6 @@ dependencies = [ ] [[package]] -name = "ocb3" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" -dependencies = [ - "aead", - "cipher", - "ctr", - "subtle", -] - -[[package]] name = "once_cell" version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3874,8 +3572,6 @@ checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ "ecdsa", "elliptic-curve", - "primeorder", - "sha2", ] [[package]] @@ -3887,21 +3583,6 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", -] - -[[package]] -name = "p521" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" -dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", - "primeorder", - "rand_core 0.6.4", - "sha2", ] [[package]] @@ -3942,17 +3623,6 @@ dependencies = [ ] [[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] name = "paste" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3976,15 +3646,6 @@ dependencies = [ ] [[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4058,73 +3719,6 @@ dependencies = [ ] [[package]] -name = "pgp" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1877a97fd422433220ad272eb008ec55691944b1200e9eb204e3cb2cb69d34e9" -dependencies = [ - "aes", - "aes-gcm", - "aes-kw", - "argon2", - "base64 0.22.1", - "bitfield", - "block-padding", - "blowfish", - "bstr", - "buffer-redux", - "byteorder", - "camellia", - "cast5", - "cfb-mode", - "chrono", - "cipher", - "const-oid", - "crc24", - "curve25519-dalek", - "derive_builder", - "derive_more", - "des", - "digest", - "dsa", - "eax", - "ecdsa", - "ed25519-dalek", - "elliptic-curve", - "flate2", - "generic-array", - "hex", - "hkdf", - "idea", - "iter-read", - "k256", - "log", - "md-5", - "nom", - "num-bigint-dig", - "num-traits", - "num_enum", - "ocb3", - "p256", - "p384", - "p521", - "rand 0.8.5", - "ripemd", - "rsa", - "sha1", - "sha1-checked", - "sha2", - "sha3", - "signature", - "smallvec", - "thiserror 1.0.59", - "twofish", - "x25519-dalek", - "x448", - "zeroize", -] - -[[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4195,17 +3789,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4400,21 +3983,12 @@ dependencies = [ ] [[package]] -name = "proc-macro-crate" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" -dependencies = [ - "toml_edit", -] - -[[package]] name = "proc-macro2" version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" dependencies = [ - "unicode-xid 0.1.0", + "unicode-xid", ] [[package]] @@ -4842,16 +4416,6 @@ dependencies = [ ] [[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4861,7 +4425,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.14", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] @@ -4886,41 +4450,12 @@ dependencies = [ ] [[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest", -] - -[[package]] name = "rs-release" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21efba391745f92fc14a5cccb008e711a1a3708d8dacd2e69d88d5de513c117a" [[package]] -name = "rsa" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] name = "rtnetlink" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5124,7 +4659,6 @@ dependencies = [ "base16ct", "der", "generic-array", - "pkcs8", "subtle", "zeroize", ] @@ -5239,17 +4773,6 @@ dependencies = [ ] [[package]] -name = "sha1-checked" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" -dependencies = [ - "digest", - "sha1", - "zeroize", -] - -[[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5309,7 +4832,7 @@ dependencies = [ "serde_urlencoded", "shadowsocks-crypto", "socket2", - "spin 0.9.8", + "spin", "thiserror 1.0.59", "tokio", "tokio-tfo", @@ -5368,7 +4891,7 @@ dependencies = [ "serde", "shadowsocks", "socket2", - "spin 0.9.8", + "spin", "thiserror 1.0.59", "tokio", "windows-sys 0.59.0", @@ -5411,7 +4934,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", "rand_core 0.6.4", ] @@ -5464,12 +4986,6 @@ dependencies = [ [[package]] name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" @@ -5539,7 +5055,7 @@ checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ "proc-macro2 0.4.30", "quote 0.6.13", - "unicode-xid 0.1.0", + "unicode-xid", ] [[package]] @@ -6389,15 +5905,6 @@ dependencies = [ ] [[package]] -name = "twofish" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" -dependencies = [ - "cipher", -] - -[[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6447,12 +5954,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" [[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" @@ -7305,17 +6806,6 @@ dependencies = [ ] [[package]] -name = "x448" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb" -dependencies = [ - "ed448-goldilocks", - "hex", - "rand_core 0.5.1", -] - -[[package]] name = "yoke" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/installer-downloader/src/controller.rs b/installer-downloader/src/controller.rs index 058b5049b9..773ed1d3bd 100644 --- a/installer-downloader/src/controller.rs +++ b/installer-downloader/src/controller.rs @@ -5,7 +5,7 @@ use crate::resource; use crate::ui_downloader::{UiAppDownloader, UiAppDownloaderParameters, UiProgressUpdater}; use mullvad_update::{ - api::{self, Version, VersionInfoProvider}, + api::{self, Version, VersionInfoProvider, VersionParameters}, app::{self, AppDownloader}, }; @@ -24,14 +24,14 @@ pub struct AppController {} /// Public entry function for registering a [AppDelegate]. pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T) { - use mullvad_update::{api::LatestVersionInfoProvider, app::HttpAppDownloader}; + use mullvad_update::{api::ApiVersionInfoProvider, app::HttpAppDownloader}; - // App downloader (factory) to use - type DownloaderFactory<T> = HttpAppDownloader<UiProgressUpdater<T>, UiProgressUpdater<T>>; + // App downloader to use + type Downloader<T> = HttpAppDownloader<UiProgressUpdater<T>>; // Version info provider to use - type VersionInfoProvider = LatestVersionInfoProvider; + type VersionInfoProvider = ApiVersionInfoProvider; - AppController::initialize::<_, DownloaderFactory<T>, VersionInfoProvider>(delegate) + AppController::initialize::<_, Downloader<T>, VersionInfoProvider>(delegate) } impl AppController { @@ -85,8 +85,15 @@ where let queue = delegate.queue(); async move { + let version_params = VersionParameters { + // TODO: detect current architecture + architecture: api::VersionArchitecture::X86, + // For the downloader, the rollout version is always preferred + rollout: 1., + }; + // TODO: handle errors, retry - let Ok(version_info) = VersionProvider::get_version_info().await else { + let Ok(version_info) = VersionProvider::get_version_info(version_params).await else { queue.queue_main(move |self_| { self_.set_status_text("Failed to fetch version info"); }); @@ -134,9 +141,7 @@ where let Some(app_url) = version_info.stable.urls.first() else { return; }; - let Some(signature_url) = version_info.stable.signature_urls.first() else { - return; - }; + let app_sha256 = version_info.stable.sha256; let app_size = version_info.stable.size; self_.set_download_text(""); @@ -147,11 +152,10 @@ where self_.show_download_progress(); let downloader = A::from(UiAppDownloaderParameters { - signature_url: signature_url.to_owned(), app_url: app_url.to_owned(), app_size, - sig_progress: UiProgressUpdater::new(self_.queue()), app_progress: UiProgressUpdater::new(self_.queue()), + app_sha256, }); let ui_downloader = UiAppDownloader::new(self_, downloader); diff --git a/installer-downloader/src/ui_downloader.rs b/installer-downloader/src/ui_downloader.rs index 41b6fa3efb..18eeb0074d 100644 --- a/installer-downloader/src/ui_downloader.rs +++ b/installer-downloader/src/ui_downloader.rs @@ -19,8 +19,7 @@ pub struct UiAppDownloader<Delegate: AppDelegate, Downloader> { } /// Parameters for [UiAppDownloader] -pub type UiAppDownloaderParameters<Delegate> = - AppDownloaderParameters<UiProgressUpdater<Delegate>, UiProgressUpdater<Delegate>>; +pub type UiAppDownloaderParameters<Delegate> = AppDownloaderParameters<UiProgressUpdater<Delegate>>; impl<Delegate: AppDelegate, Downloader: AppDownloader + Send + 'static> UiAppDownloader<Delegate, Downloader> diff --git a/installer-downloader/tests/controller.rs b/installer-downloader/tests/controller.rs index 83ed08edb8..5f227bbdb2 100644 --- a/installer-downloader/tests/controller.rs +++ b/installer-downloader/tests/controller.rs @@ -8,7 +8,7 @@ use insta::assert_yaml_snapshot; use installer_downloader::controller::AppController; use installer_downloader::delegate::{AppDelegate, AppDelegateQueue}; use installer_downloader::ui_downloader::UiAppDownloaderParameters; -use mullvad_update::api::{Version, VersionInfo, VersionInfoProvider}; +use mullvad_update::api::{Version, VersionInfo, VersionInfoProvider, VersionParameters}; use mullvad_update::app::{AppDownloader, DownloadError}; use mullvad_update::fetch::ProgressUpdater; use std::sync::{Arc, LazyLock, Mutex}; @@ -19,32 +19,33 @@ pub struct FakeVersionInfoProvider {} static FAKE_VERSION: LazyLock<VersionInfo> = LazyLock::new(|| VersionInfo { stable: Version { - version: "2025.1".to_owned(), + version: "2025.1".parse().unwrap(), urls: vec!["https://mullvad.net/fakeapp".to_owned()], size: 1234, - signature_urls: vec!["https://mullvad.net/fakesig".to_owned()], + changelog: "a changelog".to_owned(), + sha256: [0u8; 32], }, beta: None, }); #[async_trait::async_trait] impl VersionInfoProvider for FakeVersionInfoProvider { - async fn get_version_info() -> anyhow::Result<VersionInfo> { + async fn get_version_info(_params: VersionParameters) -> anyhow::Result<VersionInfo> { Ok(FAKE_VERSION.clone()) } } /// Downloader for which all steps immediately succeed -pub type FakeAppDownloaderHappyPath = FakeAppDownloader<true, true, true>; +pub type FakeAppDownloaderHappyPath = FakeAppDownloader<true, true>; /// Downloader for which the download step fails -pub type FakeAppDownloaderDownloadFail = FakeAppDownloader<true, false, false>; +pub type FakeAppDownloaderDownloadFail = FakeAppDownloader<false, false>; -/// Downloader for which all but the final verification step succeed -pub type FakeAppDownloaderVerifyFail = FakeAppDownloader<true, true, false>; +/// Downloader for which the final verification step fails +pub type FakeAppDownloaderVerifyFail = FakeAppDownloader<true, false>; -impl<const A: bool, const B: bool, const C: bool> From<UiAppDownloaderParameters<FakeAppDelegate>> - for FakeAppDownloader<A, B, C> +impl<const A: bool, const B: bool> From<UiAppDownloaderParameters<FakeAppDelegate>> + for FakeAppDownloader<A, B> { fn from(params: UiAppDownloaderParameters<FakeAppDelegate>) -> Self { FakeAppDownloader { params } @@ -54,32 +55,18 @@ impl<const A: bool, const B: bool, const C: bool> From<UiAppDownloaderParameters /// Fake app downloader /// /// Parameters: -/// * SIG_SUCCEED - whether fetching the signature succeeds /// * EXE_SUCCEED - whether fetching the binary succeeds -/// * VERIFY_SUCCEED - whether verifying the signature succeeds -pub struct FakeAppDownloader< - const SIG_SUCCEED: bool, - const EXE_SUCCEED: bool, - const VERIFY_SUCCEED: bool, -> { +/// * VERIFY_SUCCEED - whether verifying the binary succeeds +pub struct FakeAppDownloader<const EXE_SUCCEED: bool, const VERIFY_SUCCEED: bool> { params: UiAppDownloaderParameters<FakeAppDelegate>, } #[async_trait::async_trait] -impl<const SIG_SUCCEED: bool, const EXE_SUCCEED: bool, const VERIFY_SUCCEED: bool> AppDownloader - for FakeAppDownloader<SIG_SUCCEED, EXE_SUCCEED, VERIFY_SUCCEED> +impl<const EXE_SUCCEED: bool, const VERIFY_SUCCEED: bool> AppDownloader + for FakeAppDownloader<EXE_SUCCEED, VERIFY_SUCCEED> { async fn download_signature(&mut self) -> Result<(), DownloadError> { - self.params.sig_progress.set_url(&self.params.signature_url); - self.params.sig_progress.set_progress(0.); - if SIG_SUCCEED { - self.params.sig_progress.set_progress(1.); - Ok(()) - } else { - Err(DownloadError::FetchSignature(anyhow::anyhow!( - "fetching signature failed" - ))) - } + Ok(()) } async fn download_executable(&mut self) -> Result<(), DownloadError> { diff --git a/installer-downloader/tests/snapshots/controller__download-3.snap b/installer-downloader/tests/snapshots/controller__download-3.snap index 140e6d6320..d7c86160de 100644 --- a/installer-downloader/tests/snapshots/controller__download-3.snap +++ b/installer-downloader/tests/snapshots/controller__download-3.snap @@ -33,10 +33,6 @@ call_log: - "set_download_text: Downloading from mullvad.net... (0%)" - "set_download_progress: 100" - "set_download_text: Downloading from mullvad.net... (100%)" - - "set_download_progress: 0" - - "set_download_text: Downloading from mullvad.net... (0%)" - - "set_download_progress: 100" - - "set_download_text: Downloading from mullvad.net... (100%)" - "set_download_text: Download complete. Verifying..." - disable_cancel_button - "set_download_text: Verification successful. Starting install..." diff --git a/installer-downloader/tests/snapshots/controller__failed_verification.snap b/installer-downloader/tests/snapshots/controller__failed_verification.snap index bb92678de9..d345795b7e 100644 --- a/installer-downloader/tests/snapshots/controller__failed_verification.snap +++ b/installer-downloader/tests/snapshots/controller__failed_verification.snap @@ -33,10 +33,6 @@ call_log: - "set_download_text: Downloading from mullvad.net... (0%)" - "set_download_progress: 100" - "set_download_text: Downloading from mullvad.net... (100%)" - - "set_download_progress: 0" - - "set_download_text: Downloading from mullvad.net... (0%)" - - "set_download_progress: 100" - - "set_download_text: Downloading from mullvad.net... (100%)" - "set_download_text: Download complete. Verifying..." - disable_cancel_button - "set_download_text: ERROR: Verification failed!" diff --git a/mullvad-update/Cargo.toml b/mullvad-update/Cargo.toml index f3a067c3af..f858f0d917 100644 --- a/mullvad-update/Cargo.toml +++ b/mullvad-update/Cargo.toml @@ -13,13 +13,13 @@ workspace = true [dependencies] anyhow = "1.0" json-canon = "0.1" -chrono = { workspace = true, features = ["serde"] } -ed25519-dalek = { version = "2.1", default-features = false } -hex = { version = "0.4", default-features = false } -pgp = "0.14.0" +chrono = { workspace = true, features = ["serde", "now"] } +ed25519-dalek = { version = "2.1" } +hex = { version = "0.4" } reqwest = { version = "0.12.9", features = ["blocking", "json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +sha2 = "0.10" tokio = { version = "1", features = ["full"] } async-trait = "0.1" diff --git a/mullvad-update/src/api.rs b/mullvad-update/src/api.rs index 16e578d485..d4fbd0e91c 100644 --- a/mullvad-update/src/api.rs +++ b/mullvad-update/src/api.rs @@ -1,10 +1,32 @@ //! Fetch information about app versions from the Mullvad API +use anyhow::Context; + +use crate::deserializer; + +/// Parameters for [VersionInfoProvider] +#[derive(Debug)] +pub struct VersionParameters { + /// Architecture to retrieve data for + pub architecture: VersionArchitecture, + /// Rollout threshold. Any version in the response below this threshold will be ignored + pub rollout: f32, +} + +/// Architecture to retrieve data for +#[derive(Debug, Clone, Copy)] +pub enum VersionArchitecture { + /// x86-64 architecture + X86, + /// ARM64 architecture + Arm64, +} + /// See [module-level](self) docs. #[async_trait::async_trait] pub trait VersionInfoProvider { /// Return info about the stable version - async fn get_version_info() -> anyhow::Result<VersionInfo>; + async fn get_version_info(params: VersionParameters) -> anyhow::Result<VersionInfo>; } /// Contains information about all versions @@ -20,39 +42,122 @@ pub struct VersionInfo { #[derive(Debug, Clone)] pub struct Version { /// Version - pub version: String, + pub version: mullvad_version::Version, /// URLs to use for downloading the app installer pub urls: Vec<String>, /// Size of installer, in bytes pub size: usize, - /// URLs pointing to app PGP signatures - pub signature_urls: Vec<String>, + /// Version changelog + pub changelog: String, + /// App installer checksum + pub sha256: [u8; 32], } -/// Use hardcoded URL to fetch installer -/// TODO: This is temporary -pub struct LatestVersionInfoProvider; +/// Obtain version data from the Mullvad API +pub struct ApiVersionInfoProvider; #[async_trait::async_trait] -impl VersionInfoProvider for LatestVersionInfoProvider { - async fn get_version_info() -> anyhow::Result<VersionInfo> { - Ok(VersionInfo { - stable: Version { - version: "2025.3".to_string(), - urls: vec!["https://mullvad.net/en/download/app/exe/latest".to_owned()], - size: 200 * 1024 * 1024, - signature_urls: vec![ - "https://mullvad.net/en/download/app/exe/latest/signature".to_owned() - ], - }, - beta: Some(Version { - version: "2025.3-beta1".to_string(), - urls: vec!["https://mullvad.net/en/download/app/exe/latest-beta".to_owned()], - size: 200 * 1024 * 1024, - signature_urls: vec![ - "https://mullvad.net/en/download/app/exe/latest-beta/signature".to_owned(), - ], - }), +impl VersionInfoProvider for ApiVersionInfoProvider { + async fn get_version_info(params: VersionParameters) -> anyhow::Result<VersionInfo> { + // FIXME: Replace with actual API response + use deserializer::*; + + const TEST_PUBKEY: &str = + "AEC24A08466F3D6A1EDCDB2AD3C234428AB9D991B6BEA7F53CB9F172E6CB40D8"; + let pubkey = hex::decode(TEST_PUBKEY).unwrap(); + let verifying_key = + ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap(); + + let response = SignedResponse::deserialize_and_verify( + VerifyingKey(verifying_key), + include_bytes!("../test-version-response.json"), + )?; + + VersionInfo::try_from_signed_response(¶ms, response) + } +} + +impl VersionInfo { + /// Convert signed response data to public version type + /// NOTE: `response` is assumed to be verified and untampered. It is not verified. + fn try_from_signed_response( + params: &VersionParameters, + response: deserializer::SignedResponse, + ) -> anyhow::Result<Self> { + let stable = Version::try_from_signed_response(params, response.signed.stable)?; + let beta = response + .signed + .beta + .map(|response| Version::try_from_signed_response(params, response)) + .transpose() + .context("Failed to parse beta version")?; + + Ok(Self { stable, beta }) + } +} + +impl Version { + /// Convert response data to public version type + fn try_from_signed_response( + params: &VersionParameters, + response: deserializer::VersionResponse, + ) -> anyhow::Result<Self> { + // Check if the rollout version is acceptable according to threshold + if let Some(next) = response.next { + if next.rollout >= params.rollout { + // Use the version being rolled out + return Self::try_for_arch(params, next.version); + } + } + + // Return the version not being rolled out + Self::try_for_arch(params, response.current) + } + + /// Convert version response to the public version type for a given architecture + /// This may fail if the current architecture isn't included in the response + fn try_for_arch( + params: &VersionParameters, + response: deserializer::SpecificVersionResponse, + ) -> anyhow::Result<Self> { + let installer = match params.architecture { + VersionArchitecture::X86 => response.installers.x86, + VersionArchitecture::Arm64 => response.installers.arm64, + }; + let installer = installer.context("Installer missing for architecture")?; + let sha256 = hex::decode(installer.sha256) + .context("Invalid checksum hex")? + .try_into() + .map_err(|_| anyhow::anyhow!("Invalid checksum length"))?; + + Ok(Self { + changelog: response.changelog, + version: response.version, + urls: installer.urls, + size: installer.size, + sha256, }) } } + +#[cfg(test)] +mod test { + use super::*; + + /// Test API version responses can be parsed + #[test] + fn test_api_version_info_provider_parser() -> anyhow::Result<()> { + let response = deserializer::SignedResponse::deserialize_and_verify_insecure( + include_bytes!("../test-version-response.json"), + )?; + + let params = VersionParameters { + architecture: VersionArchitecture::X86, + rollout: 1., + }; + + VersionInfo::try_from_signed_response(¶ms, response)?; + + Ok(()) + } +} diff --git a/mullvad-update/src/app.rs b/mullvad-update/src/app.rs index 5aaeb1b7cc..ef08971bb0 100644 --- a/mullvad-update/src/app.rs +++ b/mullvad-update/src/app.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use crate::{ fetch::{self, ProgressUpdater}, - verify::{AppVerifier, PgpVerifier}, + verify::{AppVerifier, Sha256Verifier}, }; #[derive(Debug)] @@ -15,12 +15,12 @@ pub enum DownloadError { } /// Parameters required to construct an [AppDownloader]. -pub struct AppDownloaderParameters<SigProgress, AppProgress> { - pub signature_url: String, +#[derive(Clone)] +pub struct AppDownloaderParameters<AppProgress> { pub app_url: String, pub app_size: usize, - pub sig_progress: SigProgress, pub app_progress: AppProgress, + pub app_sha256: [u8; 32], } /// See the [module-level documentation](self). @@ -44,63 +44,40 @@ pub async fn install_and_upgrade(mut downloader: impl AppDownloader) -> Result<( } #[derive(Clone)] -pub struct HttpAppDownloader<SigProgress, AppProgress> { - signature_url: String, - app_url: String, - app_size: usize, - signature_progress_updater: SigProgress, - app_progress_updater: AppProgress, +pub struct HttpAppDownloader<AppProgress> { + params: AppDownloaderParameters<AppProgress>, // TODO: set permissions tmp_dir: PathBuf, } -impl<SigProgress, AppProgress> HttpAppDownloader<SigProgress, AppProgress> { - const MAX_SIGNATURE_SIZE: usize = 1024; - - pub fn new(parameters: AppDownloaderParameters<SigProgress, AppProgress>) -> Self { +impl<AppProgress> HttpAppDownloader<AppProgress> { + pub fn new(params: AppDownloaderParameters<AppProgress>) -> Self { let tmp_dir = std::env::temp_dir(); - Self { - signature_url: parameters.signature_url, - app_url: parameters.app_url, - app_size: parameters.app_size, - signature_progress_updater: parameters.sig_progress, - app_progress_updater: parameters.app_progress, - tmp_dir, - } + Self { params, tmp_dir } } } -impl<SigProgress: ProgressUpdater, AppProgress: ProgressUpdater> - From<AppDownloaderParameters<SigProgress, AppProgress>> - for HttpAppDownloader<SigProgress, AppProgress> +impl<AppProgress: ProgressUpdater> From<AppDownloaderParameters<AppProgress>> + for HttpAppDownloader<AppProgress> { - fn from(parameters: AppDownloaderParameters<SigProgress, AppProgress>) -> Self { + fn from(parameters: AppDownloaderParameters<AppProgress>) -> Self { HttpAppDownloader::new(parameters) } } #[async_trait::async_trait] -impl<SigProgress: ProgressUpdater, AppProgress: ProgressUpdater> AppDownloader - for HttpAppDownloader<SigProgress, AppProgress> -{ +impl<AppProgress: ProgressUpdater> AppDownloader for HttpAppDownloader<AppProgress> { async fn download_signature(&mut self) -> Result<(), DownloadError> { - fetch::get_to_file( - self.sig_path(), - &self.signature_url, - &mut self.signature_progress_updater, - fetch::SizeHint::Maximum(Self::MAX_SIGNATURE_SIZE), - ) - .await - .map_err(DownloadError::FetchSignature) + // TODO: no-op, remove + Ok(()) } async fn download_executable(&mut self) -> Result<(), DownloadError> { fetch::get_to_file( self.bin_path(), - &self.app_url, - &mut self.app_progress_updater, - // FIXME: use exact size hint - fetch::SizeHint::Maximum(self.app_size), + &self.params.app_url, + &mut self.params.app_progress, + fetch::SizeHint::Exact(self.params.app_size), ) .await .map_err(DownloadError::FetchApp) @@ -108,21 +85,19 @@ impl<SigProgress: ProgressUpdater, AppProgress: ProgressUpdater> AppDownloader async fn verify(&mut self) -> Result<(), DownloadError> { let bin_path = self.bin_path(); - let sig_path = self.sig_path(); - tokio::task::spawn_blocking(move || { - PgpVerifier::verify(bin_path, sig_path).map_err(DownloadError::Verification) - }) - .await - .expect("verifier panicked") + let hash = self.hash_sha256(); + Sha256Verifier::verify(bin_path, *hash) + .await + .map_err(DownloadError::Verification) } } -impl<SigProgress, AppProgress> HttpAppDownloader<SigProgress, AppProgress> { +impl<AppProgress> HttpAppDownloader<AppProgress> { fn bin_path(&self) -> PathBuf { self.tmp_dir.join("temp.exe") } - fn sig_path(&self) -> PathBuf { - self.tmp_dir.join("temp.exe.sig") + fn hash_sha256(&self) -> &[u8; 32] { + &self.params.app_sha256 } } diff --git a/mullvad-update/src/deserializer.rs b/mullvad-update/src/deserializer.rs index 5ddb4a45d8..5f1a0326b8 100644 --- a/mullvad-update/src/deserializer.rs +++ b/mullvad-update/src/deserializer.rs @@ -30,6 +30,20 @@ impl SignedResponse { Self::deserialize_and_verify_at_time(key, bytes, chrono::Utc::now()) } + /// This method is used for testing, and skips all verification. + /// Own method to prevent accidental misuse. + #[cfg(test)] + pub fn deserialize_and_verify_insecure(bytes: &[u8]) -> Result<Self, anyhow::Error> { + let partial_data: PartialSignedResponse = + serde_json::from_slice(bytes).context("Invalid version JSON")?; + let signed = serde_json::from_value(partial_data.signed) + .context("Failed to deserialize response")?; + Ok(Self { + signature: partial_data.signature, + signed, + }) + } + /// Deserialize some bytes to JSON, and verify them, including signature and expiry. /// If successful, the deserialized data is returned. fn deserialize_and_verify_at_time( @@ -193,7 +207,7 @@ pub struct SpecificVersionArchitectureResponse { pub urls: Vec<String>, /// Size of the installer, in bytes pub size: usize, - /// TODO: hash of the installer, in bytes + /// Hash of the installer, hexadecimal string pub sha256: String, } diff --git a/mullvad-update/src/format/mod.rs b/mullvad-update/src/format/mod.rs new file mode 100644 index 0000000000..98428babf4 --- /dev/null +++ b/mullvad-update/src/format/mod.rs @@ -0,0 +1,91 @@ +use serde::{Deserialize, Serialize}; + +pub mod deserializer; +pub mod key; +#[cfg(feature = "sign")] +pub mod serializer; + +/// JSON response including signature and signed content +/// This type does not implement [serde::Deserialize] to prevent accidental deserialization without +/// signature verification. +#[derive(Serialize)] +pub struct SignedResponse { + /// Signature of the canonicalized JSON of `signed` + pub signature: ResponseSignature, + /// Content signed by `signature` + pub signed: Response, +} + +/// Helper class that leaves the signed data untouched +/// Note that deserializing doesn't verify anything +#[derive(Deserialize, Serialize)] +struct PartialSignedResponse { + /// Signature of the canonicalized JSON of `signed` + pub signature: ResponseSignature, + /// Content signed by `signature` + pub signed: serde_json::Value, +} + +/// Signed JSON response, not including the signature +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Response { + /// When the signature expires + pub expires: chrono::DateTime<chrono::Utc>, + /// Stable version response + pub stable: VersionResponse, + /// Beta version response + pub beta: Option<VersionResponse>, +} + +#[derive(Deserialize, Serialize)] +pub struct VersionResponse { + /// The current version in this channel + pub current: SpecificVersionResponse, + /// The version being rolled out in this channel + pub next: Option<NextSpecificVersionResponse>, +} + +#[derive(Deserialize, Serialize)] +pub struct NextSpecificVersionResponse { + /// The percentage of users that should receive the new version. + pub rollout: f32, + #[serde(flatten)] + pub version: SpecificVersionResponse, +} + +#[derive(Deserialize, Serialize)] +pub struct SpecificVersionResponse { + /// Mullvad app version + pub version: mullvad_version::Version, + /// Changelog entries + pub changelog: String, + /// Installer details for different architectures + pub installers: SpecificVersionArchitectureResponses, +} + +/// Version details for supported architectures +#[derive(Deserialize, Serialize)] +pub struct SpecificVersionArchitectureResponses { + /// Details for x86 installer + pub x86: Option<SpecificVersionArchitectureResponse>, + /// Details for ARM64 installer + pub arm64: Option<SpecificVersionArchitectureResponse>, +} + +#[derive(Deserialize, Serialize)] +pub struct SpecificVersionArchitectureResponse { + /// Mirrors that host the artifact + pub urls: Vec<String>, + /// Size of the installer, in bytes + pub size: usize, + /// Hash of the installer, hexadecimal string + pub sha256: String, +} + +/// JSON response signature +#[derive(Deserialize, Serialize)] +pub struct ResponseSignature { + pub keyid: key::VerifyingKey, + pub sig: key::Signature, +} diff --git a/mullvad-update/src/verify.rs b/mullvad-update/src/verify.rs index 74d8790c4b..a6bc3c9bc0 100644 --- a/mullvad-update/src/verify.rs +++ b/mullvad-update/src/verify.rs @@ -1,61 +1,80 @@ -use std::{fs::File, io::BufReader, path::Path}; - use anyhow::Context; -use pgp::{ - armor, - packet::{Packet, PacketParser}, - types::PublicKeyTrait, - Deserializable, SignedPublicKey, +use sha2::Digest; +use tokio::{ + fs, + io::{AsyncRead, AsyncReadExt, BufReader}, }; -/// A verifier of digital file signatures +use std::{future::Future, path::Path}; + +/// A verifier of digital file signatures or hashes pub trait AppVerifier: 'static + Clone { - /// Verify `bin_path` using the signature at `sig_path`, and return an error if this fails for - /// any reason. - fn verify(bin_path: impl AsRef<Path>, sig_path: impl AsRef<Path>) -> anyhow::Result<()>; + type Parameters; + + /// Verify `bin_path` using `parameters`, and return an error if this fails for any reason. + fn verify( + bin_path: impl AsRef<Path>, + parameters: Self::Parameters, + ) -> impl Future<Output = anyhow::Result<()>>; } -/// Verification using pgp +/// Checksum verifier that uses SHA256 #[derive(Clone)] -pub struct PgpVerifier; +pub struct Sha256Verifier; -impl PgpVerifier { - const SIGNING_PUBKEY: &[u8] = include_bytes!("../mullvad-code-signing.gpg"); +impl Sha256Verifier { + /// Maximum number of bytes to read at a time + const BUF_SIZE: usize = 1024 * 1024; } -impl AppVerifier for PgpVerifier { - fn verify(bin_path: impl AsRef<Path>, sig_path: impl AsRef<Path>) -> anyhow::Result<()> { - let pubkey = SignedPublicKey::from_bytes(Self::SIGNING_PUBKEY)?; +impl AppVerifier for Sha256Verifier { + /// The checksum + type Parameters = [u8; 32]; + + fn verify( + bin_path: impl AsRef<Path>, + expected_hash: Self::Parameters, + ) -> impl Future<Output = anyhow::Result<()>> { + let bin_path = bin_path.as_ref().to_owned(); + + async move { + let file = fs::File::open(&bin_path) + .await + .context(format!("Failed to open file at {}", bin_path.display()))?; + let file = BufReader::new(file); + + Self::verify_inner(file, expected_hash).await + } + } +} - let sig_reader = BufReader::new(File::open(sig_path).context("Open signature file")?); - let signature = PacketParser::new(armor::Dearmor::new(sig_reader)) - .find_map(|packet| { - if let Ok(Packet::Signature(sig)) = packet { - Some(sig) - } else { - None - } - }) - .context("Missing signature")?; - let issuer = signature - .issuer() - .into_iter() - .next() - .context("Find issuer key ID")?; +impl Sha256Verifier { + async fn verify_inner( + mut reader: impl AsyncRead + Unpin, + expected_hash: [u8; 32], + ) -> anyhow::Result<()> { + let mut hasher = sha2::Sha256::new(); - // Find subkey used for signing - let subkey = pubkey - .public_subkeys - .iter() - .find(|subkey| &subkey.key_id() == issuer) - .context("Find signing subkey")?; - //subkey.verify(&pubkey)?; + // Read data into hasher + let mut buffer = vec![0u8; Self::BUF_SIZE]; + loop { + let read_n = reader + .read(&mut buffer) + .await + .context("Error reading bin file")?; + if read_n == 0 { + // We're done + break; + } + hasher.update(&buffer[..read_n]); + } - let bin = BufReader::with_capacity(1024 * 1024, File::open(bin_path)?); + let actual_hash = hasher.finalize(); - signature - .verify(subkey, bin) - .context("Verification failed")?; + // Verify that hash is correct + if expected_hash != actual_hash[..] { + anyhow::bail!("Invalid checksum for bin file"); + } Ok(()) } @@ -63,10 +82,32 @@ impl AppVerifier for PgpVerifier { #[cfg(test)] mod test { + use rand::RngCore; + use std::io::Cursor; + use super::*; - #[test] - fn test_pgp_signing_pubkey() { - SignedPublicKey::from_bytes(PgpVerifier::SIGNING_PUBKEY).unwrap(); + #[tokio::test] + async fn test_sha256_checksum() { + // Generate some random data + let mut data = vec![0u8; 1024 * 1024]; + rand::thread_rng().fill_bytes(&mut data); + + // Hash it + let mut hasher = sha2::Sha256::new(); + hasher.update(&data); + let expected_hash = hasher.finalize(); + let expected_hash: [u8; 32] = expected_hash[..].try_into().unwrap(); + + // Same data should be accepted + Sha256Verifier::verify_inner(Cursor::new(&data), expected_hash) + .await + .expect("expected checksum match"); + + // Compare the hash against some random data, which should fail + rand::thread_rng().fill_bytes(&mut data); + Sha256Verifier::verify_inner(Cursor::new(&data), expected_hash) + .await + .expect_err("expected checksum mismatch"); } } |
