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 #[arg(long = "pack", value_name = "DIR")]
16 pub pack: PathBuf,
17
18 #[arg(long = "manifest", value_name = "FILE")]
20 pub manifest: Option<PathBuf>,
21
22 #[arg(long = "key", value_name = "FILE")]
24 pub key: PathBuf,
25
26 #[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}