Skip to main content

packc/cli/
sign.rs

1#![forbid(unsafe_code)]
2
3use std::fs;
4use std::path::PathBuf;
5
6use anyhow::{Context, Result};
7use clap::Parser;
8use ed25519_dalek::pkcs8::DecodePrivateKey;
9use ed25519_dalek::{Signer, SigningKey};
10use greentic_types::{PackManifest, Signature, SignatureAlgorithm, encode_pack_manifest};
11
12#[derive(Debug, Parser)]
13pub struct SignArgs {
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 private key in PKCS#8 PEM format
23    #[arg(long = "key", value_name = "FILE")]
24    pub key: PathBuf,
25
26    /// Optional key identifier to embed alongside the signature
27    #[arg(long = "key-id", value_name = "ID", default_value = "default")]
28    pub key_id: String,
29}
30
31pub fn handle(args: SignArgs, json: bool) -> Result<()> {
32    let pack_dir = args
33        .pack
34        .canonicalize()
35        .with_context(|| format!("failed to resolve pack dir {}", args.pack.display()))?;
36    let manifest_path = args
37        .manifest
38        .map(|p| if p.is_relative() { pack_dir.join(p) } else { p })
39        .unwrap_or_else(|| pack_dir.join("dist").join("manifest.cbor"));
40
41    let manifest_bytes = fs::read(&manifest_path)
42        .with_context(|| format!("failed to read {}", manifest_path.display()))?;
43    let manifest: PackManifest = greentic_types::decode_pack_manifest(&manifest_bytes)
44        .context("manifest.cbor is not a valid PackManifest")?;
45
46    let unsigned_bytes = encode_unsigned(&manifest)?;
47
48    let private_pem = fs::read_to_string(&args.key)
49        .with_context(|| format!("failed to read private key {}", args.key.display()))?;
50    let signing_key =
51        SigningKey::from_pkcs8_pem(&private_pem).context("failed to parse ed25519 private key")?;
52    let signature_bytes = signing_key.sign(&unsigned_bytes).to_bytes().to_vec();
53
54    let mut signed_manifest = manifest.clone();
55    signed_manifest.signatures.signatures.push(Signature::new(
56        args.key_id.clone(),
57        SignatureAlgorithm::Ed25519,
58        signature_bytes.clone(),
59    ));
60    let encoded =
61        encode_pack_manifest(&signed_manifest).context("failed to encode signed manifest")?;
62
63    fs::write(&manifest_path, &encoded)
64        .with_context(|| format!("failed to write {}", manifest_path.display()))?;
65
66    if json {
67        println!(
68            "{}",
69            serde_json::to_string_pretty(&serde_json::json!({
70                "status": "signed",
71                "manifest": manifest_path,
72                "key_id": args.key_id,
73                "signatures": signed_manifest.signatures.signatures.len(),
74            }))?
75        );
76    } else {
77        println!(
78            "signed manifest\n  manifest: {}\n  key_id: {}\n  signatures: {}",
79            manifest_path.display(),
80            args.key_id,
81            signed_manifest.signatures.signatures.len()
82        );
83    }
84
85    Ok(())
86}
87
88fn encode_unsigned(manifest: &PackManifest) -> Result<Vec<u8>> {
89    let mut unsigned = manifest.clone();
90    unsigned.signatures.signatures.clear();
91    encode_pack_manifest(&unsigned).context("failed to encode unsigned manifest")
92}