Skip to main content

cfgd_core/oci/sign/
mod.rs

1// Cosign signing + verification + SLSA in-toto attestations.
2// All shell-out goes through `crate::cosign_cmd()` (the controlled cosign layer
3// per module-boundaries.md).
4
5use crate::errors::OciError;
6
7/// Sign an OCI artifact with cosign.
8///
9/// If `key_path` is Some, uses `cosign sign --key <path>`.
10/// If `key_path` is None, uses keyless signing (Fulcio/Rekor via OIDC).
11pub fn sign_artifact(artifact_ref: &str, key_path: Option<&str>) -> Result<(), OciError> {
12    crate::require_cosign().map_err(|_| OciError::ToolNotFound {
13        tool: "cosign".to_string(),
14    })?;
15
16    let mut cmd = crate::cosign_cmd();
17    cmd.arg("sign");
18
19    if let Some(key) = key_path {
20        cmd.arg("--key").arg(key);
21    } else {
22        cmd.arg("--yes");
23    }
24
25    cmd.arg(artifact_ref);
26
27    let output = cmd.output().map_err(|e| OciError::SigningError {
28        message: format!("failed to run cosign: {e}"),
29    })?;
30
31    if !output.status.success() {
32        return Err(OciError::SigningError {
33            message: format!(
34                "cosign sign failed: {}",
35                crate::stderr_lossy_trimmed(&output)
36            ),
37        });
38    }
39
40    tracing::info!(reference = artifact_ref, "artifact signed with cosign");
41    Ok(())
42}
43
44/// Options for cosign verification (signature or attestation).
45pub struct VerifyOptions<'a> {
46    /// Path to cosign public key for static key verification.
47    pub key: Option<&'a str>,
48    /// Certificate identity regexp for keyless verification.
49    pub identity: Option<&'a str>,
50    /// Certificate OIDC issuer regexp for keyless verification.
51    pub issuer: Option<&'a str>,
52}
53
54/// Validate that keyless verification has at least one identity constraint.
55fn validate_verify_options(opts: &VerifyOptions<'_>) -> Result<(), OciError> {
56    if opts.key.is_none() && opts.identity.is_none() && opts.issuer.is_none() {
57        return Err(OciError::VerificationFailed {
58            reference: String::new(),
59            message: "keyless verification requires identity or issuer constraint (use --key, or provide VerifyOptions.identity/issuer)".to_string(),
60        });
61    }
62    Ok(())
63}
64
65/// Apply verification args to a cosign command.
66fn apply_verify_args(cmd: &mut std::process::Command, opts: &VerifyOptions<'_>) {
67    if let Some(key) = opts.key {
68        cmd.arg("--key").arg(key);
69    } else {
70        let identity = opts.identity.unwrap_or(".*");
71        let issuer = opts.issuer.unwrap_or(".*");
72        cmd.arg("--certificate-identity-regexp").arg(identity);
73        cmd.arg("--certificate-oidc-issuer-regexp").arg(issuer);
74    }
75}
76
77/// Verify the cosign signature on an OCI artifact.
78///
79/// Uses `cosign verify --key <path>` for static key, or keyless verification
80/// with certificate identity/issuer constraints from `VerifyOptions`.
81pub fn verify_signature(artifact_ref: &str, opts: &VerifyOptions<'_>) -> Result<(), OciError> {
82    validate_verify_options(opts)?;
83
84    crate::require_cosign().map_err(|_| OciError::ToolNotFound {
85        tool: "cosign".to_string(),
86    })?;
87
88    let mut cmd = crate::cosign_cmd();
89    cmd.arg("verify");
90    apply_verify_args(&mut cmd, opts);
91    cmd.arg(artifact_ref);
92
93    let output = cmd.output().map_err(|e| OciError::VerificationFailed {
94        reference: artifact_ref.to_string(),
95        message: format!("failed to run cosign: {e}"),
96    })?;
97
98    if !output.status.success() {
99        return Err(OciError::VerificationFailed {
100            reference: artifact_ref.to_string(),
101            message: format!(
102                "cosign verify failed: {}",
103                crate::stderr_lossy_trimmed(&output)
104            ),
105        });
106    }
107
108    tracing::info!(reference = artifact_ref, "signature verified");
109    Ok(())
110}
111
112// ---------------------------------------------------------------------------
113// Attestations (SLSA provenance / in-toto)
114// ---------------------------------------------------------------------------
115
116/// Generate a SLSA v1 provenance predicate JSON for a module artifact.
117pub fn generate_slsa_provenance(
118    artifact_ref: &str,
119    digest: &str,
120    source_repo: &str,
121    source_commit: &str,
122) -> Result<String, OciError> {
123    let now = crate::utc_now_iso8601();
124    serde_json::to_string_pretty(&serde_json::json!({
125        "_type": "https://in-toto.io/Statement/v1",
126        "predicateType": "https://slsa.dev/provenance/v1",
127        "subject": [{
128            "name": artifact_ref,
129            "digest": {
130                "sha256": crate::strip_sha256_prefix(digest),
131            }
132        }],
133        "predicate": {
134            "buildDefinition": {
135                "buildType": "https://cfgd.io/ModuleBuild/v1",
136                "externalParameters": {
137                    "source": {
138                        "uri": source_repo,
139                        "digest": { "gitCommit": source_commit },
140                    }
141                },
142            },
143            "runDetails": {
144                "builder": {
145                    "id": "https://cfgd.io/builder/v1",
146                },
147                "metadata": {
148                    "invocationId": &now,
149                    "startedOn": &now,
150                }
151            }
152        }
153    }))
154    .map_err(|e| OciError::AttestationError {
155        message: format!("failed to serialize SLSA provenance: {e}"),
156    })
157}
158
159/// Attach an in-toto attestation to an OCI artifact using cosign.
160pub fn attach_attestation(
161    artifact_ref: &str,
162    attestation_path: &str,
163    key_path: Option<&str>,
164) -> Result<(), OciError> {
165    crate::require_cosign().map_err(|_| OciError::ToolNotFound {
166        tool: "cosign".to_string(),
167    })?;
168
169    let mut cmd = crate::cosign_cmd();
170    cmd.arg("attest");
171
172    if let Some(key) = key_path {
173        cmd.arg("--key").arg(key);
174    } else {
175        cmd.arg("--yes");
176    }
177
178    cmd.arg("--predicate")
179        .arg(attestation_path)
180        .arg("--type")
181        .arg("slsaprovenance")
182        .arg(artifact_ref);
183
184    let output = cmd.output().map_err(|e| OciError::AttestationError {
185        message: format!("failed to run cosign attest: {e}"),
186    })?;
187
188    if !output.status.success() {
189        return Err(OciError::AttestationError {
190            message: format!(
191                "cosign attest failed: {}",
192                crate::stderr_lossy_trimmed(&output)
193            ),
194        });
195    }
196
197    tracing::info!(reference = artifact_ref, "attestation attached");
198    Ok(())
199}
200
201/// Verify an in-toto attestation on an OCI artifact.
202pub fn verify_attestation(
203    artifact_ref: &str,
204    predicate_type: &str,
205    opts: &VerifyOptions<'_>,
206) -> Result<(), OciError> {
207    validate_verify_options(opts)?;
208
209    crate::require_cosign().map_err(|_| OciError::ToolNotFound {
210        tool: "cosign".to_string(),
211    })?;
212
213    let mut cmd = crate::cosign_cmd();
214    cmd.arg("verify-attestation");
215    apply_verify_args(&mut cmd, opts);
216    cmd.arg("--type").arg(predicate_type).arg(artifact_ref);
217
218    let output = cmd.output().map_err(|e| OciError::AttestationError {
219        message: format!("failed to run cosign verify-attestation: {e}"),
220    })?;
221
222    if !output.status.success() {
223        return Err(OciError::AttestationError {
224            message: format!(
225                "attestation verification failed: {}",
226                crate::stderr_lossy_trimmed(&output)
227            ),
228        });
229    }
230
231    tracing::info!(reference = artifact_ref, "attestation verified");
232    Ok(())
233}
234
235#[cfg(test)]
236mod tests;