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