Skip to content

Commit bd4d567

Browse files
authored
Merge pull request #66 from firefly-zero/provenance
Provenance checks
2 parents f02ddcc + 579508d commit bd4d567

3 files changed

Lines changed: 80 additions & 12 deletions

File tree

src/commands/build.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ static TIPS: &[&str] = &[
7474
pub fn cmd_build(vfs: PathBuf, args: &BuildArgs) -> anyhow::Result<()> {
7575
init_vfs(&vfs).context("init vfs")?;
7676
let config = Config::load(vfs, &args.root).context("load project config")?;
77-
if config.author_id == "joearms" {
78-
println!("⚠️ author_id in firefly.tom has the default value.");
79-
println!(" Please, change it before sharing the app with the world.");
80-
}
77+
check_provenance(&config);
8178
if !args.no_tip {
8279
show_tip();
8380
}
@@ -115,6 +112,18 @@ pub fn cmd_build(vfs: PathBuf, args: &BuildArgs) -> anyhow::Result<()> {
115112
Ok(())
116113
}
117114

115+
/// Emit warnings for suspicious config.
116+
fn check_provenance(c: &Config) {
117+
if c.author_id == "joearms" {
118+
println!("⚠️ author_id in firefly.toml has the default value.");
119+
println!(" Please, change it before sharing the app with the world.");
120+
}
121+
if (c.launcher || c.sudo) && c.author_id != "sys" {
122+
println!("⚠️ The app uses privileged system access. Make sure you trust the author.");
123+
}
124+
// TODO(@orsinium): Validate that "sys" apps are cloned from the official repos.
125+
}
126+
118127
/// Serialize and write the ROM meta information.
119128
fn write_meta(config: &Config) -> anyhow::Result<firefly_types::Meta<'_>> {
120129
use firefly_types::{Meta, validate_id, validate_name};

src/commands/import.rs

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,24 @@ pub fn cmd_import(vfs: &Path, args: &ImportArgs) -> Result<()> {
2828

2929
let meta_raw = read_meta_raw(&mut archive)?;
3030
let meta = Meta::decode(&meta_raw).context("parse meta")?;
31+
if !id_matches(&args.path, &meta) {
32+
bail!(
33+
"app ID ({}.{}) doesn't match the expected ID",
34+
meta.author_id,
35+
meta.app_id
36+
);
37+
}
38+
if (meta.launcher || meta.sudo) && meta.author_id != "sys" {
39+
println!("⚠️ The app uses privileged system access. Make sure you trust the author.");
40+
}
3141
let rom_path = vfs.join("roms").join(meta.author_id).join(meta.app_id);
3242

3343
init_vfs(vfs).context("init VFS")?;
3444
_ = fs::remove_dir_all(&rom_path);
3545
create_dir_all(&rom_path).context("create ROM dir")?;
3646
archive.extract(&rom_path).context("extract archive")?;
37-
if let Err(err) = verify(&rom_path) {
38-
println!("⚠️ verification failed: {err}");
47+
if let Err(err) = verify_hash(&rom_path) {
48+
println!("⚠️ hash verification failed: {err}");
3949
}
4050
create_data_dir(&meta, vfs).context("create app data directory")?;
4151
write_stats(&meta, vfs).context("create app stats file")?;
@@ -47,14 +57,35 @@ pub fn cmd_import(vfs: &Path, args: &ImportArgs) -> Result<()> {
4757
Ok(())
4858
}
4959

60+
/// Check if the ID from the ID/path/URL that the user provided matches the app ID in meta.
61+
///
62+
/// Currently verifies ID only if the app source is the catalog.
63+
/// For installation from URL/file we let the URL/file to have any name.
64+
fn id_matches(given: &str, meta: &Meta<'_>) -> bool {
65+
let is_catalog = !given.ends_with(".zip");
66+
if !is_catalog {
67+
return true;
68+
}
69+
if given == "launcher" {
70+
return meta.author_id == "sys" && meta.app_id == "launcher";
71+
}
72+
let full_id = format!("{}.{}", meta.author_id, meta.app_id);
73+
given == full_id
74+
}
75+
76+
/// Fetch the given app archive as a file.
77+
///
78+
/// * If file path is given, this path will be returned without any file modification.
79+
/// * If URL is given, the file will be downloaded.
80+
/// * If app ID is given, try downloading the app from the catalog.
5081
fn fetch_archive(path: &str) -> Result<PathBuf> {
51-
let mut path = path.to_string();
82+
let mut path = path;
5283
if path == "launcher" {
53-
path = "https://github.com/firefly-zero/firefly-launcher/releases/latest/download/sys.launcher.zip".to_string();
84+
path = "https://github.com/firefly-zero/firefly-launcher/releases/latest/download/sys.launcher.zip";
5485
}
86+
let mut path = path.to_string();
5587

5688
// App ID is given. Fetch download URL from the catalog.
57-
#[expect(clippy::case_sensitive_file_extension_comparisons)]
5889
if !path.ends_with(".zip") {
5990
let Some((author_id, app_id)) = path.split_once('.') else {
6091
bail!("app ID must contain dot");
@@ -90,6 +121,7 @@ fn fetch_archive(path: &str) -> Result<PathBuf> {
90121
Ok(out_path)
91122
}
92123

124+
/// Read and parse app metadata from the app archive.
93125
fn read_meta_raw(archive: &mut ZipArchive<File>) -> Result<Vec<u8>> {
94126
let mut meta_raw = Vec::new();
95127
let mut meta_file = if archive.index_for_name(META).is_some() {
@@ -134,14 +166,14 @@ fn reset_launcher_cache(vfs_path: &Path) -> anyhow::Result<()> {
134166
}
135167

136168
/// Verify SHA256 hash.
137-
fn verify(rom_path: &Path) -> anyhow::Result<()> {
169+
fn verify_hash(rom_path: &Path) -> anyhow::Result<()> {
138170
let hash_path = rom_path.join(HASH);
139171
let hash_expected: &[u8] = &fs::read(hash_path).context("read hash file")?;
140172
let hash_actual: &[u8] = &hash_dir(rom_path).context("calculate hash")?[..];
141173
if hash_actual != hash_expected {
142174
let exp = HEXLOWER.encode(hash_expected);
143175
let act = HEXLOWER.encode(hash_actual);
144-
bail!("invalid hash:\n expected: {exp}\n got: {act}");
176+
bail!("expected: {exp}, got: {act}");
145177
}
146178
Ok(())
147179
}
@@ -319,4 +351,27 @@ mod tests {
319351
dirs_eq(&vfs.join("roms"), &vfs2.join("roms"));
320352
dirs_eq(&vfs.join("data"), &vfs2.join("data"));
321353
}
354+
355+
#[test]
356+
fn test_id_matches() {
357+
let meta = Meta {
358+
author_id: "sys",
359+
app_id: "launcher",
360+
361+
app_name: "",
362+
author_name: "",
363+
launcher: true,
364+
sudo: true,
365+
version: 1,
366+
};
367+
assert!(id_matches("launcher", &meta));
368+
assert!(id_matches("sys.launcher", &meta));
369+
assert!(id_matches("sys.launcher.zip", &meta));
370+
assert!(id_matches("/tmp/sys.launcher.zip", &meta));
371+
let url = "https://github.com/firefly-zero/firefly-launcher/releases/latest/download/sys.launcher.zip";
372+
assert!(id_matches(url, &meta));
373+
374+
assert!(!id_matches("lux.snek", &meta));
375+
assert!(!id_matches("snek", &meta));
376+
}
322377
}

src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
clippy::nursery,
99
clippy::allow_attributes
1010
)]
11-
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
11+
#![allow(
12+
clippy::enum_glob_use,
13+
clippy::wildcard_imports,
14+
clippy::case_sensitive_file_extension_comparisons
15+
)]
1216
#![expect(clippy::option_if_let_else)]
1317

1418
mod args;

0 commit comments

Comments
 (0)