pkgwatch/src/verifier.rs
Austin Schaefer bf46cbf073 First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."

Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 10:04:48 +02:00

93 lines
2.9 KiB
Rust

use crate::config::Verification;
use crate::fetcher;
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use std::path::Path;
use std::process::Command;
pub struct VerificationResult {
pub tier: u8,
pub passed: bool,
pub justification: String,
}
/// Runs the verification method declared for a package against a
/// downloaded artifact. See SPEC.md > Verification trust tiers for what
/// each tier does and does not prove.
pub fn verify(
client: &reqwest::blocking::Client,
verification: &Verification,
repo: &str,
tag: &str,
artifact_path: &Path,
dest_dir: &Path,
) -> Result<VerificationResult> {
match verification {
Verification::SameOriginSha256 {
checksum_asset_pattern,
} => {
let checksum_path = fetcher::download_asset(
client,
repo,
tag,
checksum_asset_pattern,
dest_dir,
)?;
let checksum_text = std::fs::read_to_string(&checksum_path)?;
let expected = checksum_text
.split_whitespace()
.next()
.context("empty checksum file")?
.to_lowercase();
let data = std::fs::read(artifact_path)?;
let actual = sha256_hex(&data);
let passed = actual == expected;
Ok(VerificationResult {
tier: verification.tier(),
passed,
justification: if passed {
"same-origin sha256 matched — proves transport integrity only, \
not authorship (see tier 4 in SPEC.md)"
.into()
} else {
format!("sha256 mismatch: expected {expected}, got {actual}")
},
})
}
Verification::GithubAttestation => {
let output = Command::new("gh")
.args([
"attestation",
"verify",
&artifact_path.to_string_lossy(),
"-R",
repo,
])
.output()
.context("running `gh attestation verify` (is `gh` installed and authenticated?)")?;
let passed = output.status.success();
Ok(VerificationResult {
tier: verification.tier(),
passed,
justification: if passed {
"GitHub build-provenance attestation verified via `gh attestation verify`"
.into()
} else {
format!(
"gh attestation verify failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
)
},
})
}
}
}
fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}