bc-envelope-cli 0.35.0

Gordian Envelope Command Line Tool.
Documentation
use anyhow::{Result, bail};
use bc_components::{
    PrivateKeyBase, PrivateKeys, Signer, SigningOptions, SigningPrivateKey,
};
use bc_envelope::prelude::*;
use clap::Args;
use known_values::NOTE;

use super::generate::HashType;
use crate::{EnvelopeArgs, EnvelopeArgsLike};

/// Sign the envelope subject with the provided signer(s).
#[derive(Debug, Args)]
#[group(skip)]
pub struct CommandArgs {
    /// The signer to sign the envelope subject with. May be a private key base
    /// (ur:crypto-prvkey-base), private keys (ur:crypto-prvkeys), or a signing
    /// private key (ur:signing-private-key).
    ///
    /// Multiple signers may be provided.
    #[arg(long, short)]
    signer: Vec<String>,

    /// An optional note to add to the envelope.
    #[arg(long)]
    note: Option<String>,

    /// Namespace for SSH signatures.
    #[arg(long, default_value = "envelope")]
    namespace: String,

    /// Hash algorithm for SSH signatures.
    #[arg(long, default_value = "sha256")]
    hash_type: HashType,

    #[command(flatten)]
    envelope_args: EnvelopeArgs,
}

impl EnvelopeArgsLike for CommandArgs {
    fn envelope(&self) -> Option<&str> { self.envelope_args.envelope() }
}

impl crate::Exec for CommandArgs {
    fn exec(&self) -> Result<String> {
        if self.signer.is_empty() {
            if let Some(envelope_arg) = self.envelope()
                && looks_like_signer(envelope_arg)
            {
                bail!(
                    "signer keys must be passed with --signer/-s; did you mean: envelope sign --signer <key> [ENVELOPE]?"
                );
            }
            bail!("at least one signer must be provided");
        }
        let envelope = self.read_envelope()?;
        let mut private_key_bases: Vec<PrivateKeyBase> = Vec::new();
        let mut private_keys: Vec<PrivateKeys> = Vec::new();
        let mut signing_private_keys: Vec<SigningPrivateKey> = Vec::new();
        let mut signing_options: Vec<Option<SigningOptions>> = Vec::new();
        for s in &self.signer {
            if let Ok(key) = PrivateKeyBase::from_ur_string(s) {
                private_key_bases.push(key);
            } else if let Ok(key) = PrivateKeys::from_ur_string(s) {
                private_keys.push(key);
            } else if let Ok(key) = SigningPrivateKey::from_ur_string(s) {
                if key.is_ssh() {
                    let namespace = self.namespace.clone();
                    let hash_alg = self.hash_type.to_ssh_hash_alg();
                    signing_options.push(Some(SigningOptions::Ssh {
                        namespace,
                        hash_alg,
                    }));
                } else {
                    signing_options.push(None);
                }
                signing_private_keys.push(key);
            } else {
                bail!("invalid signer: {}", s);
            }
        }
        let mut signers: Vec<(
            &dyn Signer,
            Option<SigningOptions>,
            Option<SignatureMetadata>,
        )> = Vec::new();
        for key in private_key_bases.iter() {
            signers.push((key as &dyn Signer, None, None));
        }
        for key in private_keys.iter() {
            let options = if key.signing_private_key().is_ssh() {
                let namespace = self.namespace.clone();
                let hash_alg = self.hash_type.to_ssh_hash_alg();
                Some(SigningOptions::Ssh { namespace, hash_alg })
            } else {
                None
            };
            signers.push((key as &dyn Signer, options, None));
        }
        for i in 0..signing_private_keys.len() {
            signers.push((
                &signing_private_keys[i] as &dyn Signer,
                signing_options[i].clone(),
                None,
            ));
        }
        if let Some(note) = &self.note {
            if signers.len() != 1 {
                bail!("can only add a note on a single signature");
            }
            let metadata =
                SignatureMetadata::new().with_assertion(NOTE, note.clone());
            Ok(envelope
                .add_signature_opt(
                    signers[0].0,
                    signers[0].1.clone(),
                    Some(metadata),
                )
                .ur_string())
        } else {
            Ok(envelope.add_signatures_opt(&signers).ur_string())
        }
    }
}

fn looks_like_signer(value: &str) -> bool {
    PrivateKeyBase::from_ur_string(value).is_ok()
        || PrivateKeys::from_ur_string(value).is_ok()
        || SigningPrivateKey::from_ur_string(value).is_ok()
}