blue_build_process_management/drivers/
cosign_driver.rs1use std::{fmt::Debug, fs, path::Path};
2
3use blue_build_utils::{
4 constants::{COSIGN_PASSWORD, COSIGN_PUB_PATH, COSIGN_YES},
5 credentials::Credentials,
6 semver::Version,
7};
8use colored::Colorize;
9use comlexr::{cmd, pipe};
10use log::{debug, trace};
11use miette::{Context, IntoDiagnostic, Result, bail};
12use semver::VersionReq;
13use serde::Deserialize;
14
15use crate::drivers::{
16 DriverVersion,
17 opts::{PrivateKey, VerifyType},
18};
19
20use super::{
21 SigningDriver,
22 opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
23};
24
25#[derive(Debug, Clone, Deserialize)]
26#[serde(rename_all = "camelCase")]
27struct VersionJson {
28 git_version: Version,
29}
30
31#[derive(Debug)]
32pub struct CosignDriver;
33
34impl CosignDriver {
35 fn is_v3() -> bool {
36 Self::version().is_ok_and(|version| {
37 VersionReq::parse(">=3, <4").is_ok_and(|req| req.matches(&version))
38 })
39 }
40}
41
42impl DriverVersion for CosignDriver {
43 const VERSION_REQ: &'static str = ">=2";
44
45 fn version() -> Result<Version> {
46 trace!("CosignDriver::version()");
47
48 let output = {
49 let c = cmd!("cosign", "version", "--json");
50 trace!("{c:?}");
51 c
52 }
53 .output()
54 .into_diagnostic()?;
55
56 let version_json: VersionJson = serde_json::from_slice(&output.stdout).into_diagnostic()?;
57
58 Ok(version_json.git_version)
59 }
60}
61
62impl SigningDriver for CosignDriver {
63 fn generate_key_pair(opts: GenerateKeyPairOpts) -> Result<()> {
64 let path = opts.dir.unwrap_or_else(|| Path::new("."));
65
66 let status = {
67 let c = cmd!(
68 cd path;
69 env {
70 COSIGN_PASSWORD: "",
71 COSIGN_YES: "true",
72 };
73 "cosign",
74 "generate-key-pair",
75 );
76 trace!("{c:?}");
77 c
78 }
79 .status()
80 .into_diagnostic()?;
81
82 if !status.success() {
83 bail!("Failed to generate cosign key-pair!");
84 }
85
86 Ok(())
87 }
88
89 fn check_signing_files(opts: CheckKeyPairOpts) -> Result<()> {
90 let path = opts.dir.unwrap_or_else(|| Path::new("."));
91 let priv_key = PrivateKey::new(path)?;
92
93 let output = {
94 let c = cmd!(
95 env {
96 COSIGN_PASSWORD: "",
97 COSIGN_YES: "true"
98 };
99 "cosign",
100 "public-key",
101 format!("--key={priv_key}"),
102 );
103 trace!("{c:?}");
104 c
105 }
106 .output()
107 .into_diagnostic()?;
108
109 if !output.status.success() {
110 bail!(
111 "Failed to run cosign public-key: {}",
112 String::from_utf8_lossy(&output.stderr)
113 );
114 }
115
116 let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
117 let found_pub_key = fs::read_to_string(path.join(COSIGN_PUB_PATH))
118 .into_diagnostic()
119 .with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
120 trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
121
122 if calculated_pub_key.trim() == found_pub_key.trim() {
123 debug!("Cosign files match, continuing build");
124 Ok(())
125 } else {
126 bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
127 }
128 }
129
130 fn signing_login(server: &str) -> Result<()> {
131 trace!("CosignDriver::signing_login()");
132
133 if let Some(Credentials::Basic { username, password }) = Credentials::get(server) {
134 let output = pipe!(
135 stdin = password.value();
136 {
137 let c = cmd!(
138 "cosign",
139 "login",
140 "-u",
141 &username,
142 "--password-stdin",
143 server,
144 );
145 trace!("{c:?}");
146 c
147 }
148 )
149 .output()
150 .into_diagnostic()?;
151
152 if !output.status.success() {
153 let err_out = String::from_utf8_lossy(&output.stderr);
154 bail!("Failed to login for cosign:\n{}", err_out.trim());
155 }
156 debug!("Logged into {server}");
157 }
158 Ok(())
159 }
160
161 fn sign(
162 SignOpts {
163 image,
164 metadata,
165 key,
166 }: SignOpts,
167 ) -> Result<()> {
168 let image = image.clone_with_digest(metadata.digest().into());
169 let status = {
170 let c = cmd!(
171 env {
172 COSIGN_PASSWORD: "",
173 COSIGN_YES: "true",
174 };
175 "cosign",
176 "sign",
177 if let Some(key) = key => format!("--key={key}"),
178 if Self::is_v3() => [
179 "--new-bundle-format=false",
180 "--use-signing-config=false",
181 ],
182 "--recursive",
183 image.to_string(),
184 );
185 trace!("{c:?}");
186 c
187 }
188 .status()
189 .into_diagnostic()?;
190
191 if !status.success() {
192 bail!("Failed to sign {}", image.to_string().bold().red());
193 }
194
195 Ok(())
196 }
197
198 fn verify(opts: VerifyOpts) -> Result<()> {
199 let status = {
200 let c = cmd!(
201 "cosign",
202 "verify",
203 match &opts.verify_type {
204 VerifyType::File(path) => format!("--key={}", path.display()),
205 VerifyType::Keyless { issuer, identity } => [
206 "--certificate-identity-regexp",
207 &**identity,
208 "--certificate-oidc-issuer",
209 &**issuer,
210 ],
211 },
212 opts.image.to_string(),
213 );
214 trace!("{c:?}");
215 c
216 }
217 .status()
218 .into_diagnostic()?;
219
220 if !status.success() {
221 bail!("Failed to verify {}", opts.image.to_string().bold().red());
222 }
223
224 Ok(())
225 }
226}
227
228#[cfg(test)]
229mod test {
230 use std::{fs, path::Path};
231
232 use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
233 use tempfile::TempDir;
234
235 use crate::drivers::{
236 SigningDriver,
237 opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
238 };
239
240 use super::CosignDriver;
241
242 #[test]
243 fn generate_key_pair() {
244 let tempdir = TempDir::new().unwrap();
245
246 CosignDriver::generate_key_pair(GenerateKeyPairOpts::builder().dir(tempdir.path()).build())
247 .unwrap();
248
249 eprintln!(
250 "Private key:\n{}",
251 fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
252 );
253 eprintln!(
254 "Public key:\n{}",
255 fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
256 );
257
258 CosignDriver::check_signing_files(CheckKeyPairOpts::builder().dir(tempdir.path()).build())
259 .unwrap();
260 }
261
262 #[test]
263 fn check_key_pairs() {
264 let path = Path::new("../test-files/keys");
265
266 CosignDriver::check_signing_files(CheckKeyPairOpts::builder().dir(path).build()).unwrap();
267 }
268
269 #[test]
270 fn compatibility() {
271 use crate::drivers::sigstore_driver::SigstoreDriver;
272
273 let tempdir = TempDir::new().unwrap();
274
275 CosignDriver::generate_key_pair(GenerateKeyPairOpts::builder().dir(tempdir.path()).build())
276 .unwrap();
277
278 eprintln!(
279 "Private key:\n{}",
280 fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
281 );
282 eprintln!(
283 "Public key:\n{}",
284 fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
285 );
286
287 SigstoreDriver::check_signing_files(
288 CheckKeyPairOpts::builder().dir(tempdir.path()).build(),
289 )
290 .unwrap();
291 }
292}