use super::*;
use crate::test_helpers::EnvVarGuard;
#[test]
#[serial_test::serial]
fn sign_artifact_rejects_when_cosign_missing() {
let _g = EnvVarGuard::unset("CFGD_COSIGN_BIN");
if crate::command_available("cosign") {
return;
}
let result = sign_artifact("ghcr.io/test/mod:v1", None);
assert!(matches!(result, Err(OciError::ToolNotFound { .. })));
}
#[test]
fn verify_signature_rejects_keyless_without_identity() {
let result = verify_signature(
"ghcr.io/test/mod:v1",
&VerifyOptions {
key: None,
identity: None,
issuer: None,
},
);
assert!(matches!(result, Err(OciError::VerificationFailed { .. })));
}
#[test]
#[serial_test::serial]
fn verify_signature_rejects_when_cosign_missing() {
let _g = EnvVarGuard::unset("CFGD_COSIGN_BIN");
if crate::command_available("cosign") {
return;
}
let result = verify_signature(
"ghcr.io/test/mod:v1",
&VerifyOptions {
key: Some("cosign.pub"),
identity: None,
issuer: None,
},
);
assert!(matches!(result, Err(OciError::ToolNotFound { .. })));
}
#[test]
#[serial_test::serial]
fn attach_attestation_rejects_when_cosign_missing() {
let _g = EnvVarGuard::unset("CFGD_COSIGN_BIN");
if crate::command_available("cosign") {
return;
}
let result = attach_attestation("ghcr.io/test/mod:v1", "provenance.json", None);
assert!(matches!(result, Err(OciError::ToolNotFound { .. })));
}
#[test]
fn verify_attestation_rejects_keyless_without_identity() {
let result = verify_attestation(
"ghcr.io/test/mod:v1",
"slsaprovenance",
&VerifyOptions {
key: None,
identity: None,
issuer: None,
},
);
assert!(matches!(result, Err(OciError::VerificationFailed { .. })));
}
#[test]
#[serial_test::serial]
fn verify_attestation_rejects_when_cosign_missing() {
let _g = EnvVarGuard::unset("CFGD_COSIGN_BIN");
if crate::command_available("cosign") {
return;
}
let result = verify_attestation(
"ghcr.io/test/mod:v1",
"slsaprovenance",
&VerifyOptions {
key: Some("cosign.pub"),
identity: None,
issuer: None,
},
);
assert!(matches!(result, Err(OciError::ToolNotFound { .. })));
}
#[test]
fn generate_slsa_provenance_creates_valid_json() {
let prov = generate_slsa_provenance(
"ghcr.io/test/mod:v1",
"sha256:abc123",
"https://github.com/myorg/myrepo",
"abc123def",
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&prov).unwrap();
assert_eq!(parsed["predicateType"], "https://slsa.dev/provenance/v1");
assert_eq!(parsed["subject"][0]["name"], "ghcr.io/test/mod:v1");
assert_eq!(parsed["subject"][0]["digest"]["sha256"], "abc123");
}
#[test]
fn verify_options_default_keyless() {
let opts = VerifyOptions {
key: None,
identity: Some("user@example.com"),
issuer: Some("https://accounts.google.com"),
};
assert!(opts.key.is_none());
assert_eq!(opts.identity.unwrap(), "user@example.com");
assert_eq!(opts.issuer.unwrap(), "https://accounts.google.com");
}
#[test]
fn validate_verify_options_accepts_key_only() {
let opts = VerifyOptions {
key: Some("cosign.pub"),
identity: None,
issuer: None,
};
let result = validate_verify_options(&opts);
assert!(result.is_ok(), "key-only verification should be valid");
}
#[test]
fn validate_verify_options_accepts_identity_only() {
let opts = VerifyOptions {
key: None,
identity: Some("user@example.com"),
issuer: None,
};
let result = validate_verify_options(&opts);
assert!(result.is_ok(), "identity-only verification should be valid");
}
#[test]
fn validate_verify_options_accepts_issuer_only() {
let opts = VerifyOptions {
key: None,
identity: None,
issuer: Some("https://accounts.google.com"),
};
let result = validate_verify_options(&opts);
assert!(result.is_ok(), "issuer-only verification should be valid");
}
#[test]
fn validate_verify_options_accepts_all_fields() {
let opts = VerifyOptions {
key: Some("cosign.pub"),
identity: Some("user@example.com"),
issuer: Some("https://accounts.google.com"),
};
let result = validate_verify_options(&opts);
assert!(result.is_ok(), "all-fields verification should be valid");
}
#[test]
fn validate_verify_options_rejects_all_none() {
let opts = VerifyOptions {
key: None,
identity: None,
issuer: None,
};
let result = validate_verify_options(&opts);
assert!(result.is_err(), "all-none should be rejected");
let err = result.unwrap_err();
let err_msg = format!("{err}");
assert!(
err_msg.contains("keyless verification requires identity or issuer constraint"),
"error message should explain the constraint, got: {err_msg}"
);
}
#[test]
fn apply_verify_args_with_key() {
let mut cmd = std::process::Command::new("echo");
let opts = VerifyOptions {
key: Some("/path/to/cosign.pub"),
identity: None,
issuer: None,
};
apply_verify_args(&mut cmd, &opts);
let args: Vec<_> = cmd.get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args, vec!["--key", "/path/to/cosign.pub"]);
}
#[test]
fn apply_verify_args_keyless_with_identity_and_issuer() {
let mut cmd = std::process::Command::new("echo");
let opts = VerifyOptions {
key: None,
identity: Some("user@example.com"),
issuer: Some("https://accounts.google.com"),
};
apply_verify_args(&mut cmd, &opts);
let args: Vec<_> = cmd.get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(
args,
vec![
"--certificate-identity-regexp",
"user@example.com",
"--certificate-oidc-issuer-regexp",
"https://accounts.google.com"
]
);
}
#[test]
fn apply_verify_args_keyless_with_identity_only_defaults_issuer() {
let mut cmd = std::process::Command::new("echo");
let opts = VerifyOptions {
key: None,
identity: Some("ci@github.com"),
issuer: None,
};
apply_verify_args(&mut cmd, &opts);
let args: Vec<_> = cmd.get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(
args,
vec![
"--certificate-identity-regexp",
"ci@github.com",
"--certificate-oidc-issuer-regexp",
".*"
]
);
}
#[test]
fn apply_verify_args_keyless_with_issuer_only_defaults_identity() {
let mut cmd = std::process::Command::new("echo");
let opts = VerifyOptions {
key: None,
identity: None,
issuer: Some("https://token.actions.githubusercontent.com"),
};
apply_verify_args(&mut cmd, &opts);
let args: Vec<_> = cmd.get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(
args,
vec![
"--certificate-identity-regexp",
".*",
"--certificate-oidc-issuer-regexp",
"https://token.actions.githubusercontent.com"
]
);
}
#[test]
fn apply_verify_args_key_takes_precedence_over_keyless() {
let mut cmd = std::process::Command::new("echo");
let opts = VerifyOptions {
key: Some("my.pub"),
identity: Some("user@example.com"),
issuer: Some("https://issuer.example.com"),
};
apply_verify_args(&mut cmd, &opts);
let args: Vec<_> = cmd.get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args, vec!["--key", "my.pub"]);
}
#[test]
fn generate_slsa_provenance_strips_sha256_prefix() {
let prov = generate_slsa_provenance(
"ghcr.io/test/mod:v1",
"sha256:deadbeef1234",
"https://github.com/org/repo",
"abc123",
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&prov).unwrap();
assert_eq!(parsed["subject"][0]["digest"]["sha256"], "deadbeef1234");
}
#[test]
fn generate_slsa_provenance_handles_plain_digest() {
let prov = generate_slsa_provenance(
"ghcr.io/test/mod:v1",
"plaindigest",
"https://github.com/org/repo",
"abc123",
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&prov).unwrap();
assert_eq!(parsed["subject"][0]["digest"]["sha256"], "plaindigest");
}
#[test]
fn generate_slsa_provenance_includes_source_info() {
let prov = generate_slsa_provenance(
"ghcr.io/myorg/mymod:v2",
"sha256:abcdef",
"https://github.com/myorg/myrepo",
"deadbeef123",
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&prov).unwrap();
assert_eq!(parsed["_type"], "https://in-toto.io/Statement/v1");
assert_eq!(
parsed["predicate"]["buildDefinition"]["externalParameters"]["source"]["uri"],
"https://github.com/myorg/myrepo"
);
assert_eq!(
parsed["predicate"]["buildDefinition"]["externalParameters"]["source"]["digest"]["gitCommit"],
"deadbeef123"
);
assert_eq!(
parsed["predicate"]["runDetails"]["builder"]["id"],
"https://cfgd.io/builder/v1"
);
}
#[test]
fn generate_slsa_provenance_complete_structure() {
let prov = generate_slsa_provenance(
"ghcr.io/org/mod:v2.0.0",
"sha256:deadbeef1234",
"https://github.com/org/config",
"abc123def456",
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&prov).unwrap();
assert_eq!(
parsed["_type"], "https://in-toto.io/Statement/v1",
"should be in-toto v1 statement"
);
assert_eq!(
parsed["predicateType"], "https://slsa.dev/provenance/v1",
"should be SLSA v1 provenance"
);
let subject = &parsed["subject"][0];
assert_eq!(subject["name"], "ghcr.io/org/mod:v2.0.0");
assert_eq!(
subject["digest"]["sha256"], "deadbeef1234",
"sha256: prefix should be stripped"
);
let predicate = &parsed["predicate"];
assert_eq!(
predicate["buildDefinition"]["buildType"],
"https://cfgd.io/ModuleBuild/v1"
);
assert_eq!(
predicate["buildDefinition"]["externalParameters"]["source"]["uri"],
"https://github.com/org/config"
);
assert_eq!(
predicate["buildDefinition"]["externalParameters"]["source"]["digest"]["gitCommit"],
"abc123def456"
);
assert_eq!(
predicate["runDetails"]["builder"]["id"],
"https://cfgd.io/builder/v1"
);
let invocation_id = predicate["runDetails"]["metadata"]["invocationId"]
.as_str()
.unwrap();
assert!(
!invocation_id.is_empty(),
"invocationId should not be empty"
);
}
#[test]
fn generate_slsa_provenance_bare_digest() {
let prov = generate_slsa_provenance("ghcr.io/test:v1", "abcdef", "repo", "commit").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&prov).unwrap();
assert_eq!(
parsed["subject"][0]["digest"]["sha256"], "abcdef",
"bare digest should pass through as-is"
);
}
#[test]
fn validate_verify_options_key_only_passes() {
let opts = VerifyOptions {
key: Some("cosign.pub"),
identity: None,
issuer: None,
};
assert!(validate_verify_options(&opts).is_ok());
}
#[test]
fn validate_verify_options_identity_only_passes() {
let opts = VerifyOptions {
key: None,
identity: Some("user@example.com"),
issuer: None,
};
assert!(validate_verify_options(&opts).is_ok());
}
#[test]
fn validate_verify_options_issuer_only_passes() {
let opts = VerifyOptions {
key: None,
identity: None,
issuer: Some("https://accounts.google.com"),
};
assert!(validate_verify_options(&opts).is_ok());
}
#[test]
fn validate_verify_options_all_none_fails() {
let opts = VerifyOptions {
key: None,
identity: None,
issuer: None,
};
let result = validate_verify_options(&opts);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("keyless verification requires"),
"should explain the requirement: {msg}"
);
}
#[test]
fn apply_verify_args_with_key_only() {
let mut cmd = std::process::Command::new("echo");
let opts = VerifyOptions {
key: Some("/path/to/cosign.pub"),
identity: None,
issuer: None,
};
apply_verify_args(&mut cmd, &opts);
}
#[test]
fn apply_verify_args_keyless_defaults() {
let mut cmd = std::process::Command::new("echo");
let opts = VerifyOptions {
key: None,
identity: None,
issuer: Some("https://issuer.example.com"),
};
apply_verify_args(&mut cmd, &opts);
}
#[cfg(unix)]
mod fake_cosign {
use super::*;
use crate::test_helpers::CosignTestShim;
use serial_test::serial;
#[test]
#[serial]
fn sign_artifact_keyless_invokes_sign_yes_subcommand_and_returns_ok() {
let guard = CosignTestShim::install();
let result = sign_artifact("ghcr.io/test/mod:v1", None);
assert!(result.is_ok(), "happy keyless sign returns Ok: {result:?}");
let argv = guard.argv_log();
assert!(argv.contains("sign"), "argv must include 'sign': {argv}");
assert!(argv.contains("--yes"), "keyless sign passes --yes: {argv}");
assert!(
argv.contains("ghcr.io/test/mod:v1"),
"argv ends with artifact ref: {argv}"
);
}
#[test]
#[serial]
fn sign_artifact_with_key_path_passes_key_flag() {
let guard = CosignTestShim::install();
let result = sign_artifact("ghcr.io/test/mod:v1", Some("/keys/cosign.key"));
assert!(result.is_ok(), "with-key sign returns Ok");
let argv = guard.argv_log();
assert!(
argv.contains("--key /keys/cosign.key"),
"argv must include --key: {argv}"
);
assert!(
!argv.contains("--yes"),
"with-key sign must NOT pass --yes: {argv}"
);
}
#[test]
#[serial]
fn sign_artifact_propagates_cosign_failure_with_stderr_message() {
let _guard = CosignTestShim::builder()
.with_exit(1)
.with_stderr("rekor unreachable")
.install();
let result = sign_artifact("ghcr.io/test/mod:v1", None);
let err = result.expect_err("non-zero cosign must error");
let msg = format!("{err}");
assert!(
msg.contains("rekor unreachable"),
"error must surface stderr: {msg}"
);
assert!(
msg.contains("cosign sign failed"),
"error prefixes with `cosign sign failed`: {msg}"
);
}
#[test]
#[serial]
fn verify_signature_keyless_passes_identity_and_issuer_constraints() {
let guard = CosignTestShim::install();
let result = verify_signature(
"ghcr.io/myorg/mod:v1",
&VerifyOptions {
key: None,
identity: Some("user@example.com"),
issuer: Some("https://accounts.google.com"),
},
);
assert!(
result.is_ok(),
"happy keyless verify returns Ok: {result:?}"
);
let argv = guard.argv_log();
assert!(
argv.contains("verify"),
"argv must include 'verify': {argv}"
);
assert!(
argv.contains("--certificate-identity-regexp user@example.com"),
"argv must pin identity: {argv}"
);
assert!(
argv.contains("--certificate-oidc-issuer-regexp https://accounts.google.com"),
"argv must pin issuer: {argv}"
);
}
#[test]
#[serial]
fn verify_signature_with_key_takes_priority_over_identity() {
let guard = CosignTestShim::install();
let result = verify_signature(
"ghcr.io/myorg/mod:v1",
&VerifyOptions {
key: Some("/keys/cosign.pub"),
identity: None,
issuer: None,
},
);
assert!(result.is_ok());
let argv = guard.argv_log();
assert!(
argv.contains("--key /keys/cosign.pub"),
"argv must include --key: {argv}"
);
assert!(
!argv.contains("--certificate-identity-regexp"),
"--key path must NOT add identity-regexp: {argv}"
);
}
#[test]
#[serial]
fn verify_signature_propagates_cosign_failure_with_artifact_ref() {
let _guard = CosignTestShim::builder()
.with_exit(1)
.with_stderr("signature mismatch")
.install();
let result = verify_signature(
"ghcr.io/myorg/mod:v1",
&VerifyOptions {
key: Some("/keys/cosign.pub"),
identity: None,
issuer: None,
},
);
let err = result.expect_err("non-zero verify must error");
match err {
OciError::VerificationFailed { reference, message } => {
assert_eq!(reference, "ghcr.io/myorg/mod:v1");
assert!(
message.contains("signature mismatch"),
"stderr surfaced in message: {message}"
);
}
other => panic!("expected VerificationFailed, got {other:?}"),
}
}
#[test]
#[serial]
fn attach_attestation_passes_predicate_and_type_flags() {
let guard = CosignTestShim::install();
let result = attach_attestation(
"ghcr.io/myorg/mod:v1",
"/tmp/provenance.json",
Some("/keys/cosign.key"),
);
assert!(result.is_ok(), "attach returns Ok: {result:?}");
let argv = guard.argv_log();
assert!(
argv.contains("attest"),
"argv must include 'attest': {argv}"
);
assert!(
argv.contains("--predicate /tmp/provenance.json"),
"argv pins predicate path: {argv}"
);
assert!(
argv.contains("--type slsaprovenance"),
"argv pins predicate type: {argv}"
);
assert!(
argv.contains("--key /keys/cosign.key"),
"with-key attest passes --key: {argv}"
);
}
#[test]
#[serial]
fn attach_attestation_keyless_uses_yes_flag() {
let guard = CosignTestShim::install();
let result = attach_attestation("ghcr.io/myorg/mod:v1", "/tmp/p.json", None);
assert!(result.is_ok());
let argv = guard.argv_log();
assert!(
argv.contains("--yes"),
"keyless attest passes --yes: {argv}"
);
assert!(
!argv.contains("--key"),
"keyless attest must NOT pass --key: {argv}"
);
}
#[test]
#[serial]
fn verify_attestation_runs_verify_attestation_subcommand() {
let guard = CosignTestShim::install();
let result = verify_attestation(
"ghcr.io/myorg/mod:v1",
"slsaprovenance",
&VerifyOptions {
key: Some("/keys/cosign.pub"),
identity: None,
issuer: None,
},
);
assert!(result.is_ok(), "verify_attestation returns Ok: {result:?}");
let argv = guard.argv_log();
assert!(
argv.contains("verify-attestation"),
"argv must use verify-attestation subcommand: {argv}"
);
assert!(
argv.contains("--type slsaprovenance"),
"argv pins --type: {argv}"
);
assert!(
argv.contains("ghcr.io/myorg/mod:v1"),
"argv ends with artifact ref: {argv}"
);
}
#[test]
#[serial]
fn verify_attestation_propagates_failure_with_stderr() {
let _guard = CosignTestShim::builder()
.with_exit(1)
.with_stderr("no matching attestations")
.install();
let result = verify_attestation(
"ghcr.io/myorg/mod:v1",
"slsaprovenance",
&VerifyOptions {
key: Some("/keys/cosign.pub"),
identity: None,
issuer: None,
},
);
let err = result.expect_err("non-zero verify-attestation must error");
match err {
OciError::AttestationError { message } => {
assert!(
message.contains("no matching attestations"),
"stderr surfaced: {message}"
);
}
other => panic!("expected AttestationError, got {other:?}"),
}
}
}