use crate::config::{StringOrBool, deserialize_string_or_bool_opt};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
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>,
pub authenticode: Option<AuthenticodeConfig>,
pub verify: Option<SignVerifyConfig>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct SignVerifyConfig {
pub enabled: Option<bool>,
pub certificate_identity: Option<String>,
pub certificate_identity_regexp: Option<String>,
pub certificate_oidc_issuer: Option<String>,
}
impl SignVerifyConfig {
pub fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(true)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct AuthenticodeConfig {
pub cert_file: Option<String>,
pub cert_env: Option<String>,
pub password_env: Option<String>,
pub timestamp_url: Option<String>,
pub name: Option<String>,
pub url: Option<String>,
pub tool: Option<String>,
pub artifacts: Option<String>,
pub required: Option<bool>,
}
impl AuthenticodeConfig {
pub const DEFAULT_CERT_ENV: &'static str = "WINDOWS_CERT_FILE";
pub const DEFAULT_PASSWORD_ENV: &'static str = "WINDOWS_CERT_PASSWORD";
pub const DEFAULT_TIMESTAMP_URL: &'static str = "http://timestamp.digicert.com";
pub const DEFAULT_ARTIFACTS: &'static str = "windows";
pub const DEFAULT_TOOL_WINDOWS: &'static str = "signtool";
pub const DEFAULT_TOOL_UNIX: &'static str = "osslsigncode";
pub fn resolved_cert_env(&self) -> &str {
self.cert_env.as_deref().unwrap_or(Self::DEFAULT_CERT_ENV)
}
pub fn resolved_password_env(&self) -> &str {
self.password_env
.as_deref()
.unwrap_or(Self::DEFAULT_PASSWORD_ENV)
}
pub fn resolved_timestamp_url(&self) -> &str {
self.timestamp_url
.as_deref()
.unwrap_or(Self::DEFAULT_TIMESTAMP_URL)
}
pub fn resolved_artifacts(&self) -> &str {
self.artifacts.as_deref().unwrap_or(Self::DEFAULT_ARTIFACTS)
}
pub fn resolved_tool(&self) -> &str {
self.tool.as_deref().unwrap_or({
if cfg!(windows) {
Self::DEFAULT_TOOL_WINDOWS
} else {
Self::DEFAULT_TOOL_UNIX
}
})
}
pub fn is_required(&self) -> bool {
self.required.unwrap_or(false)
}
}
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 verify_enabled(&self) -> bool {
self.verify
.as_ref()
.is_none_or(SignVerifyConfig::is_enabled)
}
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) => is_gpg_command(cmd),
}
}
}
pub fn is_gpg_command(cmd: &str) -> bool {
std::path::Path::new(cmd)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(cmd)
.starts_with("gpg")
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
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>,
pub verify: Option<SignVerifyConfig>,
#[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()
})
}
pub fn verify_enabled(&self) -> bool {
self.verify
.as_ref()
.is_none_or(SignVerifyConfig::is_enabled)
}
}
#[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);
}
#[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());
}
#[test]
fn is_gpg_command_matches_gpg_variants() {
for cmd in [
"gpg",
"gpg2",
"gpg.exe",
"/usr/bin/gpg",
"/usr/local/bin/gpg2",
] {
assert!(is_gpg_command(cmd), "{cmd} must classify as gpg");
}
}
#[test]
fn is_gpg_command_rejects_non_gpg() {
for cmd in [
"cosign",
"notation",
"/usr/bin/cosign",
"mygpg-wrapper/cosign",
] {
assert!(!is_gpg_command(cmd), "{cmd} must not classify as gpg");
}
}
#[test]
fn authenticode_resolved_cert_env_default() {
assert_eq!(
AuthenticodeConfig::default().resolved_cert_env(),
"WINDOWS_CERT_FILE"
);
assert_eq!(AuthenticodeConfig::DEFAULT_CERT_ENV, "WINDOWS_CERT_FILE");
}
#[test]
fn authenticode_resolved_cert_env_user_value_wins() {
let cfg = AuthenticodeConfig {
cert_env: Some("MY_CERT_PATH".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_cert_env(), "MY_CERT_PATH");
}
#[test]
fn authenticode_resolved_password_env_default() {
assert_eq!(
AuthenticodeConfig::default().resolved_password_env(),
"WINDOWS_CERT_PASSWORD"
);
assert_eq!(
AuthenticodeConfig::DEFAULT_PASSWORD_ENV,
"WINDOWS_CERT_PASSWORD"
);
}
#[test]
fn authenticode_resolved_password_env_user_value_wins() {
let cfg = AuthenticodeConfig {
password_env: Some("CERT_PW".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_password_env(), "CERT_PW");
}
#[test]
fn authenticode_resolved_timestamp_url_default() {
assert_eq!(
AuthenticodeConfig::default().resolved_timestamp_url(),
"http://timestamp.digicert.com"
);
assert_eq!(
AuthenticodeConfig::DEFAULT_TIMESTAMP_URL,
"http://timestamp.digicert.com"
);
}
#[test]
fn authenticode_resolved_timestamp_url_user_value_wins() {
let cfg = AuthenticodeConfig {
timestamp_url: Some("http://timestamp.sectigo.com".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_timestamp_url(), "http://timestamp.sectigo.com");
}
#[test]
fn authenticode_resolved_artifacts_default() {
assert_eq!(
AuthenticodeConfig::default().resolved_artifacts(),
"windows"
);
assert_eq!(AuthenticodeConfig::DEFAULT_ARTIFACTS, "windows");
}
#[test]
fn authenticode_resolved_artifacts_user_value_wins() {
let cfg = AuthenticodeConfig {
artifacts: Some("binary".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_artifacts(), "binary");
}
#[test]
fn authenticode_resolved_tool_host_default() {
let expected = if cfg!(windows) {
"signtool"
} else {
"osslsigncode"
};
assert_eq!(AuthenticodeConfig::default().resolved_tool(), expected);
}
#[test]
fn authenticode_resolved_tool_user_value_wins() {
let cfg = AuthenticodeConfig {
tool: Some("osslsigncode".to_string()),
..Default::default()
};
assert_eq!(cfg.resolved_tool(), "osslsigncode");
}
#[test]
fn authenticode_is_required_default_false() {
assert!(!AuthenticodeConfig::default().is_required());
}
#[test]
fn authenticode_is_required_user_value_wins() {
let cfg = AuthenticodeConfig {
required: Some(true),
..Default::default()
};
assert!(cfg.is_required());
}
}