Implement, document PIN management in opgpcard

This commit is contained in:
Heiko Schaefer 2022-04-21 13:27:01 +02:00
parent fbdb9e87b2
commit 99e0c6caaf
No known key found for this signature in database
GPG key ID: 4A849A1904CCBD7D
4 changed files with 417 additions and 71 deletions

View file

@ -5,8 +5,8 @@ SPDX-License-Identifier: MIT OR Apache-2.0
# OpenPGP card tools # OpenPGP card tools
This crate contains two tools for inspecting, configuring and using OpenPGP This crate contains the `opgpcard` tool for inspecting, configuring and using OpenPGP
cards: `opgpcard` and `opgpcard-pin`. cards.
# Install # Install
@ -36,7 +36,7 @@ binaries.
## opgpcard ## opgpcard
A tool to inspect, configure and use OpenPGP cards. All calls of this tool are 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). shell-scripts).
Alternatively, PINs can be entered interactively on the host computer, or via a pinpad on the smartcard reader, 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 <cert-file> <input-file> $ opgpcard decrypt -c ABCD:01234567 -r <cert-file> <input-file>
``` ```
### 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 <old-user-pin-file> -q <new-user-pin-file>
```
#### 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 <old-admin-pin-file> -q <new-admin-pin-file>
```
#### 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 <admin-pin-file> -p <new-user-pin-file>
```
#### 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 <admin-pin-file> -r <resetting-code-file>
```
#### 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 <resetting-code-file> -p <new-user-pin-file>
```
### Factory reset ### Factory reset
Factory reset: Factory reset:
@ -427,7 +541,7 @@ NOTE: you do not need a PIN to reset a card!
When using a shell like When using a shell like
[bash](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Strings) [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 <cert-file> 3<<<123456 $ opgpcard sign --detached -c ABCD:01234567 -p /dev/fd/3 -s <cert-file> 3<<<123456
@ -440,70 +554,6 @@ $ opgpcard admin -c ABCD:01234567 -P /dev/fd/3 generate -p /dev/fd/4 -o <output-
### Directly entering PINs on card readers with pinpad ### Directly entering PINs on card readers with pinpad
If your OpenPGP card is inserted in a card reader with a pinpad, this tool If your OpenPGP card is inserted in a card reader with a pinpad, this tool
offers you the option to use the pinpad to enter the user- or admin-PINs. offers you the option to use the pinpad to enter the User- or Admin PINs.
To do this, you can omit the `-p` and/or '`-P`' parameters - then you will To do this, you can omit the `-p` and/or `-P` parameters. Then you will
be prompted to enter the user or admin PINs where needed. be prompted to enter the user or admin PINs where needed.
## opgpcard-pin
An interactive tool to set the admin and user PINs, and to reset the user PIN
on OpenPGP cards.
### Set a new user PIN
Setting a new user PIN requires the admin PIN:
```
opgpcard-pin -c ABCD:01234567 set-user-pin
```
(The default admin PIN on unconfigured cards is typically `12345678`)
### Set new admin PIN
This requires the (previous) admin PIN.
```
opgpcard-pin -c ABCD:01234567 set-admin-pin
```
(The default admin PIN on unconfigured cards is typically `12345678`)
### Recover from blocked user PIN (using the admin PIN)
When a user has entered a wrong user PIN too often, the card goes into a blocked state, in which presenting the
user PIN 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".
Reset user PIN after it has been blocked (requires admin PIN):
```
opgpcard-pin -c ABCD:01234567 reset-user-pin -a
```
### Recover from blocked user PIN (using the resetting code)
The resetting code is an optional/alternative method to recover from a blocked user PIN.
Context: in some (e.g. 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.
Set resetting code (requires admin PIN):
```
opgpcard-pin -c ABCD:01234567 set-reset-code
```
Once a reset code has been defined, the user can
reset the blocked user PIN, using the resetting code:
```
opgpcard-pin -c ABCD:01234567 reset-user-pin
```
### Directly entering PINs on card readers with pinpad
If your OpenPGP card is inserted in a card reader with a pinpad, this tool
assumes you will want to enter all PINs via that pinpad. It will prompt
you to enter PINs accordingly.

View file

@ -63,6 +63,13 @@ pub enum Command {
#[clap(subcommand)] #[clap(subcommand)]
cmd: AdminCommand, cmd: AdminCommand,
}, },
Pin {
#[clap(name = "card ident", short = 'c', long = "card")]
ident: String,
#[clap(subcommand)]
cmd: PinCommand,
},
Decrypt { Decrypt {
#[clap(name = "card ident", short = 'c', long = "card")] #[clap(name = "card ident", short = 'c', long = "card")]
ident: String, ident: String,
@ -144,3 +151,51 @@ pub enum AdminCommand {
algo: Option<String>, algo: Option<String>,
}, },
} }
#[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<PathBuf>,
#[clap(name = "User PIN file new", short = 'q', long = "user-pin-new")]
user_pin_new: Option<PathBuf>,
},
/// Set Admin PIN
SetAdmin {
#[clap(name = "Admin PIN file old", short = 'P', long = "admin-pin-old")]
admin_pin_old: Option<PathBuf>,
#[clap(name = "Admin PIN file new", short = 'Q', long = "admin-pin-new")]
admin_pin_new: Option<PathBuf>,
},
/// Reset User PIN with admin PIN
ResetUser {
#[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")]
admin_pin: Option<PathBuf>,
#[clap(name = "User PIN file new", short = 'p', long = "user-pin-new")]
user_pin_new: Option<PathBuf>,
},
/// Set Resetting Code
SetReset {
#[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")]
admin_pin: Option<PathBuf>,
#[clap(name = "Resetting code file", short = 'r', long = "reset-code")]
reset_code: Option<PathBuf>,
},
/// Reset User PIN with 'resetting code'
ResetUserRc {
#[clap(name = "Resetting code file", short = 'r', long = "reset-code")]
reset_code: Option<PathBuf>,
#[clap(name = "User PIN file new", short = 'p', long = "user-pin-new")]
user_pin_new: Option<PathBuf>,
},
}

View file

@ -20,6 +20,7 @@ use openpgp_card_sequoia::util::{
}; };
use openpgp_card_sequoia::{sq_util, PublicKey}; use openpgp_card_sequoia::{sq_util, PublicKey};
use crate::util::{load_pin, print_gnuk_note};
use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm}; use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm};
use std::io::Write; use std::io::Write;
@ -141,6 +142,210 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
} }
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(()) Ok(())

View file

@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use openpgp_card::algorithm::{Algo, Curve}; use openpgp_card::algorithm::{Algo, Curve};
use openpgp_card::crypto_data::{EccType, PublicKeyMaterial}; 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_pcsc::PcscBackend;
use openpgp_card_sequoia::card::{Admin, Open, Sign, User}; use openpgp_card_sequoia::card::{Admin, Open, Sign, User};
@ -37,6 +37,19 @@ pub(crate) fn get_pin(open: &mut Open, pin_file: Option<PathBuf>, 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<Vec<u8>> {
// 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>( pub(crate) fn verify_to_user<'app, 'open>(
open: &'open mut Open<'app>, open: &'open mut Open<'app>,
pin: Option<&[u8]>, pin: Option<&[u8]>,
@ -195,3 +208,26 @@ pub(crate) fn get_ssh_pubkey_string(pkm: &PublicKeyMaterial, ident: String) -> R
Ok(s.trim().into()) 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(())
}