use crate::config::{StringOrBool, deserialize_string_or_bool_opt};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub(crate) const GPG_PROBE_ARGS: &[&str] = &["--faked-system-time", "0!", "--version"];
pub fn gpg_supports_faked_system_time() -> bool {
crate::tool_detect::tool_runs_with_args("gpg", GPG_PROBE_ARGS)
}
pub fn gpg_supports_faked_system_time_with<F>(probe: F) -> bool
where
F: FnOnce(&[&str]) -> std::io::Result<std::process::Output>,
{
match probe(GPG_PROBE_ARGS) {
Ok(out) => out.status.success(),
Err(_) => false, }
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct SignConfig {
pub id: Option<String>,
pub artifacts: Option<String>,
pub cmd: Option<String>,
pub args: Option<Vec<String>>,
pub signature: Option<String>,
pub stdin: Option<String>,
pub stdin_file: Option<String>,
pub ids: Option<Vec<String>>,
#[serde(default)]
pub env: Option<Vec<String>>,
pub certificate: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub output: Option<StringOrBool>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
impl SignConfig {
pub const DEFAULT_ID: &'static str = "default";
pub const DEFAULT_ARTIFACTS: &'static str = "none";
pub const DEFAULT_ARTIFACTS_BINARY: &'static str = "binary";
pub const DEFAULT_SIGNATURE_TEMPLATE: &'static str = "{{ .Artifact }}.sig";
pub const DEFAULT_BINARY_SIGNATURE_TEMPLATE: &'static str = "{{ .Artifact }}.sig";
pub const DEFAULT_ARGS: &[&'static str] = &[
"--output",
"{{ .Signature }}",
"--detach-sig",
"{{ .Artifact }}",
];
pub fn resolved_id(&self) -> &str {
self.id.as_deref().unwrap_or(Self::DEFAULT_ID)
}
pub fn resolved_artifacts<'a>(&'a self, fallback: &'a str) -> &'a str {
self.artifacts.as_deref().unwrap_or(fallback)
}
pub fn resolved_signature_template<'a>(&'a self, default: &'a str) -> &'a str {
self.signature.as_deref().unwrap_or(default)
}
pub fn resolved_args(&self) -> Vec<String> {
self.args.clone().unwrap_or_else(|| {
Self::DEFAULT_ARGS
.iter()
.map(|s| (*s).to_string())
.collect()
})
}
pub fn is_gpg(&self) -> bool {
let artifacts = self.resolved_artifacts(Self::DEFAULT_ARTIFACTS);
if artifacts == "none" {
return false;
}
match self.cmd.as_deref() {
None => true, Some(cmd) => {
let basename = std::path::Path::new(cmd)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(cmd);
basename.starts_with("gpg")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct DockerSignConfig {
pub id: Option<String>,
pub artifacts: Option<String>,
pub cmd: Option<String>,
pub args: Option<Vec<String>>,
pub signature: Option<String>,
pub certificate: Option<String>,
pub ids: Option<Vec<String>>,
pub stdin: Option<String>,
pub stdin_file: Option<String>,
#[serde(default)]
pub env: Option<Vec<String>>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub output: Option<StringOrBool>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
impl DockerSignConfig {
pub const DEFAULT_ID: &'static str = "default";
pub const DEFAULT_CMD: &'static str = "cosign";
pub const DEFAULT_ARTIFACTS: &'static str = "";
pub const DEFAULT_ARGS: &[&'static str] = &[
"sign",
"--key=cosign.key",
"{{ .Artifact }}@{{ .Digest }}",
"--yes",
];
pub fn resolved_id(&self) -> &str {
self.id.as_deref().unwrap_or(Self::DEFAULT_ID)
}
pub fn resolved_cmd(&self) -> &str {
self.cmd.as_deref().unwrap_or(Self::DEFAULT_CMD)
}
pub fn resolved_artifacts(&self) -> &str {
self.artifacts.as_deref().unwrap_or(Self::DEFAULT_ARTIFACTS)
}
pub fn resolved_args(&self) -> Vec<String> {
self.args.clone().unwrap_or_else(|| {
Self::DEFAULT_ARGS
.iter()
.map(|s| (*s).to_string())
.collect()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sign_resolved_id_default() {
assert_eq!(SignConfig::default().resolved_id(), "default");
}
#[test]
fn sign_resolved_id_user_value_wins() {
let cfg = SignConfig {
id: Some("cosign".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_id(), "cosign");
}
#[test]
fn sign_resolved_artifacts_falls_back_to_supplied_default() {
let cfg = SignConfig::default();
assert_eq!(
cfg.resolved_artifacts(SignConfig::DEFAULT_ARTIFACTS),
"none"
);
assert_eq!(
cfg.resolved_artifacts(SignConfig::DEFAULT_ARTIFACTS_BINARY),
"binary"
);
}
#[test]
fn sign_resolved_artifacts_user_value_wins_over_fallback() {
let cfg = SignConfig {
artifacts: Some("checksum".to_string()),
..Default::default()
};
assert_eq!(
cfg.resolved_artifacts(SignConfig::DEFAULT_ARTIFACTS),
"checksum"
);
assert_eq!(
cfg.resolved_artifacts(SignConfig::DEFAULT_ARTIFACTS_BINARY),
"checksum"
);
}
#[test]
fn sign_resolved_signature_template_default_paths() {
let cfg = SignConfig::default();
assert_eq!(
cfg.resolved_signature_template(SignConfig::DEFAULT_SIGNATURE_TEMPLATE),
"{{ .Artifact }}.sig"
);
assert_eq!(
cfg.resolved_signature_template(SignConfig::DEFAULT_BINARY_SIGNATURE_TEMPLATE),
"{{ .Artifact }}.sig"
);
}
#[test]
fn sign_resolved_signature_template_user_value_wins() {
let cfg = SignConfig {
signature: Some("custom-{{ .Artifact }}.asc".to_string()),
..Default::default()
};
assert_eq!(
cfg.resolved_signature_template(SignConfig::DEFAULT_SIGNATURE_TEMPLATE),
"custom-{{ .Artifact }}.asc"
);
}
#[test]
fn sign_resolved_args_default_matches_goreleaser() {
let cfg = SignConfig::default();
assert_eq!(
cfg.resolved_args(),
vec![
"--output".to_string(),
"{{ .Signature }}".to_string(),
"--detach-sig".to_string(),
"{{ .Artifact }}".to_string(),
]
);
}
#[test]
fn sign_resolved_args_user_value_wins() {
let custom = vec!["sign".to_string(), "--key=k".to_string()];
let cfg = SignConfig {
args: Some(custom.clone()),
..Default::default()
};
assert_eq!(cfg.resolved_args(), custom);
}
#[test]
fn docker_sign_resolved_id_default() {
assert_eq!(DockerSignConfig::default().resolved_id(), "default");
}
#[test]
fn docker_sign_resolved_id_user_value_wins() {
let cfg = DockerSignConfig {
id: Some("custom".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_id(), "custom");
}
#[test]
fn docker_sign_resolved_cmd_default() {
assert_eq!(DockerSignConfig::default().resolved_cmd(), "cosign");
}
#[test]
fn docker_sign_resolved_cmd_user_value_wins() {
let cfg = DockerSignConfig {
cmd: Some("notation".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_cmd(), "notation");
}
#[test]
fn docker_sign_resolved_artifacts_default() {
assert_eq!(DockerSignConfig::default().resolved_artifacts(), "");
}
#[test]
fn docker_sign_resolved_artifacts_user_value_wins() {
let cfg = DockerSignConfig {
artifacts: Some("manifests".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_artifacts(), "manifests");
}
#[test]
fn docker_sign_resolved_args_default_matches_goreleaser() {
assert_eq!(
DockerSignConfig::default().resolved_args(),
vec![
"sign".to_string(),
"--key=cosign.key".to_string(),
"{{ .Artifact }}@{{ .Digest }}".to_string(),
"--yes".to_string(),
]
);
}
#[test]
fn docker_sign_resolved_args_user_value_wins() {
let custom = vec!["verify".to_string(), "--cert=c".to_string()];
let cfg = DockerSignConfig {
args: Some(custom.clone()),
..Default::default()
};
assert_eq!(cfg.resolved_args(), custom);
}
use std::process::{ExitStatus, Output};
#[cfg(unix)]
fn mk_exit_status(success: bool) -> ExitStatus {
use std::os::unix::process::ExitStatusExt;
if success {
ExitStatus::from_raw(0)
} else {
ExitStatus::from_raw(1 << 8)
}
}
#[cfg(windows)]
fn mk_exit_status(success: bool) -> ExitStatus {
use std::os::windows::process::ExitStatusExt;
ExitStatus::from_raw(if success { 0 } else { 1 })
}
fn mk_output(success: bool) -> Output {
Output {
status: mk_exit_status(success),
stdout: Vec::new(),
stderr: Vec::new(),
}
}
#[test]
fn gpg_probe_argv_is_pinned() {
assert_eq!(
super::GPG_PROBE_ARGS,
&["--faked-system-time", "0!", "--version"]
);
}
#[test]
fn gpg_faked_time_supported_returns_true_when_probe_succeeds() {
let supported = gpg_supports_faked_system_time_with(|args| {
assert_eq!(args, &["--faked-system-time", "0!", "--version"]);
Ok(mk_output(true))
});
assert!(supported);
}
#[test]
fn gpg_faked_time_unsupported_returns_false_when_probe_fails() {
let supported = gpg_supports_faked_system_time_with(|_| Ok(mk_output(false)));
assert!(!supported);
}
#[test]
fn gpg_faked_time_returns_false_when_probe_errors() {
let supported = gpg_supports_faked_system_time_with(|_| {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"gpg not on PATH",
))
});
assert!(!supported);
}
#[test]
fn is_gpg_default_cmd_with_signing_artifacts_is_true() {
let cfg = SignConfig {
artifacts: Some("all".to_string()),
..Default::default()
};
assert!(cfg.is_gpg());
}
#[test]
fn is_gpg_default_artifacts_none_is_false() {
let cfg = SignConfig::default();
assert!(!cfg.is_gpg());
}
#[test]
fn is_gpg_cosign_cmd_is_false() {
let cfg = SignConfig {
artifacts: Some("all".to_string()),
cmd: Some("cosign".to_string()),
..Default::default()
};
assert!(!cfg.is_gpg());
}
#[test]
fn is_gpg_gpg2_cmd_is_true() {
let cfg = SignConfig {
artifacts: Some("checksum".to_string()),
cmd: Some("gpg2".to_string()),
..Default::default()
};
assert!(cfg.is_gpg());
}
#[test]
fn is_gpg_absolute_gpg_path_is_true() {
let cfg = SignConfig {
artifacts: Some("binary".to_string()),
cmd: Some("/usr/local/bin/gpg".to_string()),
..Default::default()
};
assert!(cfg.is_gpg());
}
}