Skip to main content

packc/cli/
verify.rs

1#![forbid(unsafe_code)]
2
3use std::fs;
4use std::path::PathBuf;
5
6use anyhow::{Context, Result};
7use clap::Parser;
8use ed25519_dalek::VerifyingKey;
9use ed25519_dalek::pkcs8::DecodePublicKey;
10use greentic_types::{PackManifest, SignatureAlgorithm, encode_pack_manifest};
11
12#[derive(Debug, Parser)]
13pub struct VerifyArgs {
14    /// Path to the pack directory containing pack.yaml
15    #[arg(long = "pack", value_name = "DIR")]
16    pub pack: PathBuf,
17
18    /// Path to manifest.cbor (defaults to <pack>/dist/manifest.cbor)
19    #[arg(long = "manifest", value_name = "FILE")]
20    pub manifest: Option<PathBuf>,
21
22    /// Ed25519 public key in PKCS#8 PEM format
23    #[arg(long = "key", value_name = "FILE")]
24    pub key: PathBuf,
25}
26
27pub fn handle(args: VerifyArgs, json: bool) -> Result<()> {
28    let pack_dir = args
29        .pack
30        .canonicalize()
31        .with_context(|| format!("failed to resolve pack dir {}", args.pack.display()))?;
32    let manifest_path = args
33        .manifest
34        .map(|p| if p.is_relative() { pack_dir.join(p) } else { p })
35        .unwrap_or_else(|| pack_dir.join("dist").join("manifest.cbor"));
36
37    let manifest_bytes = fs::read(&manifest_path)
38        .with_context(|| format!("failed to read {}", manifest_path.display()))?;
39    let manifest: PackManifest = greentic_types::decode_pack_manifest(&manifest_bytes)
40        .context("manifest.cbor is not a valid PackManifest")?;
41
42    if manifest.signatures.signatures.is_empty() {
43        anyhow::bail!(
44            "{}",
45            crate::cli_i18n::t("cli.verify.error.no_signatures_present")
46        );
47    }
48
49    let public_pem = fs::read_to_string(&args.key)
50        .with_context(|| format!("failed to read public key {}", args.key.display()))?;
51    let verifying_key =
52        VerifyingKey::from_public_key_pem(&public_pem).context("failed to parse public key")?;
53
54    let unsigned_bytes = encode_unsigned(&manifest)?;
55
56    let mut verified = false;
57    let mut errors = Vec::new();
58    for sig in &manifest.signatures.signatures {
59        if sig.algorithm != SignatureAlgorithm::Ed25519 {
60            errors.push(format!("unsupported algorithm {:?}", sig.algorithm));
61            continue;
62        }
63        let Ok(signature) = ed25519_dalek::Signature::try_from(sig.signature.as_slice()) else {
64            errors.push("invalid signature bytes".to_string());
65            continue;
66        };
67        if verifying_key
68            .verify_strict(&unsigned_bytes, &signature)
69            .is_ok()
70        {
71            verified = true;
72            break;
73        } else {
74            errors.push("signature verification failed".to_string());
75        }
76    }
77
78    if !verified {
79        anyhow::bail!(
80            "{}",
81            crate::cli_i18n::tf(
82                "cli.verify.error.no_signatures_verified",
83                &[&errors.join(", ")]
84            )
85        );
86    }
87
88    if json {
89        println!(
90            "{}",
91            serde_json::to_string_pretty(&serde_json::json!({
92                "status": crate::cli_i18n::t("cli.verify.status.verified"),
93                "manifest": manifest_path,
94                "signatures": manifest.signatures.signatures.len(),
95            }))?
96        );
97    } else {
98        println!("{}", crate::cli_i18n::t("cli.verify.verified_manifest"));
99        println!(
100            "{}",
101            crate::cli_i18n::tf(
102                "cli.verify.manifest",
103                &[&manifest_path.display().to_string()]
104            )
105        );
106        println!(
107            "{}",
108            crate::cli_i18n::tf(
109                "cli.verify.signatures_checked",
110                &[&manifest.signatures.signatures.len().to_string()]
111            )
112        );
113    }
114
115    Ok(())
116}
117
118fn encode_unsigned(manifest: &PackManifest) -> Result<Vec<u8>> {
119    let mut unsigned = manifest.clone();
120    unsigned.signatures.signatures.clear();
121    encode_pack_manifest(&unsigned).context("failed to encode unsigned manifest")
122}