cfgd_core/oci/sign/
mod.rs1use crate::errors::OciError;
6
7pub 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
44pub struct VerifyOptions<'a> {
46 pub key: Option<&'a str>,
48 pub identity: Option<&'a str>,
50 pub issuer: Option<&'a str>,
52}
53
54fn 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
65fn 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
77pub 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
112pub 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
159pub 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
201pub 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;