rsop-oct 0.1.3

SOP CLI tool for OpenPGP card devices based on rPGP
Documentation
// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::io;

use pgp::composed::{ArmorOptions, CleartextSignedMessage, MessageBuilder};
use pgp::packet::Signature;
use pgp::types::{Password, PublicKeyTrait, SecretKeyTrait};
use rand::thread_rng;
use rpgpie::certificate::Checked;
use rpgpie::key::ComponentKeyPub;

use crate::card::get_card;
use crate::cmd::sign::Sign;
use crate::{card, Keys, RPGSOPOCT};

pub(crate) struct InlineSign {
    armor: bool,
    sign: Sign,
    mode: sop::ops::InlineSignAs,
}

impl InlineSign {
    pub(crate) fn new() -> Self {
        Self {
            armor: true,
            sign: Sign::new(),
            mode: Default::default(),
        }
    }
}

impl<'a> sop::ops::InlineSign<'a, RPGSOPOCT, Keys> for InlineSign {
    fn no_armor(mut self: Box<Self>) -> Box<dyn sop::ops::InlineSign<'a, RPGSOPOCT, Keys> + 'a> {
        self.armor = false;
        self
    }

    fn mode(
        mut self: Box<Self>,
        mode: sop::ops::InlineSignAs,
    ) -> Box<dyn sop::ops::InlineSign<'a, RPGSOPOCT, Keys> + 'a> {
        self.mode = mode;
        self
    }

    fn keys(
        mut self: Box<Self>,
        keys: &Keys,
    ) -> sop::Result<Box<dyn sop::ops::InlineSign<'a, RPGSOPOCT, Keys> + 'a>> {
        self.sign.add_signing_keys(keys)?;
        Ok(self)
    }

    fn with_key_password(
        mut self: Box<Self>,
        password: sop::Password,
    ) -> sop::Result<Box<dyn sop::ops::InlineSign<'a, RPGSOPOCT, Keys> + 'a>> {
        self.sign.with_key_password.push(password);
        Ok(self)
    }

    fn data<'d>(
        self: Box<Self>,
        data: &'d mut (dyn io::Read + Send + Sync),
    ) -> sop::Result<Box<dyn sop::ops::Ready + 'd>>
    where
        'a: 'd,
    {
        if self.sign.signers.is_empty() {
            return Err(sop::errors::Error::MissingArg);
        }

        if !self.armor && matches!(self.mode, sop::ops::InlineSignAs::ClearSigned) {
            return Err(sop::errors::Error::IncompatibleOptions);
        }

        Ok(Box::new(InlineSignReady {
            inline_sign: *self,
            data,
        }))
    }
}

struct InlineSignReady<'a> {
    inline_sign: InlineSign,
    data: &'a mut (dyn io::Read + Send + Sync),
}

impl sop::ops::Ready for InlineSignReady<'_> {
    fn to_writer(
        mut self: Box<Self>,
        mut sink: &mut (dyn io::Write + Send + Sync),
    ) -> sop::Result<()> {
        let hash_algo = self
            .inline_sign
            .sign
            .hash_algos
            .first()
            .cloned()
            .unwrap_or_default();

        assert!(!self.inline_sign.sign.signers.is_empty()); // FIXME

        // public key representations of signing-capable component keys.
        // we'll search for card-slots that contain those keys, in the next step.
        let mut signers: Vec<ComponentKeyPub> = vec![];
        for cert in self.inline_sign.sign.signers {
            let ccert: Checked = (&cert).into();

            let mut keys: Vec<ComponentKeyPub> = ccert
                .valid_signing_capable_component_keys_at(&chrono::offset::Utc::now())
                .iter()
                .map(|x| x.as_componentkey().clone())
                .collect();

            if keys.is_empty() {
                panic!(
                    "no signing capable component key found for signer {:02x?}",
                    cert.fingerprint()
                );
            }

            signers.append(&mut keys);
        }

        if self.inline_sign.mode == sop::ops::InlineSignAs::ClearSigned {
            let mut data = vec![];
            self.data.read_to_end(&mut data)?;

            let signers = |text: &str| -> Result<Vec<Signature>, pgp::errors::Error> {
                let mut sigs: Vec<Signature> = vec![];

                for ds in signers {
                    let mut card =
                        get_card(ds.public_params(), openpgp_card::ocard::KeyType::Signing)
                            .expect("FIXME");

                    let res = card::do_on_card(
                        &mut card,
                        openpgp_card::ocard::KeyType::Signing,
                        &self.inline_sign.sign.with_key_password,
                        &|| {},
                        |cs| {
                            let res =
                                cs.sign_data(text.as_bytes(), true, &Password::empty(), hash_algo);

                            if let Ok(sig) = res {
                                Ok(sig)
                            } else {
                                Err(crate::Error::Message(
                                    "Didn't get a Message from signer".to_string(),
                                ))
                            }
                        },
                    );

                    if let Ok(signature) = res {
                        sigs.push(signature);
                    } else {
                        // We failed to produce a signature, for some reason.
                        // Maybe it's password protection.
                        // TODO: return a more concrete error?

                        return Err(pgp::errors::Error::Message {
                            message: "Couldn't sign".to_string(),
                            backtrace: None,
                        });
                    }
                }

                Ok(sigs)
            };

            let text = core::str::from_utf8(&data).map_err(|_| sop::errors::Error::BadData)?;
            let Ok(csf) = CleartextSignedMessage::new_many(text, signers) else {
                // TODO: Check error conditions more closely, and return more specific errors.

                return Err(sop::errors::Error::KeyIsProtected);
            };

            csf.to_armored_writer(&mut sink, ArmorOptions::default())
                .expect("writing failed");

            Ok(())
        } else {
            if signers.len() != 1 {
                eprintln!(
                    "Found {} potential signing component keys, must be 1!",
                    signers.len()
                );
                return Err(sop::errors::Error::BadData);
            }

            let sig = &signers[0];

            let mut card = get_card(sig.public_params(), openpgp_card::ocard::KeyType::Signing)
                .expect("FIXME");

            let res = card::do_on_card(
                &mut card,
                openpgp_card::ocard::KeyType::Signing,
                &self.inline_sign.sign.with_key_password,
                &|| {},
                |cs| {
                    let mut builder = MessageBuilder::from_reader("", &mut self.data);

                    if self.inline_sign.mode == sop::ops::InlineSignAs::Text {
                        builder.sign_text();
                    }
                    builder.sign(cs, Password::empty(), cs.hash_alg());

                    let rng = thread_rng();
                    if self.inline_sign.armor {
                        builder
                            .to_armored_writer(rng, ArmorOptions::default(), sink)
                            .expect("write failure");
                    } else {
                        builder.to_writer(rng, sink).expect("write failure");
                    }

                    Ok(())
                },
            );

            if res.is_ok() {
                Ok(())
            } else {
                unimplemented!("error")
            }
        }
    }
}