From 44a858ace260e720c7e25fdee4f90cfeec32d52c Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Tue, 29 Sep 2026 08:02:43 +0530 Subject: [PATCH] feat(bootstrap): implement APK signature validation and targeted binary extraction --- crates/molotov-bootstrap/src/apk.rs | 104 +++++++++++++++++++++ crates/molotov-bootstrap/src/lib.rs | 1 + crates/molotov-bootstrap/tests/apk_test.rs | 15 +++ 3 files changed, 120 insertions(+) create mode 100644 crates/molotov-bootstrap/src/apk.rs create mode 100644 crates/molotov-bootstrap/tests/apk_test.rs diff --git a/crates/molotov-bootstrap/src/apk.rs b/crates/molotov-bootstrap/src/apk.rs new file mode 100644 index 0000000..1a19e6a --- /dev/null +++ b/crates/molotov-bootstrap/src/apk.rs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::error::MolotovError; +use sha2::{Digest, Sha256}; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use zip::ZipArchive; + +pub const ROBLOX_CERT_FINGERPRINT: &str = + "1c:00:81:4e:66:35:b0:b3:f1:ff:37:37:3e:38:29:b9:28:90:54:e5:cf:51:65:70:e4:65:fc:e0:bc:95:68:5b"; + +pub struct ApkExtractor<'a> { + apk_path: &'a Path, +} + +impl<'a> ApkExtractor<'a> { + pub fn new(apk_path: &'a Path) -> Self { + Self { apk_path } + } + + pub fn validate_fingerprint(fingerprint: &str) -> Result<(), MolotovError> { + let normalized_expected = ROBLOX_CERT_FINGERPRINT.to_lowercase().replace(':', ""); + let normalized_actual = fingerprint.to_lowercase().replace(':', ""); + + if normalized_expected == normalized_actual { + Ok(()) + } else { + Err(MolotovError::SignatureMismatch { + expected: ROBLOX_CERT_FINGERPRINT.to_string(), + found: fingerprint.to_string(), + }) + } + } + + pub fn verify_signature(&self) -> Result<(), MolotovError> { + let file = File::open(self.apk_path)?; + let mut archive = ZipArchive::new(file) + .map_err(|e| MolotovError::ApkMalformed(format!("Failed to open APK zip: {}", e)))?; + + // Locate .RSA or .DSA certificate file in META-INF + let mut cert_data = None; + for i in 0..archive.len() { + let file = archive.by_index(i) + .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; + let name = file.name(); + if name.starts_with("META-INF/") && (name.ends_with(".RSA") || name.ends_with(".DSA") || name.ends_with(".EC")) { + let mut data = Vec::new(); + let mut reader = file; + reader.read_to_end(&mut data)?; + cert_data = Some(data); + break; + } + } + + let data = cert_data.ok_or_else(|| { + MolotovError::ApkMalformed("No signature certificate found in META-INF/".into()) + })?; + + // Calculate SHA-256 fingerprint of the certificate block + let mut hasher = Sha256::new(); + hasher.update(&data); + let digest = hasher.finalize(); + let computed_fingerprint = digest + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(":"); + + Self::validate_fingerprint(&computed_fingerprint) + } + + pub fn extract_target_binaries(&self, destination_dir: &Path) -> Result { + let file = File::open(self.apk_path)?; + let mut archive = ZipArchive::new(file) + .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; + + std::fs::create_dir_all(destination_dir)?; + let mut libroblox_path = None; + + for i in 0..archive.len() { + let mut file = archive.by_index(i) + .map_err(|e| MolotovError::ApkMalformed(e.to_string()))?; + let name = file.name().to_string(); + + if name == "lib/x86_64/libroblox.so" { + let out_path = destination_dir.join("libroblox.so"); + let mut outfile = File::create(&out_path)?; + std::io::copy(&mut file, &mut outfile)?; + libroblox_path = Some(out_path); + } else if name.starts_with("assets/") && !name.ends_with('/') { + let out_path = destination_dir.join(&name); + if let Some(parent) = out_path.parent() { + std::fs::create_dir_all(parent)?; + } + let mut outfile = File::create(&out_path)?; + std::io::copy(&mut file, &mut outfile)?; + } + } + + libroblox_path.ok_or_else(|| { + MolotovError::ApkMalformed("lib/x86_64/libroblox.so not found in APK".into()) + }) + } +} diff --git a/crates/molotov-bootstrap/src/lib.rs b/crates/molotov-bootstrap/src/lib.rs index 31752fe..3434e32 100644 --- a/crates/molotov-bootstrap/src/lib.rs +++ b/crates/molotov-bootstrap/src/lib.rs @@ -1,3 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later +pub mod apk; pub mod cdn; pub mod error; diff --git a/crates/molotov-bootstrap/tests/apk_test.rs b/crates/molotov-bootstrap/tests/apk_test.rs new file mode 100644 index 0000000..a8aadb3 --- /dev/null +++ b/crates/molotov-bootstrap/tests/apk_test.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use molotov_bootstrap::apk::{ApkExtractor, ROBLOX_CERT_FINGERPRINT}; + +#[test] +fn test_verify_cert_fingerprint_mismatch() { + let dummy_fingerprint = "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"; + let res = ApkExtractor::validate_fingerprint(dummy_fingerprint); + assert!(res.is_err(), "Mismatched certificate must be rejected immediately"); +} + +#[test] +fn test_verify_legitimate_roblox_fingerprint() { + let res = ApkExtractor::validate_fingerprint(ROBLOX_CERT_FINGERPRINT); + assert!(res.is_ok(), "Roblox official certificate fingerprint must be accepted"); +}