Skip to content

Commit d395448

Browse files
authored
feat(install): implicit-trust model for user-declared sources + checksum pinning (#171)
1 parent fe7a6f4 commit d395448

3 files changed

Lines changed: 105 additions & 3 deletions

File tree

crates/soar-config/src/packages.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ pub struct PackageOptions {
199199
/// Direct URL to download the package from (makes it a "local" package).
200200
pub url: Option<String>,
201201

202+
/// Expected BLAKE3 checksum (hex) of the downloaded artifact, for `url`/`github`/
203+
/// `gitlab` packages. When set, soar verifies the download against it and refuses to
204+
/// install on mismatch.
205+
/// Without it, these user-declared sources install on implicit trust.
206+
/// Has no effect on registry packages, which already ship their own checksum.
207+
pub bsum: Option<String>,
208+
202209
/// GitHub repository in "owner/repo" format for installing from releases.
203210
/// When set, soar fetches the latest release and downloads the matching asset.
204211
pub github: Option<String>,
@@ -301,6 +308,7 @@ pub struct ResolvedPackage {
301308
pub version: Option<String>,
302309
pub repo: Option<String>,
303310
pub url: Option<String>,
311+
pub bsum: Option<String>,
304312
pub github: Option<String>,
305313
pub gitlab: Option<String>,
306314
pub asset_pattern: Option<String>,
@@ -340,6 +348,7 @@ impl PackageSpec {
340348
version,
341349
repo: None,
342350
url: None,
351+
bsum: None,
343352
github: None,
344353
gitlab: None,
345354
asset_pattern: None,
@@ -376,6 +385,7 @@ impl PackageSpec {
376385
version,
377386
repo: opts.repo.clone(),
378387
url: opts.url.clone(),
388+
bsum: opts.bsum.clone(),
379389
github: opts.github.clone(),
380390
gitlab: opts.gitlab.clone(),
381391
asset_pattern: opts.asset_pattern.clone(),
@@ -663,6 +673,34 @@ special = { profile = "isolated" }
663673
assert_eq!(resolved[0].profile, Some("isolated".to_string()));
664674
}
665675

676+
#[test]
677+
fn test_bsum_pin_resolved() {
678+
let toml_str = r#"
679+
[packages]
680+
myapp = { url = "https://example.com/myapp-1.0.AppImage", bsum = "abc123" }
681+
nopin = { url = "https://example.com/nopin-1.0.AppImage" }
682+
"#;
683+
let config: PackagesConfig = toml::from_str(toml_str).unwrap();
684+
let resolved = config.resolved_packages();
685+
686+
let myapp = resolved.iter().find(|p| p.name == "myapp").unwrap();
687+
let nopin = resolved.iter().find(|p| p.name == "nopin").unwrap();
688+
689+
assert_eq!(myapp.bsum, Some("abc123".to_string()));
690+
assert_eq!(nopin.bsum, None);
691+
}
692+
693+
#[test]
694+
fn test_bsum_none_for_simple_spec() {
695+
let toml_str = r#"
696+
[packages]
697+
curl = "*"
698+
"#;
699+
let config: PackagesConfig = toml::from_str(toml_str).unwrap();
700+
let resolved = config.resolved_packages();
701+
assert_eq!(resolved[0].bsum, None);
702+
}
703+
666704
#[test]
667705
fn test_portable_config() {
668706
let toml_str = r#"

crates/soar-operations/src/apply.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,8 +636,10 @@ fn create_url_install_target(
636636
resolved: &ResolvedPackage,
637637
existing: Option<InstalledPackage>,
638638
) -> InstallTarget {
639+
let mut package = url_pkg.to_package();
640+
package.bsum = resolved.bsum.as_ref().map(|s| s.trim().to_lowercase());
639641
InstallTarget {
640-
package: url_pkg.to_package(),
642+
package,
641643
existing_install: existing,
642644
pinned: resolved.pinned,
643645
profile: resolved.profile.clone(),

crates/soar-operations/src/install.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,14 @@ pub async fn perform_installation(
690690
})
691691
}
692692

693+
/// Whether the registry-style "checksum or signature required" integrity gate is
694+
/// inapplicable to this package's source.
695+
/// Exemption only skips the gate; an explicit `bsum` (e.g. a user-provided pin) is still
696+
/// enforced by checksum verification.
697+
fn source_skips_integrity_gate(pkg: &Package) -> bool {
698+
pkg.repo_name == "local" || pkg.ghcr_pkg.is_some()
699+
}
700+
693701
#[allow(clippy::too_many_arguments)]
694702
async fn install_single_package(
695703
ctx: &SoarContext,
@@ -764,7 +772,9 @@ async fn install_single_package(
764772
let config = ctx.config();
765773
let bin_dir = config.get_bin_path()?;
766774

767-
if !no_verify && pkg.bsum.is_none() {
775+
let skip_integrity_gate = source_skips_integrity_gate(pkg);
776+
777+
if !no_verify && !skip_integrity_gate && pkg.bsum.is_none() {
768778
let has_signing = config
769779
.get_repository(&pkg.repo_name)
770780
.map(|repo| repo.signature_verification() && repo.pubkey.is_some())
@@ -919,7 +929,7 @@ async fn install_single_package(
919929
cleanup_sig_files(&install_dir);
920930
}
921931

922-
if !no_verify && pkg.bsum.is_none() && verified_sig_count == 0 {
932+
if !no_verify && !skip_integrity_gate && pkg.bsum.is_none() && verified_sig_count == 0 {
923933
return Err(SoarError::Custom(format!(
924934
"Refusing to install {}#{}: no checksum and no valid signature found to verify integrity (use --no-verify to override)",
925935
pkg.pkg_name, pkg.pkg_id
@@ -1133,3 +1143,55 @@ fn cleanup_sig_files(install_dir: &Path) {
11331143
}
11341144
}
11351145
}
1146+
1147+
#[cfg(test)]
1148+
mod tests {
1149+
use super::*;
1150+
1151+
fn pkg(repo_name: &str, ghcr: Option<&str>, bsum: Option<&str>) -> Package {
1152+
Package {
1153+
repo_name: repo_name.to_string(),
1154+
ghcr_pkg: ghcr.map(String::from),
1155+
bsum: bsum.map(String::from),
1156+
..Default::default()
1157+
}
1158+
}
1159+
1160+
#[test]
1161+
fn local_source_skips_integrity_gate() {
1162+
assert!(source_skips_integrity_gate(&pkg("local", None, None)));
1163+
}
1164+
1165+
#[test]
1166+
fn ghcr_source_skips_integrity_gate() {
1167+
assert!(source_skips_integrity_gate(&pkg(
1168+
"local",
1169+
Some("ghcr.io/org/repo:tag"),
1170+
None
1171+
)));
1172+
assert!(source_skips_integrity_gate(&pkg(
1173+
"some-repo",
1174+
Some("ghcr.io/org/repo:tag"),
1175+
None
1176+
)));
1177+
}
1178+
1179+
#[test]
1180+
fn registry_source_is_subject_to_integrity_gate() {
1181+
assert!(!source_skips_integrity_gate(&pkg("soarpkgs", None, None)));
1182+
}
1183+
1184+
#[test]
1185+
fn integrity_gate_exemption_is_independent_of_pinned_bsum() {
1186+
assert!(source_skips_integrity_gate(&pkg(
1187+
"local",
1188+
None,
1189+
Some("deadbeef")
1190+
)));
1191+
assert!(!source_skips_integrity_gate(&pkg(
1192+
"soarpkgs",
1193+
None,
1194+
Some("deadbeef")
1195+
)));
1196+
}
1197+
}

0 commit comments

Comments
 (0)