rsop-oct 0.1.2

SOP CLI tool for OpenPGP card devices based on rPGP
Documentation
use openpgp_card::ocard::Transaction;
use openpgp_card::state::Open;
use openpgp_card_rpgp::CardSlot;
use pgp::types::{PublicKeyTrait, PublicParams};
use sop::plumbing::PasswordsAreHumanReadable;

/// Get a card based on matching available cards with public key parameters.
///
/// FIXME: replace with a filter function, so main can directly iterate over cards.
/// Caller shouldn't need to re-open a transaction (?)
pub fn card_by_pp(
    pp: &PublicParams,
    kt: openpgp_card::ocard::KeyType,
) -> Result<Option<openpgp_card::Card<Open>>, crate::Error> {
    let Ok(backends) = card_backend_pcsc::PcscBackend::cards(None) else {
        return Ok(None);
    };

    for b in backends.filter_map(|c| c.ok()) {
        if let Ok(mut card) = openpgp_card::Card::<Open>::new(b) {
            // signals that this is the right card
            let mut found = false;

            {
                if let Ok(mut tx) = card.transaction() {
                    let cs = CardSlot::init_from_card(&mut tx, kt, &|| {})?;

                    if cs.public_key().public_params() == pp {
                        found = true;
                    }
                }
            }

            if found {
                return Ok(Some(card));
            }
        }
    }

    Ok(None)
}

/// Run an item of work on an OpenPGP card.
///
/// This finds the card, unlocks it with its User PIN, and performs `work`.
///
/// TODO: This approach is not suitable for doing a thing on more than one card, for now.
///  That's not a good match with the rPGP message builder, or with one-shot CSF message generation.
pub(crate) fn do_on_card<W, T>(
    pp: &PublicParams,
    slot: openpgp_card::ocard::KeyType,
    sop_pw: &[sop::Password],
    touch_prompt: &(dyn Fn() + Send + Sync),
    work: W,
) -> Result<T, crate::Error>
where
    W: FnOnce(&mut CardSlot) -> Result<T, crate::Error>,
{
    let Some(mut card) = card_by_pp(pp, slot).expect("FIXME") else {
        eprintln!("No card found.");
        return Err(crate::Error::Message("No card found.".to_string()));
    };

    let mut tx = card.transaction().expect("FIXME");

    // TODO: How to prioritize between SOP-provided password and openpgp-card-state?
    // (Currently we prefer the PIN from openpgp-card-state, because we're probably less likely
    // to lock up a card with that mechanism.)

    // Get and use the User PIN from openpgp-card-state, if possible
    if verify_pin_from_card_state(tx.card(), slot == openpgp_card::ocard::KeyType::Signing).is_ok()
    {
        // We're happy with the pin situation, based on a PIN from openpgp-card-state
    } else if !sop_pw.is_empty() {
        // The PIN from openpgp-card-state didn't work out
        // -> try using the SOP-provided "key password" as User PIN

        // We are not willing to reason about more than one SOP-provided PIN
        assert_eq!(sop_pw.len(), 1, "Expecting exactly one User PIN");

        // Try verifying the PIN from SOP
        let res = verify_pin(
            tx.card(),
            sop_pw[0].normalized(),
            slot == openpgp_card::ocard::KeyType::Signing,
        );

        // TODO: If the PIN is wrong, the user needs to be warned before they lock their card!
        if let Err(e) = res {
            eprintln!("Invalid User Pin!");
            return Err(e.into());
        }
    } else {
        return Err(crate::Error::Message(
            "No PIN found for this card.".to_string(),
        ));
    }

    let mut cs = CardSlot::init_from_card(&mut tx, slot, touch_prompt)?;
    work(&mut cs)
}

/// Verify User PIN against explicit `pin`
pub fn verify_pin(tx: &mut Transaction, pin: &[u8], sign: bool) -> Result<(), rpgpie::Error> {
    let verify = match sign {
        true => Transaction::verify_pw1_sign,
        false => Transaction::verify_pw1_user,
    };

    verify(tx, pin.to_vec().into())
        .map_err(|_| rpgpie::Error::Message("User PIN verification failed.".to_string()))
}

/// Get the User PIN for the card via openpgp_card_state, and present it to the card.
///
/// If the card rejects the User PIN, it is dropped from openpgp_card_state.
#[cfg(feature = "openpgp-card-state")]
pub fn verify_pin_from_card_state(tx: &mut Transaction, sign: bool) -> Result<(), rpgpie::Error> {
    let ard = tx.application_related_data().expect("FIXME");
    let ident = ard.application_id().expect("FIXME").ident();

    if let Ok(Some(pin)) = openpgp_card_state::get_pin(&ident) {
        if verify_pin(tx, pin.as_bytes(), sign).is_err() {
            // We drop the PIN from the state backend, to avoid exhausting
            // the retry counter and locking up the User PIN.
            let res = openpgp_card_state::drop_pin(&ident);

            if res.is_ok() {
                eprintln!(
                    "ERROR: The stored User PIN for OpenPGP card '{}' seems wrong or blocked! Dropped it from storage.",
                    &ident);
            } else {
                eprintln!(
                    "ERROR: The stored User PIN for OpenPGP card '{}' seems wrong or blocked! In addition, dropping it from storage failed.",
                    &ident);
            }

            return Err(rpgpie::Error::Message(
                "User PIN verification failed.".to_string(),
            ));
        }
    } else {
        return Err(rpgpie::Error::Message(
            "No User PIN configured.".to_string(),
        ));
    }

    Ok(())
}

/// When the feature "openpgp-card-state" is disabled, this function returns an error, forcing
/// PIN handling to fall back on SOP key-passwords.
#[cfg(not(feature = "openpgp-card-state"))]
pub fn verify_pin_from_card_state(_tx: &mut Transaction, _sign: bool) -> Result<(), rpgpie::Error> {
    Err(rpgpie::Error::Message(String::default()))
}