94 lines
2.9 KiB
Rust
94 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())
|
||
|
|
}
|