Skip to main content

blue_build_process_management/drivers/
cosign_driver.rs

1use 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}