rsop-oct 0.1.9

SOP CLI tool for OpenPGP card devices based on rPGP
Documentation
use pgp::{
    composed::SignedPublicKey,
    packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData},
    types::{KeyDetails, Tag, Timestamp},
};
use rand::thread_rng;
use rpgpie::certificate::{Certificate, Checked};
use sop::Password;

use crate::{Certs, Keys, RPGSOPOCT, card, card::get_card};

pub(crate) struct CertifyUserID {
    userids: Vec<String>,
    with_key_password: Vec<Password>,
    no_require_self_sig: bool,
    certifiers: Vec<Certificate>,
}

impl CertifyUserID {
    pub(crate) fn new() -> CertifyUserID {
        Self {
            userids: Default::default(),
            with_key_password: Default::default(),
            no_require_self_sig: false,
            certifiers: Default::default(),
        }
    }
}

impl<'a> sop::ops::CertifyUserID<'a, RPGSOPOCT, Certs, Keys> for CertifyUserID {
    fn userid(
        mut self: Box<Self>,
        userid: String,
    ) -> Box<dyn sop::ops::CertifyUserID<'a, RPGSOPOCT, Certs, Keys> + 'a> {
        self.userids.push(userid);
        self
    }

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

    fn no_require_self_sig(
        mut self: Box<Self>,
    ) -> Box<dyn sop::ops::CertifyUserID<'a, RPGSOPOCT, Certs, Keys> + 'a> {
        self.no_require_self_sig = true;
        self
    }

    fn keys(
        mut self: Box<Self>,
        keys: &Keys,
    ) -> sop::Result<Box<dyn sop::ops::CertifyUserID<'a, RPGSOPOCT, Certs, Keys> + 'a>> {
        for key in &keys.keys {
            self.certifiers.push(key.clone());
        }
        Ok(self)
    }

    fn certify(self: Box<Self>, certs: &Certs) -> sop::Result<Certs> {
        if self.no_require_self_sig {
            unimplemented!("no-require-self-sig is currently unsupported"); // FIXME
        }

        let now = Timestamp::now();

        // Collect `Card` objects that we'll want to issue signatures with
        let mut cards = Vec::new();
        for certifier in self.certifiers {
            let ccert: Checked = certifier.clone().into();

            // check if certifier is valid, search for a suitable card slot

            if ccert
                .primary_valid_at(now)
                // FIXME: should be `KEY_CANNOT_CERTIFY`
                .map_err(|_e| sop::errors::Error::KeyCannotSign)?
            {
                // FIXME: check for "certification" key flag (expose flag status from rpgpie - how?)

                let spk: SignedPublicKey = certifier.into();
                let signer = spk.primary_key;

                log::info!(
                    "Trying to certify with signer: {:02x?}",
                    signer.fingerprint()
                );

                let card = get_card(&signer, openpgp_card::ocard::KeyType::Signing)
                    .map_err(|_| sop::errors::Error::UnspecifiedFailure)?;

                cards.push(card);
            } else {
                // FIXME: should be KEY_CANNOT_CERTIFY
                return Err(sop::errors::Error::KeyCannotSign);
            }
        }

        let mut res = vec![];

        for target in &certs.certs {
            // Additional certifications will be added to this copy (if no errors occur)
            let mut updated: SignedPublicKey = target.clone().into();

            for uid in &self.userids {
                // find matching valid userid for each (or error)

                let checked: Checked = target.clone().into();
                let checked_users = checked.user_ids();

                let Some(su) = checked_users
                    .iter()
                    .find(|user| user.id.id() == uid.as_bytes())
                else {
                    return Err(sop::errors::Error::CertUseridNoMatch);
                };

                for card in &mut cards {
                    // Certify the user id (or error)
                    let sig = card::do_on_card(
                        card,
                        openpgp_card::ocard::KeyType::Signing,
                        &self.with_key_password,
                        &|| {
                            eprintln!("Touch confirmation required for certification with rsop-oct")
                        },
                        |cs| {
                            let mut sc = SignatureConfig::from_key(
                                thread_rng(),
                                cs,
                                SignatureType::CertGeneric,
                            )?;

                            sc.hashed_subpackets = vec![
                                Subpacket::regular(SubpacketData::IssuerFingerprint(
                                    cs.fingerprint(),
                                ))?,
                                Subpacket::regular(SubpacketData::SignatureCreationTime(now))?,
                            ];
                            sc.unhashed_subpackets = vec![Subpacket::regular(
                                SubpacketData::IssuerKeyId(cs.legacy_key_id()),
                            )?];

                            sc.sign_certification_third_party(
                                cs,
                                &pgp::types::Password::empty(),
                                &updated.primary_key,
                                Tag::UserId,
                                &su.id,
                            )
                            .map_err(|e| crate::Error::OcardRpgp(e.into()))
                        },
                    )
                    .map_err(|e| {
                        // FIXME: better error handling
                        eprintln!("Error while using card: {e:?}");
                        sop::errors::Error::UnspecifiedFailure
                    })?;

                    // Then add the signature to the SignedUser in a new copy of the original
                    // `target` spk
                    if let Some(su) = updated
                        .details
                        .users
                        .iter_mut()
                        .find(|user| user.id.id() == uid.as_bytes())
                    {
                        su.signatures.push(sig);
                    } else {
                        // This should not happen, since we found the id above
                        return Err(sop::errors::Error::CertUseridNoMatch);
                    };
                }
            }

            // store updated certificate for output
            res.push(updated.into());
        }

        if res.is_empty() {
            eprintln!("No certificates to output");
            return Err(sop::errors::Error::UnspecifiedFailure);
        }

        Ok(Certs {
            certs: res,
            source_name: None,
        })
    }
}