feat: verify signed launcher manifests
This commit is contained in:
+16
-3
@@ -1,8 +1,21 @@
|
||||
# ShaCraft manifest v1
|
||||
|
||||
Каждая игровая сборка описывается одним JSON-документом. Лаунчер принимает
|
||||
документ только после проверки подписи релизного ключа и затем валидирует эту
|
||||
схему до начала загрузки.
|
||||
Каждая игровая сборка описывается одним JSON-документом. API передаёт его в
|
||||
Ed25519-конверте: `payload` — base64 от UTF-8 JSON, `signature` — подпись
|
||||
точно этих байтов. Лаунчер принимает документ только после проверки
|
||||
встроенного публичного релизного ключа и затем валидирует эту схему до начала
|
||||
загрузки.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"keyId": "2026-09-06",
|
||||
"payload": "eyJkaXNwbGF5TmFtZSI6Ii4uLiJ9",
|
||||
"signature": "base64-ed25519-signature"
|
||||
}
|
||||
```
|
||||
|
||||
После проверки и декодирования `payload` имеет следующий вид:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
Generated
+123
-4
@@ -94,6 +94,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
@@ -323,7 +329,7 @@ checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.1",
|
||||
"rand_core",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -348,6 +354,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.2"
|
||||
@@ -489,6 +501,33 @@ version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
@@ -565,6 +604,16 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
@@ -738,6 +787,30 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "3.0.11"
|
||||
@@ -790,6 +863,12 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
@@ -1081,7 +1160,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core",
|
||||
"rand_core 0.10.1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -2279,6 +2358,16 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.34"
|
||||
@@ -2516,7 +2605,16 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2531,7 +2629,7 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3040,6 +3138,8 @@ dependencies = [
|
||||
name = "shacraft-launcher"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3055,6 +3155,15 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
@@ -3137,6 +3246,16 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
|
||||
@@ -16,6 +16,8 @@ tauri-build = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
base64 = "0.22"
|
||||
ed25519-dalek = "2"
|
||||
tauri = { version = "2", features = [] }
|
||||
url = "2"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||
|
||||
+30
-2
@@ -1,9 +1,22 @@
|
||||
use crate::manifest::{self, Manifest};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use reqwest::{blocking::Client, redirect::Policy};
|
||||
use serde::Deserialize;
|
||||
use std::{fmt, time::Duration};
|
||||
|
||||
const AERONAUTICS_MANIFEST: &str =
|
||||
"https://shacraft.ru/api/launcher/v2/profiles/aeronautics/manifest";
|
||||
"https://shacraft.ru/api/launcher/v2/profiles/aeronautics/signed-manifest";
|
||||
const MANIFEST_PUBLIC_KEY: &str = "2S3FRdZj4Xw5nJpZ3IhqVITBg3nTH9AtGSo1Ew9+qVQ=";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SignedManifest {
|
||||
schema_version: u32,
|
||||
key_id: String,
|
||||
payload: String,
|
||||
signature: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RemoteError {
|
||||
@@ -11,6 +24,7 @@ pub enum RemoteError {
|
||||
Network(reqwest::Error),
|
||||
Status(reqwest::StatusCode),
|
||||
TooLarge,
|
||||
InvalidSignature,
|
||||
InvalidManifest(manifest::ManifestError),
|
||||
}
|
||||
|
||||
@@ -21,6 +35,7 @@ impl fmt::Display for RemoteError {
|
||||
Self::Network(error) => write!(formatter, "Cannot load ShaCraft manifest: {error}"),
|
||||
Self::Status(status) => write!(formatter, "ShaCraft manifest request failed: {status}"),
|
||||
Self::TooLarge => formatter.write_str("ShaCraft manifest is too large"),
|
||||
Self::InvalidSignature => formatter.write_str("ShaCraft manifest signature is invalid"),
|
||||
Self::InvalidManifest(error) => write!(formatter, "ShaCraft manifest is invalid: {error}"),
|
||||
}
|
||||
}
|
||||
@@ -44,5 +59,18 @@ pub fn fetch_manifest(profile_id: &str) -> Result<Manifest, RemoteError> {
|
||||
return Err(RemoteError::TooLarge);
|
||||
}
|
||||
let source = response.text().map_err(RemoteError::Network)?;
|
||||
manifest::validate_json(&source).map_err(RemoteError::InvalidManifest)
|
||||
let envelope = serde_json::from_str::<SignedManifest>(&source)
|
||||
.map_err(|_| RemoteError::InvalidSignature)?;
|
||||
if envelope.schema_version != 1 || envelope.key_id != "2026-09-06" {
|
||||
return Err(RemoteError::InvalidSignature);
|
||||
}
|
||||
let payload = STANDARD.decode(envelope.payload).map_err(|_| RemoteError::InvalidSignature)?;
|
||||
let signature_bytes = STANDARD.decode(envelope.signature).map_err(|_| RemoteError::InvalidSignature)?;
|
||||
let public_key_bytes = STANDARD.decode(MANIFEST_PUBLIC_KEY).expect("embedded public key must be valid");
|
||||
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().expect("embedded public key must be 32 bytes"))
|
||||
.expect("embedded public key must be valid");
|
||||
let signature = Signature::from_slice(&signature_bytes).map_err(|_| RemoteError::InvalidSignature)?;
|
||||
public_key.verify(&payload, &signature).map_err(|_| RemoteError::InvalidSignature)?;
|
||||
let payload = String::from_utf8(payload).map_err(|_| RemoteError::InvalidSignature)?;
|
||||
manifest::validate_json(&payload).map_err(RemoteError::InvalidManifest)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user