deps: replace sigstore-verification with sigstore-verify to kill vulns (#8064)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Generated
+274
-1282
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ urlencoding = { workspace = true }
|
||||
clap_complete = "4.5.62"
|
||||
comfy-table = "7.2.2"
|
||||
sha2 = "0.10"
|
||||
sigstore-verification = { version = "0.1", default-features = false, features = ["rustls"] }
|
||||
sigstore-verify = { version = "0.6", default-features = false, features = ["rustls"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["wincred"] }
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sigstore_verify::trust_root::TrustedRoot;
|
||||
use sigstore_verify::types::{Bundle, Sha256Hash};
|
||||
use sigstore_verify::VerificationPolicy;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
@@ -53,72 +55,143 @@ fn sha256_hex(data: &[u8]) -> String {
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
/// Verify the downloaded archive against GitHub's Sigstore SLSA provenance.
|
||||
///
|
||||
/// Uses the attestations published by `actions/attest-build-provenance` in the
|
||||
/// release/canary/nightly workflows (added in PR #7097). Verification fetches
|
||||
/// the attestation bundle from GitHub's attestation API and validates the
|
||||
/// Sigstore signature chain, Rekor transparency log inclusion, and artifact
|
||||
/// digest match.
|
||||
/// Returns `Ok(true)` when the attestation is fully verified, `Ok(false)` when
|
||||
/// verification was skipped with a warning (no attestation, transient error),
|
||||
/// and `Err` when verification actively fails (invalid signature, digest
|
||||
/// mismatch, etc.).
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AttestationResponse {
|
||||
attestations: Vec<AttestationEntry>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AttestationEntry {
|
||||
bundle: serde_json::Value,
|
||||
}
|
||||
|
||||
const GITHUB_ACTIONS_ISSUER: &str = "https://token.actions.githubusercontent.com";
|
||||
|
||||
async fn fetch_attestations(digest: &str, token: Option<&str>) -> Result<Vec<serde_json::Value>> {
|
||||
let url = format!(
|
||||
"https://api.github.com/repos/block/goose/attestations/sha256:{digest}\
|
||||
?per_page=30&predicate_type=https://slsa.dev/provenance/v1"
|
||||
);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut req = client
|
||||
.get(&url)
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||
.header("User-Agent", "goose-cli");
|
||||
|
||||
if let Some(tok) = token {
|
||||
req = req.header("Authorization", format!("Bearer {tok}"));
|
||||
}
|
||||
|
||||
let resp = req.send().await.context("Failed to fetch attestations")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
bail!("GitHub attestation API returned HTTP {}", resp.status());
|
||||
}
|
||||
|
||||
let body: AttestationResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse attestation response")?;
|
||||
|
||||
Ok(body.attestations.into_iter().map(|a| a.bundle).collect())
|
||||
}
|
||||
|
||||
// Verify a single attestation bundle against the artifact digest and workflow.
|
||||
fn verify_bundle(
|
||||
bundle_json: &serde_json::Value,
|
||||
artifact_digest: Sha256Hash,
|
||||
policy: &VerificationPolicy,
|
||||
trusted_root: &TrustedRoot,
|
||||
workflow: &str,
|
||||
) -> Result<()> {
|
||||
let bundle_str = serde_json::to_string(bundle_json)?;
|
||||
let bundle = Bundle::from_json(&bundle_str)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse bundle: {e}"))?;
|
||||
|
||||
let result = sigstore_verify::verify(artifact_digest, &bundle, policy, trusted_root)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
if !result.success {
|
||||
bail!("Verification unsuccessful");
|
||||
}
|
||||
|
||||
let identity = result
|
||||
.identity
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("No identity in certificate"))?;
|
||||
|
||||
let expected = format!("/.github/workflows/{workflow}");
|
||||
if !identity.contains(&expected) {
|
||||
bail!("Workflow mismatch: expected {workflow}, got {identity}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `Ok(true)` verified, `Ok(false)` skipped (soft warning), `Err` hard failure.
|
||||
async fn verify_provenance(archive_data: &[u8], tag: &str) -> Result<bool> {
|
||||
let digest = sha256_hex(archive_data);
|
||||
println!("Archive SHA-256: {digest}");
|
||||
|
||||
let mut tmp_file = tempfile::NamedTempFile::new()
|
||||
.context("Failed to create temp file for provenance verification")?;
|
||||
tmp_file
|
||||
.write_all(archive_data)
|
||||
.context("Failed to write archive to temp file")?;
|
||||
tmp_file.flush().context("Failed to flush temp file")?;
|
||||
|
||||
let workflow = match tag {
|
||||
"canary" => "canary.yml",
|
||||
_ => "release.yml",
|
||||
};
|
||||
|
||||
let token = std::env::var("GITHUB_TOKEN")
|
||||
let token = env::var("GITHUB_TOKEN")
|
||||
.ok()
|
||||
.or_else(|| std::env::var("GH_TOKEN").ok());
|
||||
.or_else(|| env::var("GH_TOKEN").ok());
|
||||
|
||||
println!("Verifying SLSA provenance via Sigstore...");
|
||||
|
||||
match sigstore_verification::verify_github_attestation(
|
||||
tmp_file.path(),
|
||||
"block",
|
||||
"goose",
|
||||
token.as_deref(),
|
||||
Some(workflow),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("Sigstore provenance verification passed.");
|
||||
Ok(true)
|
||||
}
|
||||
Err(sigstore_verification::AttestationError::NoAttestations) => {
|
||||
let bundles = match fetch_attestations(&digest, token.as_deref()).await {
|
||||
Ok(b) if b.is_empty() => {
|
||||
eprintln!(
|
||||
"Warning: No Sigstore attestation found for this build. \
|
||||
This may be expected for canary or nightly builds."
|
||||
);
|
||||
Ok(false)
|
||||
return Ok(false);
|
||||
}
|
||||
Err(sigstore_verification::AttestationError::Verification(msg)) => Err(anyhow::anyhow!(
|
||||
"Sigstore verification failed: {}\n\nAborting update due to security check failure.",
|
||||
msg
|
||||
)),
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Sigstore provenance check could not complete: {e}\n\
|
||||
This may be expected for releases published before provenance \
|
||||
attestations were enabled."
|
||||
);
|
||||
Ok(false)
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let trusted_root = TrustedRoot::production().context("Failed to load Sigstore trusted root")?;
|
||||
let policy = VerificationPolicy::with_issuer(GITHUB_ACTIONS_ISSUER);
|
||||
let artifact_digest =
|
||||
Sha256Hash::from_hex(&digest).context("Failed to parse artifact digest")?;
|
||||
|
||||
// One passing attestation is sufficient.
|
||||
let mut last_err = None;
|
||||
for bundle_json in &bundles {
|
||||
match verify_bundle(
|
||||
bundle_json,
|
||||
artifact_digest,
|
||||
&policy,
|
||||
&trusted_root,
|
||||
workflow,
|
||||
) {
|
||||
Ok(()) => {
|
||||
println!("Sigstore provenance verification passed.");
|
||||
return Ok(true);
|
||||
}
|
||||
Err(e) => last_err = Some(e),
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"Sigstore verification failed: {}\n\nAborting update due to security check failure.",
|
||||
last_err.unwrap()
|
||||
))
|
||||
}
|
||||
|
||||
/// Update the goose binary to the latest release.
|
||||
|
||||
@@ -89,10 +89,10 @@ sqlx = { version = "0.8", default-features = false, features = [
|
||||
# For Bedrock provider
|
||||
aws-config = { version = "=1.8.12", features = ["behavior-version-latest"] }
|
||||
aws-smithy-types = "=1.3.5"
|
||||
aws-sdk-bedrockruntime = "=1.120.0"
|
||||
aws-sdk-bedrockruntime = { version = "=1.120.0", default-features = false, features = ["default-https-client", "rt-tokio"] }
|
||||
|
||||
# For SageMaker TGI provider
|
||||
aws-sdk-sagemakerruntime = "1.62.0"
|
||||
aws-sdk-sagemakerruntime = { version = "1.62.0", default-features = false, features = ["default-https-client", "rt-tokio"] }
|
||||
|
||||
# For GCP Vertex AI provider auth
|
||||
jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] }
|
||||
|
||||
Reference in New Issue
Block a user