use crate::errors::OciError;
pub fn sign_artifact(artifact_ref: &str, key_path: Option<&str>) -> Result<(), OciError> {
crate::require_cosign().map_err(|_| OciError::ToolNotFound {
tool: "cosign".to_string(),
})?;
let mut cmd = crate::cosign_cmd();
cmd.arg("sign");
if let Some(key) = key_path {
cmd.arg("--key").arg(key);
} else {
cmd.arg("--yes");
}
cmd.arg(artifact_ref);
let output = cmd.output().map_err(|e| OciError::SigningError {
message: format!("failed to run cosign: {e}"),
})?;
if !output.status.success() {
return Err(OciError::SigningError {
message: format!(
"cosign sign failed: {}",
crate::stderr_lossy_trimmed(&output)
),
});
}
tracing::info!(reference = artifact_ref, "artifact signed with cosign");
Ok(())
}
pub struct VerifyOptions<'a> {
pub key: Option<&'a str>,
pub identity: Option<&'a str>,
pub issuer: Option<&'a str>,
}
fn validate_verify_options(opts: &VerifyOptions<'_>) -> Result<(), OciError> {
if opts.key.is_none() && opts.identity.is_none() && opts.issuer.is_none() {
return Err(OciError::VerificationFailed {
reference: String::new(),
message: "keyless verification requires identity or issuer constraint (use --key, or provide VerifyOptions.identity/issuer)".to_string(),
});
}
Ok(())
}
fn apply_verify_args(cmd: &mut std::process::Command, opts: &VerifyOptions<'_>) {
if let Some(key) = opts.key {
cmd.arg("--key").arg(key);
} else {
let identity = opts.identity.unwrap_or(".*");
let issuer = opts.issuer.unwrap_or(".*");
cmd.arg("--certificate-identity-regexp").arg(identity);
cmd.arg("--certificate-oidc-issuer-regexp").arg(issuer);
}
}
pub fn verify_signature(artifact_ref: &str, opts: &VerifyOptions<'_>) -> Result<(), OciError> {
validate_verify_options(opts)?;
crate::require_cosign().map_err(|_| OciError::ToolNotFound {
tool: "cosign".to_string(),
})?;
let mut cmd = crate::cosign_cmd();
cmd.arg("verify");
apply_verify_args(&mut cmd, opts);
cmd.arg(artifact_ref);
let output = cmd.output().map_err(|e| OciError::VerificationFailed {
reference: artifact_ref.to_string(),
message: format!("failed to run cosign: {e}"),
})?;
if !output.status.success() {
return Err(OciError::VerificationFailed {
reference: artifact_ref.to_string(),
message: format!(
"cosign verify failed: {}",
crate::stderr_lossy_trimmed(&output)
),
});
}
tracing::info!(reference = artifact_ref, "signature verified");
Ok(())
}
pub fn generate_slsa_provenance(
artifact_ref: &str,
digest: &str,
source_repo: &str,
source_commit: &str,
) -> Result<String, OciError> {
let now = crate::utc_now_iso8601();
serde_json::to_string_pretty(&serde_json::json!({
"_type": "https://in-toto.io/Statement/v1",
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [{
"name": artifact_ref,
"digest": {
"sha256": crate::strip_sha256_prefix(digest),
}
}],
"predicate": {
"buildDefinition": {
"buildType": "https://cfgd.io/ModuleBuild/v1",
"externalParameters": {
"source": {
"uri": source_repo,
"digest": { "gitCommit": source_commit },
}
},
},
"runDetails": {
"builder": {
"id": "https://cfgd.io/builder/v1",
},
"metadata": {
"invocationId": &now,
"startedOn": &now,
}
}
}
}))
.map_err(|e| OciError::AttestationError {
message: format!("failed to serialize SLSA provenance: {e}"),
})
}
pub fn attach_attestation(
artifact_ref: &str,
attestation_path: &str,
key_path: Option<&str>,
) -> Result<(), OciError> {
crate::require_cosign().map_err(|_| OciError::ToolNotFound {
tool: "cosign".to_string(),
})?;
let mut cmd = crate::cosign_cmd();
cmd.arg("attest");
if let Some(key) = key_path {
cmd.arg("--key").arg(key);
} else {
cmd.arg("--yes");
}
cmd.arg("--predicate")
.arg(attestation_path)
.arg("--type")
.arg("slsaprovenance")
.arg(artifact_ref);
let output = cmd.output().map_err(|e| OciError::AttestationError {
message: format!("failed to run cosign attest: {e}"),
})?;
if !output.status.success() {
return Err(OciError::AttestationError {
message: format!(
"cosign attest failed: {}",
crate::stderr_lossy_trimmed(&output)
),
});
}
tracing::info!(reference = artifact_ref, "attestation attached");
Ok(())
}
pub fn verify_attestation(
artifact_ref: &str,
predicate_type: &str,
opts: &VerifyOptions<'_>,
) -> Result<(), OciError> {
validate_verify_options(opts)?;
crate::require_cosign().map_err(|_| OciError::ToolNotFound {
tool: "cosign".to_string(),
})?;
let mut cmd = crate::cosign_cmd();
cmd.arg("verify-attestation");
apply_verify_args(&mut cmd, opts);
cmd.arg("--type").arg(predicate_type).arg(artifact_ref);
let output = cmd.output().map_err(|e| OciError::AttestationError {
message: format!("failed to run cosign verify-attestation: {e}"),
})?;
if !output.status.success() {
return Err(OciError::AttestationError {
message: format!(
"attestation verification failed: {}",
crate::stderr_lossy_trimmed(&output)
),
});
}
tracing::info!(reference = artifact_ref, "attestation verified");
Ok(())
}
#[cfg(test)]
mod tests;