diff --git a/tools/README.md b/tools/README.md index 2d2b68d..59e6952 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,5 +1,5 @@ @@ -36,9 +36,12 @@ binaries. ## opgpcard 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). +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 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` -### Set card metadata +### Admin commands -Set cardholder name: +All `admin` commands need the admin PIN. It can be provided as a file, with `-P `, +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 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 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 (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 import key.priv ``` +or interactively + +``` +$ opgpcard admin -c ABCD:01234567 import key.priv +``` + Import private key onto a card while explicitly selecting subkeys. Explicitly specified fingerprints are necessary if more than one subkey exists in `key.priv` for any role (note: spaces in fingerprints are ignored). @@ -200,10 +230,26 @@ $ opgpcard admin -c ABCD:01234567 -P import key.priv \ When fingerprints are only specified for a subset of the roles, no keys will 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 `, or interactively on the host computer or via the smartcard +reader pinpad. ``` $ opgpcard admin -c ABCD:01234567 -P generate -p -o 25519 +``` + +or interactively + +``` +$ opgpcard admin -c ABCD:01234567 generate -o 25519 +``` + +Output will look like: + +``` Generate subkey for Signing Generate subkey for Decryption 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 -s ``` +or interactively + +``` +$ opgpcard sign --detached -c ABCD:01234567 -s +``` + ### Decrypting 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 -r ``` +or interactively + +``` +$ opgpcard decrypt -c ABCD:01234567 -r +``` + ### Factory reset Factory reset: diff --git a/tools/src/bin/opgpcard/main.rs b/tools/src/bin/opgpcard/main.rs index dae801c..04dbae5 100644 --- a/tools/src/bin/opgpcard/main.rs +++ b/tools/src/bin/opgpcard/main.rs @@ -23,6 +23,9 @@ use std::io::Write; mod cli; mod util; +const ENTER_USER_PIN: &str = "Enter user PIN:"; +const ENTER_ADMIN_PIN: &str = "Enter admin PIN:"; + fn main() -> Result<(), Box> { env_logger::init(); @@ -77,15 +80,16 @@ fn main() -> Result<(), Box> { let mut pgp = OpenPgp::new(&mut card); let mut open = Open::new(pgp.transaction()?)?; + let admin_pin = util::get_pin(&mut open, admin_pin, ENTER_ADMIN_PIN); match cmd { 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)?; } 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)?; } @@ -96,7 +100,7 @@ fn main() -> Result<(), Box> { auth_fp, } => { 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 no fingerprint has been provided, we check if @@ -114,10 +118,12 @@ fn main() -> Result<(), Box> { no_auth, algo, } => { + let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN); + generate_keys( open, - admin_pin, - user_pin, + admin_pin.as_deref(), + user_pin.as_deref(), output, !no_decrypt, !no_auth, @@ -379,7 +385,9 @@ fn decrypt( 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 db = DecryptorBuilder::from_reader(input)?; @@ -405,7 +413,9 @@ fn sign_detached( 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 message = Armorer::new(Message::new(std::io::stdout())).build()?; @@ -492,8 +502,8 @@ fn key_import_explicit( fn generate_keys( mut open: Open, - pw3_path: Option, - pw1_path: Option, + admin_pin: Option<&[u8]>, + user_pin: Option<&[u8]>, output: Option, decrypt: bool, auth: bool, @@ -529,7 +539,7 @@ fn generate_keys( // 2) Then, generate keys on the card. // We need "admin" access to the card for this). 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)? } else { 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 // need "signing" access to the card (to make binding signatures within // the Cert). - let pin = if let Some(pw1) = pw1_path { - Some(util::load_pin(&pw1)?) - } else { - if open.feature_pinpad_verify() { - 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 - }; + if user_pin.is_none() && open.feature_pinpad_verify() { + println!( + "The public cert will now be generated.\n\n\ + You will need to enter your user PIN multiple times during this process.\n\n" + ); + } - let cert = make_cert( - &mut open, - key_sig, - key_dec, - key_aut, - pin.as_deref(), - &|| println!("Enter user PIN on card reader pinpad."), - )?; + let cert = make_cert(&mut open, key_sig, key_dec, key_aut, user_pin, &|| { + println!("Enter user PIN on card reader pinpad.") + })?; let armored = String::from_utf8(cert.armored().to_vec()?)?; // Write armored certificate to the output file (or stdout) diff --git a/tools/src/bin/opgpcard/util.rs b/tools/src/bin/opgpcard/util.rs index b2f6c59..f5b71ae 100644 --- a/tools/src/bin/opgpcard/util.rs +++ b/tools/src/bin/opgpcard/util.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 use anyhow::{anyhow, Context, Result}; @@ -18,12 +18,31 @@ pub(crate) fn open_card(ident: &str) -> Result { 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, msg: &str) -> Option> { + 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>( open: &'open mut Open<'app>, - pin_file: Option, + pin: Option<&[u8]>, ) -> Result, Box> { - if let Some(path) = pin_file { - open.verify_user(&load_pin(&path)?)?; + if let Some(pin) = pin { + open.verify_user(pin)?; } else { if !open.feature_pinpad_verify() { 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>( open: &'open mut Open<'app>, - pin_file: Option, + pin: Option<&[u8]>, ) -> Result, Box> { - if let Some(path) = pin_file { - open.verify_user_for_signing(&load_pin(&path)?)?; + if let Some(pin) = pin { + open.verify_user_for_signing(pin)?; } else { if !open.feature_pinpad_verify() { 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()) } -// pub fn admin_card<'b>(&'b mut self) -> Option> { - pub(crate) fn verify_to_admin<'app, 'open>( open: &'open mut Open<'app>, - pin_file: Option, + pin: Option<&[u8]>, ) -> Result, Box> { - if let Some(path) = pin_file { - open.verify_admin(&load_pin(&path)?)?; + if let Some(pin) = pin { + open.verify_admin(pin)?; } else { if !open.feature_pinpad_verify() { return Err(anyhow!("No admin PIN file provided, and no pinpad found").into());