openpgp-card-tools 0.11.11

A tool for inspecting, configuring and using OpenPGP cards
// SPDX-FileCopyrightText: 2021-2024 Heiko Schaefer <heiko@schaefer.name>
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
// SPDX-FileCopyrightText: 2024 David Runge <dave@sleepmap.de>
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::io::Cursor;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Result};
use clap::Parser;
use openpgp_card::ocard::KeyType;
use openpgp_card_rpgp::CardSlot;
use pgp::composed::{ArmorOptions, CleartextSignedMessage, MessageBuilder};
use pgp::types::{Password, SecretKeyTrait};
use rand::thread_rng;

use crate::util;

#[derive(Parser, Debug)]
pub struct SignCommand {
    #[arg(
        name = "card ident",
        short = 'c',
        long = "card",
        help = "Identifier of the card to use"
    )]
    pub ident: String,

    #[arg(
        name = "User PIN file",
        short = 'p',
        long = "user-pin",
        help = "Optionally, get User PIN from a file"
    )]
    pub user_pin: Option<PathBuf>,

    #[command(subcommand)]
    pub form: SignSubCommand,
}

#[derive(Debug, Parser, PartialEq)]
pub enum SignSubCommand {
    /// Create a detached signature
    Detached {
        /// Input file (stdin if unset)
        #[arg(name = "input")]
        input: Option<PathBuf>,

        /// Output file (stdout if unset)
        #[arg(name = "output", long = "output", short = 'o')]
        output: Option<PathBuf>,
    },

    /// Create a cleartext signature
    Cleartext {
        /// Input file (stdin if unset)
        #[arg(name = "input")]
        input: Option<PathBuf>,

        /// Output file (stdout if unset)
        #[arg(name = "output", long = "output", short = 'o')]
        output: Option<PathBuf>,
    },

    /// Create an inline signature
    Inline {
        /// Input file (stdin if unset)
        #[arg(name = "input")]
        input: Option<PathBuf>,

        /// Output file (stdout if unset)
        #[arg(name = "output", long = "output", short = 'o')]
        output: Option<PathBuf>,
    },
}

impl SignSubCommand {
    /// Get optional input and output Paths from the subcommand
    pub fn get_input_output(&self) -> (Option<&Path>, Option<&Path>) {
        match self {
            SignSubCommand::Detached { input, output }
            | SignSubCommand::Cleartext { input, output }
            | SignSubCommand::Inline { input, output } => (input.as_deref(), output.as_deref()),
        }
    }

    /// Evaluate whether the subcommand is used for creating a cleartext signature
    pub fn is_cleartext(&self) -> bool {
        matches!(self, SignSubCommand::Cleartext { .. })
    }

    /// Evaluate whether the subcommand is used for creating a detached signature
    pub fn is_detached(&self) -> bool {
        matches!(self, SignSubCommand::Detached { .. })
    }
}

pub fn sign(command: SignCommand) -> Result<(), Box<dyn std::error::Error>> {
    sign_message(&command.ident, command.user_pin, command.form)
}

pub fn sign_message(
    ident: &str,
    pin_file: Option<PathBuf>,
    form: SignSubCommand,
) -> Result<(), Box<dyn std::error::Error>> {
    let cleartext = form.is_cleartext();
    let detached = form.is_detached();
    let (input, output) = form.get_input_output();

    let mut input = util::open_or_stdin(input)?;

    let mut open = util::open_card(ident)?;
    let mut tx = open.transaction()?;

    if tx.fingerprints()?.signature().is_none() {
        return Err(anyhow!("Can't sign: this card has no key in the signing slot.").into());
    }

    let user_pin = util::get_pin(&mut tx, pin_file, crate::ENTER_USER_PIN)?;
    let _ = util::verify_to_sign(&mut tx, user_pin)?;

    let cs = CardSlot::init_from_card(&mut tx, KeyType::Signing, &|| {
        eprintln!("Touch confirmation needed for signing");
    })?;

    let mut data = vec![];
    input.read_to_end(&mut data)?;

    let mut sink = util::open_or_stdout(output)?;

    if cleartext {
        // Cleartext framework signature

        let text = String::from_utf8(data)?;

        let csf = CleartextSignedMessage::sign(thread_rng(), &text, &cs, &Password::empty())?;
        csf.to_armored_writer(&mut sink, ArmorOptions::default())?;
    } else if detached {
        // Detached signature

        let signature = cs.sign_data(
            &data,
            false, // binary signature
            &Password::empty(),
            cs.hash_alg(),
        )?;

        rpgpie::signature::save(&[signature], true, &mut sink)?
    } else {
        // Inline signature
        // (Complete OPS message, including both payload and signature)

        let mut builder = MessageBuilder::from_reader("", Cursor::new(data));
        builder.sign(&cs, Password::empty(), cs.hash_alg());

        builder.to_armored_writer(thread_rng(), ArmorOptions::default(), sink)?;
    }

    Ok(())
}