rsop-oct 0.1.9

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},
    packet::Signature,
    types::{KeyDetails, Password, SigningKey, Timestamp},
};
use rand::thread_rng;
use rpgpie::{certificate::Checked, key::ComponentKeyPub};

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

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<()> {
        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.clone().into();

            let mut keys: Vec<ComponentKeyPub> = ccert
                .valid_signing_capable_component_keys_at(Timestamp::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, openpgp_card::ocard::KeyType::Signing)
                            .map_err(|_| pgp::errors::Error::Message {
                                message: "Couldn't get card".to_string(),
                                backtrace: None,
                            })?;

                        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(),
                                    ds.public_params().hash_alg(),
                                );

                                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, openpgp_card::ocard::KeyType::Signing)
                .map_err(|_| sop::errors::Error::UnspecifiedFailure)?;

            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 {
                Err(sop::errors::Error::KeyCannotSign)
            }
        }
    }
}