Optionally allow interactive PIN entry.

This commit is contained in:
Heiko Schaefer 2022-03-29 23:25:01 +02:00
parent f069fb1e20
commit 1f7d17bc70
No known key found for this signature in database
GPG key ID: 4A849A1904CCBD7D
3 changed files with 123 additions and 51 deletions

View file

@ -1,5 +1,5 @@
<!-- <!--
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name> SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
SPDX-License-Identifier: MIT OR Apache-2.0 SPDX-License-Identifier: MIT OR Apache-2.0
--> -->
@ -36,9 +36,12 @@ 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
non-interactive (this tool is designed to be easily usable from usable in a non-interactive way (this tool is designed 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,
if available.
### List and inspect cards ### List and inspect cards
List idents of all currently connected cards: List idents of all currently connected cards:
@ -163,21 +166,42 @@ In the example output above, this string is the ssh public key:
`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2dcYBqMCamidT5MpE3Cl3MIKcYMBekGXbK2aaN6JaH opgpcard:ABCD:01234567` `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2dcYBqMCamidT5MpE3Cl3MIKcYMBekGXbK2aaN6JaH opgpcard:ABCD:01234567`
### Set card metadata ### Admin commands
Set cardholder name: All `admin` commands need the admin PIN. It can be provided as a file, with `-P <admin-pin-file>`,
for non-interactive use.
Alternatively, the PIN can be entered interactively on the host computer, or via a pinpad if the OpenPGP card is
used in a smartcard reader that has a pinpad.
#### Set cardholder name
Set cardholder name, with pin file:
``` ```
$ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> name "Foo Bar" $ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> name "Foo Bar"
``` ```
Set cardholder URL: Set cardholder name, with interactive PIN input
(either on the host computer, or via a smartcard reader pinpad):
```
$ opgpcard admin -c ABCD:01234567 name "Foo Bar"
```
#### Set cardholder URL
``` ```
$ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> url "https://key.url.example" $ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> url "https://key.url.example"
``` ```
### Import keys or interactively
```
$ opgpcard admin -c ABCD:01234567 url "https://key.url.example"
```
#### Import keys
Import private key onto a card. This works if at most one (sub)key per role Import private key onto a card. This works if at most one (sub)key per role
(sign, decrypt, auth) exists in `key.priv`: (sign, decrypt, auth) exists in `key.priv`:
@ -186,6 +210,12 @@ Import private key onto a card. This works if at most one (sub)key per role
$ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> import key.priv $ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> import key.priv
``` ```
or interactively
```
$ opgpcard admin -c ABCD:01234567 import key.priv
```
Import private key onto a card while explicitly selecting subkeys. Explicitly Import private key onto a card while explicitly selecting subkeys. Explicitly
specified fingerprints are necessary if more than one subkey exists specified fingerprints are necessary if more than one subkey exists
in `key.priv` for any role (note: spaces in fingerprints are ignored). in `key.priv` for any role (note: spaces in fingerprints are ignored).
@ -200,10 +230,26 @@ $ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> import key.priv \
When fingerprints are only specified for a subset of the roles, no keys will When fingerprints are only specified for a subset of the roles, no keys will
be imported for the other roles. be imported for the other roles.
### Generate Keys on the card #### Generate Keys on the card
Key generation needs both the admin PIN and the user PIN (the user PIN is needed to export the new key as a public key).
The user PIN can be provided with the `-p <user-pin-file>`, or interactively on the host computer or via the smartcard
reader pinpad.
``` ```
$ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> generate -p <user-pin-file> -o <output-cert-file> 25519 $ opgpcard admin -c ABCD:01234567 -P <admin-pin-file> generate -p <user-pin-file> -o <output-cert-file> 25519
```
or interactively
```
$ opgpcard admin -c ABCD:01234567 generate -o <output-cert-file> 25519
```
Output will look like:
```
Generate subkey for Signing Generate subkey for Signing
Generate subkey for Decryption Generate subkey for Decryption
Generate subkey for Authentication Generate subkey for Authentication
@ -241,6 +287,12 @@ For now, this tool only supports creating detached signatures, like this
$ opgpcard sign --detached -c ABCD:01234567 -p <user-pin-file> -s <cert-file> <input-file> $ opgpcard sign --detached -c ABCD:01234567 -p <user-pin-file> -s <cert-file> <input-file>
``` ```
or interactively
```
$ opgpcard sign --detached -c ABCD:01234567 -s <cert-file> <input-file>
```
### Decrypting ### Decrypting
Decryption using a card (if no input file is set, stdin is read): Decryption using a card (if no input file is set, stdin is read):
@ -249,6 +301,12 @@ Decryption using a card (if no input file is set, stdin is read):
$ opgpcard decrypt -c ABCD:01234567 -p <user-pin-file> -r <cert-file> <input-file> $ opgpcard decrypt -c ABCD:01234567 -p <user-pin-file> -r <cert-file> <input-file>
``` ```
or interactively
```
$ opgpcard decrypt -c ABCD:01234567 -r <cert-file> <input-file>
```
### Factory reset ### Factory reset
Factory reset: Factory reset:

View file

@ -23,6 +23,9 @@ use std::io::Write;
mod cli; mod cli;
mod util; mod util;
const ENTER_USER_PIN: &str = "Enter user PIN:";
const ENTER_ADMIN_PIN: &str = "Enter admin PIN:";
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init(); env_logger::init();
@ -77,15 +80,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut pgp = OpenPgp::new(&mut card); let mut pgp = OpenPgp::new(&mut card);
let mut open = Open::new(pgp.transaction()?)?; let mut open = Open::new(pgp.transaction()?)?;
let admin_pin = util::get_pin(&mut open, admin_pin, ENTER_ADMIN_PIN);
match cmd { match cmd {
cli::AdminCommand::Name { name } => { cli::AdminCommand::Name { name } => {
let mut admin = util::verify_to_admin(&mut open, admin_pin)?; let mut admin = util::verify_to_admin(&mut open, admin_pin.as_deref())?;
let _ = admin.set_name(&name)?; let _ = admin.set_name(&name)?;
} }
cli::AdminCommand::Url { url } => { cli::AdminCommand::Url { url } => {
let mut admin = util::verify_to_admin(&mut open, admin_pin)?; let mut admin = util::verify_to_admin(&mut open, admin_pin.as_deref())?;
let _ = admin.set_url(&url)?; let _ = admin.set_url(&url)?;
} }
@ -96,7 +100,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
auth_fp, auth_fp,
} => { } => {
let key = Cert::from_file(keyfile)?; let key = Cert::from_file(keyfile)?;
let admin = util::verify_to_admin(&mut open, admin_pin)?; let admin = util::verify_to_admin(&mut open, admin_pin.as_deref())?;
if (&sig_fp, &dec_fp, &auth_fp) == (&None, &None, &None) { if (&sig_fp, &dec_fp, &auth_fp) == (&None, &None, &None) {
// If no fingerprint has been provided, we check if // If no fingerprint has been provided, we check if
@ -114,10 +118,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
no_auth, no_auth,
algo, algo,
} => { } => {
let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN);
generate_keys( generate_keys(
open, open,
admin_pin, admin_pin.as_deref(),
user_pin, user_pin.as_deref(),
output, output,
!no_decrypt, !no_decrypt,
!no_auth, !no_auth,
@ -379,7 +385,9 @@ fn decrypt(
let mut open = Open::new(pgp.transaction()?)?; let mut open = Open::new(pgp.transaction()?)?;
let mut user = util::verify_to_user(&mut open, pin_file)?; let user_pin = util::get_pin(&mut open, pin_file, ENTER_USER_PIN);
let mut user = util::verify_to_user(&mut open, user_pin.as_deref())?;
let d = user.decryptor(&cert)?; let d = user.decryptor(&cert)?;
let db = DecryptorBuilder::from_reader(input)?; let db = DecryptorBuilder::from_reader(input)?;
@ -405,7 +413,9 @@ fn sign_detached(
let mut open = Open::new(pgp.transaction()?)?; let mut open = Open::new(pgp.transaction()?)?;
let mut sign = util::verify_to_sign(&mut open, pin_file)?; let user_pin = util::get_pin(&mut open, pin_file, ENTER_USER_PIN);
let mut sign = util::verify_to_sign(&mut open, user_pin.as_deref())?;
let s = sign.signer(&cert)?; let s = sign.signer(&cert)?;
let message = Armorer::new(Message::new(std::io::stdout())).build()?; let message = Armorer::new(Message::new(std::io::stdout())).build()?;
@ -492,8 +502,8 @@ fn key_import_explicit(
fn generate_keys( fn generate_keys(
mut open: Open, mut open: Open,
pw3_path: Option<PathBuf>, admin_pin: Option<&[u8]>,
pw1_path: Option<PathBuf>, user_pin: Option<&[u8]>,
output: Option<PathBuf>, output: Option<PathBuf>,
decrypt: bool, decrypt: bool,
auth: bool, auth: bool,
@ -529,7 +539,7 @@ fn generate_keys(
// 2) Then, generate keys on the card. // 2) Then, generate keys on the card.
// We need "admin" access to the card for this). // We need "admin" access to the card for this).
let (key_sig, key_dec, key_aut) = { let (key_sig, key_dec, key_aut) = {
if let Ok(mut admin) = util::verify_to_admin(&mut open, pw3_path) { if let Ok(mut admin) = util::verify_to_admin(&mut open, admin_pin) {
gen_subkeys(&mut admin, decrypt, auth, a)? gen_subkeys(&mut admin, decrypt, auth, a)?
} else { } else {
return Err(anyhow!("Failed to open card in admin mode.")); return Err(anyhow!("Failed to open card in admin mode."));
@ -539,29 +549,16 @@ fn generate_keys(
// 3) Generate a Cert from the generated keys. For this, we // 3) Generate a Cert from the generated keys. For this, we
// need "signing" access to the card (to make binding signatures within // need "signing" access to the card (to make binding signatures within
// the Cert). // the Cert).
let pin = if let Some(pw1) = pw1_path { if user_pin.is_none() && open.feature_pinpad_verify() {
Some(util::load_pin(&pw1)?) println!(
} else { "The public cert will now be generated.\n\n\
if open.feature_pinpad_verify() { You will need to enter your user PIN multiple times during this process.\n\n"
println!(); );
println!( }
"Next: generating your public cert. You will need to enter \
your user PIN multiple times to make binding signatures."
);
} else {
return Err(anyhow!("No user PIN file provided, and no pinpad found"));
}
None
};
let cert = make_cert( let cert = make_cert(&mut open, key_sig, key_dec, key_aut, user_pin, &|| {
&mut open, println!("Enter user PIN on card reader pinpad.")
key_sig, })?;
key_dec,
key_aut,
pin.as_deref(),
&|| println!("Enter user PIN on card reader pinpad."),
)?;
let armored = String::from_utf8(cert.armored().to_vec()?)?; let armored = String::from_utf8(cert.armored().to_vec()?)?;
// Write armored certificate to the output file (or stdout) // Write armored certificate to the output file (or stdout)

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name> // SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
@ -18,12 +18,31 @@ pub(crate) fn open_card(ident: &str) -> Result<impl CardBackend, Error> {
PcscBackend::open_by_ident(ident, None) PcscBackend::open_by_ident(ident, None)
} }
/// Get pin from file. Or via user input, if no file and no pinpad is available.
///
/// If a pinpad is available, return Null (the pinpad will be used to get access to the card).
///
/// `msg` is the message to show when asking the user to enter a PIN.
pub(crate) fn get_pin(open: &mut Open, pin_file: Option<PathBuf>, msg: &str) -> Option<Vec<u8>> {
if let Some(path) = pin_file {
// we have a pin file
Some(load_pin(&path).ok()?)
} else if !open.feature_pinpad_verify() {
// we have no pin file and no pinpad
let pin = rpassword::read_password_from_tty(Some(msg)).ok()?;
Some(pin.into_bytes())
} else {
// we have a pinpad
None
}
}
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_file: Option<PathBuf>, pin: Option<&[u8]>,
) -> Result<User<'app, 'open>, Box<dyn std::error::Error>> { ) -> Result<User<'app, 'open>, Box<dyn std::error::Error>> {
if let Some(path) = pin_file { if let Some(pin) = pin {
open.verify_user(&load_pin(&path)?)?; open.verify_user(pin)?;
} else { } else {
if !open.feature_pinpad_verify() { if !open.feature_pinpad_verify() {
return Err(anyhow!("No user PIN file provided, and no pinpad found").into()); return Err(anyhow!("No user PIN file provided, and no pinpad found").into());
@ -38,10 +57,10 @@ pub(crate) fn verify_to_user<'app, 'open>(
pub(crate) fn verify_to_sign<'app, 'open>( pub(crate) fn verify_to_sign<'app, 'open>(
open: &'open mut Open<'app>, open: &'open mut Open<'app>,
pin_file: Option<PathBuf>, pin: Option<&[u8]>,
) -> Result<Sign<'app, 'open>, Box<dyn std::error::Error>> { ) -> Result<Sign<'app, 'open>, Box<dyn std::error::Error>> {
if let Some(path) = pin_file { if let Some(pin) = pin {
open.verify_user_for_signing(&load_pin(&path)?)?; open.verify_user_for_signing(pin)?;
} else { } else {
if !open.feature_pinpad_verify() { if !open.feature_pinpad_verify() {
return Err(anyhow!("No user PIN file provided, and no pinpad found").into()); return Err(anyhow!("No user PIN file provided, and no pinpad found").into());
@ -52,14 +71,12 @@ pub(crate) fn verify_to_sign<'app, 'open>(
.ok_or_else(|| anyhow!("Couldn't get sign access").into()) .ok_or_else(|| anyhow!("Couldn't get sign access").into())
} }
// pub fn admin_card<'b>(&'b mut self) -> Option<Admin<'a, 'b>> {
pub(crate) fn verify_to_admin<'app, 'open>( pub(crate) fn verify_to_admin<'app, 'open>(
open: &'open mut Open<'app>, open: &'open mut Open<'app>,
pin_file: Option<PathBuf>, pin: Option<&[u8]>,
) -> Result<Admin<'app, 'open>, Box<dyn std::error::Error>> { ) -> Result<Admin<'app, 'open>, Box<dyn std::error::Error>> {
if let Some(path) = pin_file { if let Some(pin) = pin {
open.verify_admin(&load_pin(&path)?)?; open.verify_admin(pin)?;
} else { } else {
if !open.feature_pinpad_verify() { if !open.feature_pinpad_verify() {
return Err(anyhow!("No admin PIN file provided, and no pinpad found").into()); return Err(anyhow!("No admin PIN file provided, and no pinpad found").into());