From 99e0c6caafe0673c1630d03c5bbb14000482363e Mon Sep 17 00:00:00 2001 From: Heiko Schaefer Date: Thu, 21 Apr 2022 13:27:01 +0200 Subject: [PATCH] Implement, document PIN management in opgpcard --- tools/README.md | 190 +++++++++++++++++++----------- tools/src/bin/opgpcard/cli.rs | 55 +++++++++ tools/src/bin/opgpcard/main.rs | 205 +++++++++++++++++++++++++++++++++ tools/src/bin/opgpcard/util.rs | 38 +++++- 4 files changed, 417 insertions(+), 71 deletions(-) diff --git a/tools/README.md b/tools/README.md index 18ca596..bf36444 100644 --- a/tools/README.md +++ b/tools/README.md @@ -5,8 +5,8 @@ SPDX-License-Identifier: MIT OR Apache-2.0 # OpenPGP card tools -This crate contains two tools for inspecting, configuring and using OpenPGP -cards: `opgpcard` and `opgpcard-pin`. +This crate contains the `opgpcard` tool for inspecting, configuring and using OpenPGP +cards. # Install @@ -36,7 +36,7 @@ binaries. ## opgpcard A tool to inspect, configure and use OpenPGP cards. All calls of this tool are -usable in a non-interactive way (this tool is designed to be easily usable from +usable in a non-interactive way (this tool is designed both for interactive use, and to be easily usable from shell-scripts). Alternatively, PINs can be entered interactively on the host computer, or via a pinpad on the smartcard reader, @@ -413,6 +413,120 @@ or interactively $ opgpcard decrypt -c ABCD:01234567 -r ``` +### PIN management + +OpenPGP cards use PINs (numerical passwords) to verify that a user is allowed to perform an operation. + +To use the cryptographic operations on a card (such as decryption or signing), the *User PIN* is required. + +To configure a card (for example to import OpenPGP key material into the card's key slots), the *Admin PIN* is needed. + +By default, on unconfigured (or factory reset) cards, the User PIN is typically set to `123456`, +and the Admin PIN is set to `12345678`. + +#### Blocked cards and resetting + +When a user has entered a wrong User PIN too often, the card goes into a blocked state, in which presenting the +User PIN successfully is not possible anymore. The purpose of this is to prevent attackers from trying all possible +PINs (e.g. after stealing a card). + +To be able to use the card again, the user PIN must be "reset". + +A user PIN reset can be performed by presenting the Admin PIN. + +#### The resetting code + +OpenPGP cards offer an additional, optional, *Resetting Code* mechanism. + +The resetting code may be configured on a card and used to reset the User PIN if it has been forgotten or blocked. +When unblocking a card with the Resetting Code, the Admin PIN is not needed. + +The Resetting Code mechanism is only useful in scenarios where a user doesn't have access to (or prefers not to use) +the Admin PIN (e.g. in some corporate settings, users might not be given the Admin PIN for +their cards. Instead, an admin may define a resetting code and give that code to the user). + +On unconfigured (or factory reset) cards, the Resetting Code is typically unset. + + +#### Set a new user PIN + +Setting a new user PIN requires the admin PIN: + +``` +$ opgpcard pin -c ABCD:01234567 set-user +``` + +For non-interactive PIN change: + +``` +$ opgpcard pin -c ABCD:01234567 set-user -p -q +``` + +#### Set new admin PIN + +This requires the (previous) admin PIN. + +``` +$ opgpcard pin -c ABCD:01234567 set-admin +``` + +For non-interactive PIN change: + +``` +$ opgpcard pin -c ABCD:01234567 set-admin -p -q +``` + +#### Reset user PIN with admin PIN + +The user PIN can be reset to a different (or the same) PIN by providing the admin PIN. +This is possible at any time, including when a wrong user PIN has been entered too often, and the card refuses to accept the user PIN any more. + +``` +$ opgpcard pin -c ABCD:01234567 reset-user +``` + +For non-interactive PIN change: + +``` +$ opgpcard pin -c ABCD:01234567 reset-user -P -p +``` + +#### Configuring the resetting code + +The resetting code is an alternative mechanism to recover from a lost or locked user PIN. + +You can set the resetting code after verifying the admin PIN. Once a resetting code is configured on your card, +you can use that code to reset the user PIN without needing the admin PIN. + +``` +$ opgpcard pin -c 0006:16019180 set-reset +``` + +To non-interactively set the resetting code: + +``` +$ opgpcard pin -c 0006:16019180 set-reset -P -r +``` + +#### Reset user PIN with the resetting code + +If a resetting code is configured on a card, you can use that code to reset the user PIN: + +``` +$ opgpcard pin -c 0006:16019180 reset-user-rc +Enter resetting code: +Enter new user PIN: +Repeat the new user PIN: + +User PIN has been set. +``` + +To non-interactively use the resetting code: + +``` +$ opgpcard pin -c 0006:16019180 reset-user-rc -r -p +``` + ### Factory reset Factory reset: @@ -427,7 +541,7 @@ NOTE: you do not need a PIN to reset a card! When using a shell like [bash](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Strings) -, you can pass user and/or admin PINs via file-descriptors: +, you can pass user and/or admin PINs via file-descriptors (instead of from a file on disk): ``` $ opgpcard sign --detached -c ABCD:01234567 -p /dev/fd/3 -s 3<<<123456 @@ -440,70 +554,6 @@ $ opgpcard admin -c ABCD:01234567 -P /dev/fd/3 generate -p /dev/fd/4 -o , }, } + +#[derive(Parser, Debug)] +pub enum PinCommand { + /// Set User PIN + SetUser { + #[clap(name = "User PIN file old", short = 'p', long = "user-pin-old")] + user_pin_old: Option, + + #[clap(name = "User PIN file new", short = 'q', long = "user-pin-new")] + user_pin_new: Option, + }, + + /// Set Admin PIN + SetAdmin { + #[clap(name = "Admin PIN file old", short = 'P', long = "admin-pin-old")] + admin_pin_old: Option, + + #[clap(name = "Admin PIN file new", short = 'Q', long = "admin-pin-new")] + admin_pin_new: Option, + }, + + /// Reset User PIN with admin PIN + ResetUser { + #[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")] + admin_pin: Option, + + #[clap(name = "User PIN file new", short = 'p', long = "user-pin-new")] + user_pin_new: Option, + }, + + /// Set Resetting Code + SetReset { + #[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")] + admin_pin: Option, + + #[clap(name = "Resetting code file", short = 'r', long = "reset-code")] + reset_code: Option, + }, + + /// Reset User PIN with 'resetting code' + ResetUserRc { + #[clap(name = "Resetting code file", short = 'r', long = "reset-code")] + reset_code: Option, + + #[clap(name = "User PIN file new", short = 'p', long = "user-pin-new")] + user_pin_new: Option, + }, +} diff --git a/tools/src/bin/opgpcard/main.rs b/tools/src/bin/opgpcard/main.rs index bf8af49..28172f6 100644 --- a/tools/src/bin/opgpcard/main.rs +++ b/tools/src/bin/opgpcard/main.rs @@ -20,6 +20,7 @@ use openpgp_card_sequoia::util::{ }; use openpgp_card_sequoia::{sq_util, PublicKey}; +use crate::util::{load_pin, print_gnuk_note}; use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm}; use std::io::Write; @@ -141,6 +142,210 @@ fn main() -> Result<(), Box> { } } } + cli::Command::Pin { ident, cmd } => { + let mut card = util::open_card(&ident)?; + let mut pgp = OpenPgp::new(&mut card); + let pgpt = pgp.transaction()?; + + let pinpad_modify = pgpt.feature_pinpad_modify(); + + let mut open = Open::new(pgpt)?; + + match cmd { + cli::PinCommand::SetUser { + user_pin_old, + user_pin_new, + } => { + let res = if !pinpad_modify { + // get current user pin + let user_pin1 = util::get_pin(&mut open, user_pin_old, ENTER_USER_PIN) + .expect("this should never be None"); + + // verify pin + open.verify_user(&user_pin1)?; + println!("PIN was accepted by the card.\n"); + + let pin_new = match user_pin_new { + None => { + // ask user for new user pin + util::input_pin_twice( + "Enter new user PIN: ", + "Repeat the new user PIN: ", + )? + } + Some(path) => load_pin(&path)?, + }; + + // set new user pin + open.change_user_pin(&user_pin1, &pin_new) + } else { + // set new user pin via pinpad + open.change_user_pin_pinpad(&|| { + println!( + "Enter old user PIN on card reader pinpad, then new user PIN (twice)." + ) + }) + }; + + if res.is_err() { + println!("\nFailed to change the user PIN!"); + println!("{:?}", res); + + if let Err(err) = res { + print_gnuk_note(err, &open)?; + } + } else { + println!("\nUser PIN has been set."); + } + } + cli::PinCommand::SetAdmin { + admin_pin_old, + admin_pin_new, + } => { + if !pinpad_modify { + // get current admin pin + let admin_pin1 = util::get_pin(&mut open, admin_pin_old, ENTER_ADMIN_PIN) + .expect("this should never be None"); + + // verify pin + open.verify_admin(&admin_pin1)?; + println!("PIN was accepted by the card.\n"); + + let pin_new = match admin_pin_new { + None => { + // ask user for new admin pin + util::input_pin_twice( + "Enter new admin PIN: ", + "Repeat the new admin PIN: ", + )? + } + Some(path) => load_pin(&path)?, + }; + + // set new admin pin + open.change_admin_pin(&admin_pin1, &pin_new)?; + } else { + // set new admin pin via pinpad + open.change_admin_pin_pinpad(&|| { + println!( + "Enter old admin PIN on card reader pinpad, then new admin PIN (twice)." + ) + })?; + }; + + println!("\nAdmin PIN has been set."); + } + + cli::PinCommand::ResetUser { + admin_pin, + user_pin_new, + } => { + // verify admin pin + match util::get_pin(&mut open, admin_pin, ENTER_ADMIN_PIN) { + Some(admin_pin) => { + // verify pin + open.verify_admin(&admin_pin)?; + } + None => { + open.verify_admin_pinpad(&|| println!("Enter admin PIN on pinpad."))?; + } + } + println!("PIN was accepted by the card.\n"); + + // ask user for new user pin + let pin = match user_pin_new { + None => util::input_pin_twice( + "Enter new user PIN: ", + "Repeat the new user PIN: ", + )?, + Some(path) => load_pin(&path)?, + }; + + let res = if let Some(mut admin) = open.admin_card() { + admin.reset_user_pin(&pin) + } else { + return Err(anyhow::anyhow!("Failed to use card in admin-mode.").into()); + }; + + if res.is_err() { + println!("\nFailed to change the user PIN!"); + if let Err(err) = res { + print_gnuk_note(err, &open)?; + } + } else { + println!("\nUser PIN has been set."); + } + } + + cli::PinCommand::SetReset { + admin_pin, + reset_code, + } => { + // verify admin pin + match util::get_pin(&mut open, admin_pin, ENTER_ADMIN_PIN) { + Some(admin_pin) => { + // verify pin + open.verify_admin(&admin_pin)?; + } + None => { + open.verify_admin_pinpad(&|| println!("Enter admin PIN on pinpad."))?; + } + } + println!("PIN was accepted by the card.\n"); + + // ask user for new resetting code + let code = match reset_code { + None => util::input_pin_twice( + "Enter new resetting code: ", + "Repeat the new resetting code: ", + )?, + Some(path) => load_pin(&path)?, + }; + + if let Some(mut admin) = open.admin_card() { + admin.set_resetting_code(&code)?; + println!("\nResetting code has been set."); + } else { + return Err(anyhow::anyhow!("Failed to use card in admin-mode.").into()); + }; + } + + cli::PinCommand::ResetUserRc { + reset_code, + user_pin_new, + } => { + // reset by presenting resetting code + + let rst = if let Some(path) = reset_code { + // load resetting code from file + load_pin(&path)? + } else { + // input resetting code + rpassword::read_password_from_tty(Some("Enter resetting code: "))? + .as_bytes() + .to_vec() + }; + + // ask user for new user pin + let pin = match user_pin_new { + None => util::input_pin_twice( + "Enter new user PIN: ", + "Repeat the new user PIN: ", + )?, + Some(path) => load_pin(&path)?, + }; + + // reset to new user pin + match open.reset_user_pin(&rst, &pin) { + Err(err) => { + println!("\nFailed to change the user PIN!"); + print_gnuk_note(err, &open)?; + } + Ok(_) => println!("\nUser PIN has been set."), + } + } + } + } } Ok(()) diff --git a/tools/src/bin/opgpcard/util.rs b/tools/src/bin/opgpcard/util.rs index f5b71ae..9fdea33 100644 --- a/tools/src/bin/opgpcard/util.rs +++ b/tools/src/bin/opgpcard/util.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use openpgp_card::algorithm::{Algo, Curve}; use openpgp_card::crypto_data::{EccType, PublicKeyMaterial}; -use openpgp_card::{CardBackend, Error}; +use openpgp_card::{CardBackend, Error, StatusBytes}; use openpgp_card_pcsc::PcscBackend; use openpgp_card_sequoia::card::{Admin, Open, Sign, User}; @@ -37,6 +37,19 @@ pub(crate) fn get_pin(open: &mut Open, pin_file: Option, msg: &str) -> } } +/// Let the user input a PIN twice, return PIN if both entries match, error otherwise +pub(crate) fn input_pin_twice(msg1: &str, msg2: &str) -> Result> { + // get new user pin + let newpin1 = rpassword::read_password_from_tty(Some(msg1))?; + let newpin2 = rpassword::read_password_from_tty(Some(msg2))?; + + if newpin1 != newpin2 { + Err(anyhow::anyhow!("PINs do not match.")) + } else { + Ok(newpin1.as_bytes().to_vec()) + } +} + pub(crate) fn verify_to_user<'app, 'open>( open: &'open mut Open<'app>, pin: Option<&[u8]>, @@ -195,3 +208,26 @@ pub(crate) fn get_ssh_pubkey_string(pkm: &PublicKeyMaterial, ident: String) -> R Ok(s.trim().into()) } + +/// Gnuk doesn't allow the User password (pw1) to be changed while no +/// private key material exists on the card. +/// +/// This fn checks for Gnuk's Status code and the case that no keys exist +/// on the card, and prints a note to the user, pointing out that the +/// absence of keys on the card might be the reason for the error they get. +pub(crate) fn print_gnuk_note(err: Error, card: &Open) -> Result<()> { + if matches!( + err, + Error::CardStatus(StatusBytes::ConditionOfUseNotSatisfied) + ) { + // check if no keys exist on the card + let fps = card.fingerprints()?; + if fps.signature() == None && fps.decryption() == None && fps.authentication() == None { + println!( + "\nNOTE: Some cards (e.g. Gnuk) don't allow \ + User PIN change while no keys exist on the card." + ); + } + } + Ok(()) +}