Add openpgp-card-tools crate

This commit is contained in:
Heiko Schaefer 2021-10-28 00:09:44 +02:00
parent aa7528ec9a
commit 59d77f584d
8 changed files with 898 additions and 2 deletions

View file

@ -1,13 +1,13 @@
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
# SPDX-License-Identifier: MIT OR Apache-2.0
[workspace]
members = [
"openpgp-card-apps",
"openpgp-card",
"openpgp-card-sequoia",
"pcsc",
"scdc",
"openpgp-card-apps",
"tools",
"card-functionality",
]

28
tools/Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: 2021 Wiktor Kwapisiewicz <wiktor@metacode.biz>
# SPDX-License-Identifier: MIT OR Apache-2.0
[package]
name = "openpgp-card-tools"
description = "Tools for using OpenPGP cards from the command line"
license = "MIT OR Apache-2.0"
version = "0.0.1"
authors = ["Heiko Schaefer <heiko@schaefer.name>"]
edition = "2018"
repository = "https://gitlab.com/hkos/openpgp-card"
documentation = "https://docs.rs/crate/openpgp-card-tools"
[dependencies]
sequoia-openpgp = "1.3"
nettle = "7"
openpgp-card = { path = "../openpgp-card", version = "0.0.4" }
openpgp-card-pcsc = { path = "../pcsc", version = "0.0.4" }
openpgp-card-scdc = { path = "../scdc", version = "0.0.2" }
openpgp-card-sequoia = { path = "../openpgp-card-sequoia", version = "0.0.3" }
rpassword = "5"
chrono = "0.4"
anyhow = "1"
thiserror = "1"
structopt = "0.3"
clap = "2.33"
env_logger = "0.8"
log = "0.4"

129
tools/README.md Normal file
View file

@ -0,0 +1,129 @@
<!--
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
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`
## 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
shell-scripts).
### List and inspect cards
List idents of all currently connected cards:
```
$ opgpcard list
```
Print status information about a card. The card is implicitly selected.
However, this only works if exactly one card is connected:
```
$ opgpcard status
```
Explicitly print the status information for a specific card:
```
$ opgpcard status -c ABCD:12345678
```
Add `-v` for more verbose card status, including the list of supported
algorithms of the card:
```
$ opgpcard status -c ABCD:12345678 -v
```
### 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`:
```
$ opgpcard admin -c ABCD:12345678 -p <pin-file> 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).
```
$ opgpcard admin -c ABCD:12345678 -p <pin-file> import key.priv \
--sig-fp "F290 DBBF 21DB 8634 3C96 157B 87BE 15B7 F548 D97C" \
--dec-fp "3C6E 08F6 7613 8935 8B8D 7666 73C7 F1A9 EEDA C360" \
--auth-fp "D6AA 48EF 39A2 6F26 C42D 5BCB AAD2 14D5 5332 C838"
```
When fingerprints are only specified for a subset of the roles, no
keys will be imported for the other roles.
### Set card metadata
Set cardholder name:
```
$ opgpcard admin -c ABCD:12345678 -p <pin-file> name "Bar<<Foo"
```
Set cardholder URL:
```
$ opgpcard admin -c ABCD:12345678 -p <pin-file> url "https://keyurl.example"
```
### Signing
For now, this tool only supports creating detached signatures, like this
(if no input file is set, stdin is read):
```
$ opgpcard sign --detached -c ABCD:12345678 -p <pin-file> -s <cert-file> <input-file>
```
### Decrypting
Decryption using a card (if no input file is set, stdin is read):
```
$ opgpcard decrypt -c ABCD:12345678 -p <pin-file> -r <cert-file> <input-file>
```
### Factory reset
Factory reset:
```
$ opgpcard factory-reset -c ABCD:12345678
```
## opgpcard-pin
An interactive tool to set the admin and user PINs, and to reset the user
PIN on OpenPGP cards.
Set the user PIN (requires admin PIN):
```
opgpcard-pin -c 1234:12345678 set-user-pin
```
Set new admin PIN (requires admin PIN):
```
opgpcard-pin -c 1234:12345678 set-admin-pin
```
Reset user PIN after it has been blocked (requires admin PIN):
```
opgpcard-pin -c 1234:12345678 reset-user-pin -a
```
Set resetting code (requires admin PIN):
```
opgpcard-pin -c 1234:12345678 set-reset-code
```
Reset user PIN (requires resetting code):
```
opgpcard-pin -c 1234:12345678 reset-user-pin
```

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
use clap::AppSettings;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(name = "opgpcard",
author = "Heiko Schäfer <heiko@schaefer.name>",
global_settings(& [AppSettings::VersionlessSubcommands,
AppSettings::DisableHelpSubcommand, AppSettings::DeriveDisplayOrder]),
about = "A tool for managing OpenPGP cards."
)]
pub struct Cli {
#[structopt(name = "card ident", short = "c", long = "card")]
pub ident: String,
#[structopt(subcommand)]
pub cmd: Command,
}
#[derive(StructOpt, Debug)]
pub enum Command {
SetUserPin {},
SetAdminPin {},
SetResetCode {},
ResetUserPin {
#[structopt(name = "reset as admin", short = "a", long = "admin")]
admin: bool,
},
}

View file

@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
use anyhow::Result;
use structopt::StructOpt;
use openpgp_card_pcsc::PcscClient;
use openpgp_card_sequoia::card::Open;
mod cli;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = cli::Cli::from_args();
let ccb = PcscClient::open_by_ident(&cli.ident)?;
let mut card = Open::open_card(ccb)?;
match cli.cmd {
cli::Command::SetUserPin {} => {
// get current user pin
let pin =
rpassword::read_password_from_tty(Some("Enter user PIN: "))?;
// verify pin
card.verify_user(&pin)?;
// get new user pin
let newpin1 = rpassword::read_password_from_tty(Some(
"Enter new user PIN: ",
))?;
let newpin2 = rpassword::read_password_from_tty(Some(
"Repeat the new user PIN: ",
))?;
if newpin1 != newpin2 {
return Err(anyhow::anyhow!("PINs do not match.").into());
}
// set new user pin
card.change_user_pin(&pin, &newpin1)?;
println!("\nUser PIN has been set.");
}
cli::Command::SetAdminPin {} => {
// get current admin pin
let pin =
rpassword::read_password_from_tty(Some("Enter admin PIN: "))?;
// verify pin
card.verify_admin(&pin)?;
// get new admin pin
let newpin1 = rpassword::read_password_from_tty(Some(
"Enter new admin PIN: ",
))?;
let newpin2 = rpassword::read_password_from_tty(Some(
"Repeat the new admin PIN: ",
))?;
if newpin1 != newpin2 {
return Err(anyhow::anyhow!("PINs do not match.").into());
}
// set new user pin
card.change_admin_pin(&pin, &newpin1)?;
println!("\nAdmin PIN has been set.");
}
cli::Command::SetResetCode {} => {
// get current admin pin
let pin =
rpassword::read_password_from_tty(Some("Enter admin PIN: "))?;
// verify admin pin
card.verify_admin(&pin)?;
if let Some(mut admin) = card.admin_card() {
// ask user for new resetting code
let newpin1 = rpassword::read_password_from_tty(Some(
"Enter new resetting code: ",
))?;
let newpin2 = rpassword::read_password_from_tty(Some(
"Repeat the new resetting code: ",
))?;
if newpin1 == newpin2 {
admin.set_resetting_code(&pin)?;
} else {
return Err(anyhow::anyhow!("PINs do not match.").into());
}
} else {
return Err(anyhow::anyhow!(
"Failed to use card in admin-mode."
)
.into());
}
println!("\nResetting code has been set.");
}
cli::Command::ResetUserPin { admin } => {
// either with resetting code, or by presenting pw3
let rst = if admin {
// get current admin pin
let pin = rpassword::read_password_from_tty(Some(
"Enter admin PIN: ",
))?;
// verify pin
card.verify_admin(&pin)?;
None
} else {
// get current admin pin
let rst = rpassword::read_password_from_tty(Some(
"Enter resetting code: ",
))?;
Some(rst)
};
// get new user pin
let newpin1 = rpassword::read_password_from_tty(Some(
"Enter new user PIN: ",
))?;
let newpin2 = rpassword::read_password_from_tty(Some(
"Repeat the new user PIN: ",
))?;
if newpin1 != newpin2 {
return Err(anyhow::anyhow!("PINs do not match.").into());
}
if let Some(rst) = rst {
// reset to new user pin
card.reset_user_pin(&rst, &newpin1)?;
} else {
if let Some(mut admin) = card.admin_card() {
admin.reset_user_pin(&newpin1)?;
} else {
unimplemented!()
}
}
println!("\nUser PIN has been set.");
}
}
Ok(())
}

View file

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
use clap::AppSettings;
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(name = "opgpcard",
author = "Heiko Schäfer <heiko@schaefer.name>",
global_settings(& [AppSettings::VersionlessSubcommands,
AppSettings::DisableHelpSubcommand, AppSettings::DeriveDisplayOrder]),
about = "A tool for managing OpenPGP cards."
)]
pub struct Cli {
#[structopt(subcommand)]
pub cmd: Command,
}
#[derive(StructOpt, Debug)]
pub enum Command {
List {},
Status {
#[structopt(name = "card ident", short = "c", long = "card")]
ident: Option<String>,
#[structopt(name = "verbose", short = "v", long = "verbose")]
verbose: bool,
},
FactoryReset {
#[structopt(name = "card ident", short = "c", long = "card")]
ident: String,
},
Admin {
#[structopt(name = "card ident", short = "c", long = "card")]
ident: String,
#[structopt(name = "admin pin file", short = "p", long = "pin-file")]
pin_file: PathBuf,
#[structopt(subcommand)]
cmd: AdminCommand,
},
Decrypt {
#[structopt(name = "card ident", short = "c", long = "card")]
ident: String,
#[structopt(name = "user pin file", short = "p", long = "pin-file")]
pin_file: PathBuf,
#[structopt(
name = "recipient-cert-file",
short = "r",
long = "recipient-cert"
)]
cert_file: PathBuf,
#[structopt(about = "Input file (stdin if unset)", name = "input")]
input: Option<PathBuf>,
},
Sign {
#[structopt(name = "card ident", short = "c", long = "card")]
ident: String,
#[structopt(name = "user pin file", short = "p", long = "pin-file")]
pin_file: PathBuf,
#[structopt(name = "detached", short = "d", long = "detached")]
detached: bool,
#[structopt(
name = "signer-cert-file",
short = "s",
long = "signer-cert"
)]
cert_file: PathBuf,
#[structopt(about = "Input file (stdin if unset)", name = "input")]
input: Option<PathBuf>,
},
}
#[derive(StructOpt, Debug)]
pub enum AdminCommand {
/// Set name
Name { name: String },
/// Set URL
Url { url: String },
/// Import a Key.
/// If no fingerprint is provided, the key will
Import {
keyfile: PathBuf,
#[structopt(
name = "Signature key fingerprint",
short = "s",
long = "sig-fp"
)]
sig_fp: Option<String>,
#[structopt(
name = "Decryption key fingerprint",
short = "d",
long = "dec-fp"
)]
dec_fp: Option<String>,
#[structopt(
name = "Authentication key fingerprint",
short = "a",
long = "auth-fp"
)]
auth_fp: Option<String>,
},
}

View file

@ -0,0 +1,377 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
use anyhow::Result;
use std::path::Path;
use structopt::StructOpt;
use sequoia_openpgp::parse::{stream::DecryptorBuilder, Parse};
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::serialize::stream::{Armorer, Message, Signer};
use sequoia_openpgp::Cert;
use openpgp_card_sequoia::card::Admin;
use openpgp_card_sequoia::sq_util;
use openpgp_card::{card_do::Sex, KeyType};
mod cli;
mod util;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = cli::Cli::from_args();
match cli.cmd {
cli::Command::List {} => {
list_cards()?;
}
cli::Command::Status { ident, verbose } => {
print_status(ident, verbose)?;
}
cli::Command::Decrypt {
ident,
pin_file,
cert_file,
input,
} => {
decrypt(&ident, &pin_file, &cert_file, input.as_deref())?;
}
cli::Command::Sign {
ident,
pin_file,
cert_file,
detached,
input,
} => {
if detached {
sign_detached(
&ident,
&pin_file,
&cert_file,
input.as_deref(),
)?;
} else {
return Err(anyhow::anyhow!(
"Only detached signatures are supported for now"
)
.into());
}
}
cli::Command::FactoryReset { ident } => {
factory_reset(&ident)?;
}
cli::Command::Admin {
ident,
pin_file,
cmd,
} => {
let mut open = util::open_card(&ident)?;
let mut admin = util::get_admin(&mut open, &pin_file)?;
match cmd {
cli::AdminCommand::Name { name } => {
let _ = admin.set_name(&name)?;
}
cli::AdminCommand::Url { url } => {
let _ = admin.set_url(&url)?;
}
cli::AdminCommand::Import {
keyfile,
sig_fp,
dec_fp,
auth_fp,
} => {
let key = Cert::from_file(keyfile)?;
if (&sig_fp, &dec_fp, &auth_fp) == (&None, &None, &None) {
// If no fingerprint has been provided, we check if
// there is zero or one (sub)key for each keytype,
// and if so, import these keys to the card.
key_import_yolo(admin, &key)?;
} else {
key_import_explicit(
admin, &key, sig_fp, dec_fp, auth_fp,
)?;
}
}
}
}
}
Ok(())
}
fn list_cards() -> Result<()> {
let cards = util::cards()?;
if !cards.is_empty() {
println!("Available OpenPGP cards:");
for card in cards {
println!(" {}", card.application_identifier()?.ident());
}
} else {
println!("No OpenPGP cards found.");
}
Ok(())
}
fn print_status(ident: Option<String>, verbose: bool) -> Result<()> {
let mut open = if let Some(ident) = ident {
util::open_card(&ident)?
} else {
let mut cards = util::cards()?;
if cards.len() == 1 {
cards.pop().unwrap()
} else {
return Err(anyhow::anyhow!("Found {} cards", cards.len()).into());
}
};
print!("OpenPGP card {}", open.application_identifier()?.ident());
let ai = open.application_identifier()?;
let version = ai.version().to_be_bytes();
println!(" (card version {}.{})\n", version[0], version[1]);
// card / cardholder metadata
let crd = open.cardholder_related_data()?;
if let Some(name) = crd.name() {
print!("Cardholder: ");
// This field is silly, maybe ignore it?!
if let Some(sex) = crd.sex() {
if sex == Sex::Male {
print!("Mr. ");
} else if sex == Sex::Female {
print!("Mrs. ");
}
}
// re-format name ("last<<first")
let name: Vec<_> = name.split("<<").collect();
let name = name.iter().cloned().rev().collect::<Vec<_>>().join(" ");
println!("{}", name);
}
let url = open.url()?;
if !url.is_empty() {
println!("URL: {}", url);
}
if let Some(lang) = crd.lang() {
let lang = lang
.iter()
.map(|lang| lang.iter().collect::<String>())
.collect::<Vec<_>>()
.join(", ");
println!("Language preferences '{}'", lang);
}
// information about subkeys
let fps = open.fingerprints()?;
let kgt = open.key_generation_times()?;
println!();
println!(
"Signature key ({})",
open.algorithm_attributes(KeyType::Signing)?,
);
if let Some(fp) = fps.signature() {
println!(" fingerprint: {}", fp.to_spaced_hex());
}
if let Some(kgt) = kgt.signature() {
println! {" created: {}",kgt.formatted()};
}
println!();
println!(
"Decryption key ({})",
open.algorithm_attributes(KeyType::Decryption)?,
);
if let Some(fp) = fps.decryption() {
println!(" fingerprint: {}", fp.to_spaced_hex());
}
if let Some(kgt) = kgt.decryption() {
println! {" created: {}",kgt.formatted()};
}
println!();
println!(
"Authentication key ({})",
open.algorithm_attributes(KeyType::Authentication)?,
);
if let Some(fp) = fps.authentication() {
println!(" fingerprint: {}", fp.to_spaced_hex());
}
if let Some(kgt) = kgt.authentication() {
println! {" created: {}",kgt.formatted()};
}
// technical details about the card and its state
println!();
let sst = open.security_support_template()?;
println!("Signature counter: {}", sst.get_signature_count());
let pws = open.pw_status_bytes()?;
println!(
"Signature pin only valid once: {}",
pws.get_pw1_cds_valid_once()
);
println!("Password validation retry count:");
println!(
" user pw: {}, reset: {}, admin pw: {}",
pws.get_err_count_pw1(),
pws.get_err_count_rst(),
pws.get_err_count_pw3(),
);
// FIXME: add General key info; login data; KDF setting
if verbose {
if let Some(ai) = open.algorithm_information()? {
println!();
println!("Supported algorithms:");
println!("{}", ai);
}
}
Ok(())
}
fn decrypt(
ident: &str,
pin_file: &Path,
cert_file: &Path,
input: Option<&Path>,
) -> Result<(), Box<dyn std::error::Error>> {
let p = StandardPolicy::new();
let cert = Cert::from_file(cert_file)?;
let input = util::open_or_stdin(input.as_deref())?;
let mut open = util::open_card(&ident)?;
let mut user = util::get_user(&mut open, &pin_file)?;
let d = user.decryptor(&cert, &p)?;
let db = DecryptorBuilder::from_reader(input)?;
let mut decryptor = db.with_policy(&p, None, d)?;
std::io::copy(&mut decryptor, &mut std::io::stdout())?;
Ok(())
}
fn sign_detached(
ident: &str,
pin_file: &Path,
cert_file: &Path,
input: Option<&Path>,
) -> Result<(), Box<dyn std::error::Error>> {
let p = StandardPolicy::new();
let cert = Cert::from_file(cert_file)?;
let mut input = util::open_or_stdin(input.as_deref())?;
let mut open = util::open_card(&ident)?;
let mut sign = util::get_sign(&mut open, &pin_file)?;
let s = sign.signer(&cert, &p)?;
let message = Armorer::new(Message::new(std::io::stdout())).build()?;
let mut signer = Signer::new(message, s).detached().build()?;
std::io::copy(&mut input, &mut signer)?;
signer.finalize()?;
Ok(())
}
fn factory_reset(ident: &str) -> Result<()> {
println!("Resetting Card {}", ident);
util::open_card(ident)?.factory_reset()
}
fn key_import_yolo(mut admin: Admin, key: &Cert) -> Result<()> {
let p = StandardPolicy::new();
let sig =
openpgp_card_sequoia::sq_util::get_subkey(&key, &p, KeyType::Signing)?;
let dec = openpgp_card_sequoia::sq_util::get_subkey(
&key,
&p,
KeyType::Decryption,
)?;
let auth = openpgp_card_sequoia::sq_util::get_subkey(
&key,
&p,
KeyType::Authentication,
)?;
if let Some(sig) = sig {
println!("Uploading {} as signing key", sig.fingerprint());
admin.upload_key(sig, KeyType::Signing, None)?;
}
if let Some(dec) = dec {
println!("Uploading {} as decryption key", dec.fingerprint());
admin.upload_key(dec, KeyType::Decryption, None)?;
}
if let Some(auth) = auth {
println!("Uploading {} as authentication key", auth.fingerprint());
admin.upload_key(auth, KeyType::Authentication, None)?;
}
Ok(())
}
fn key_import_explicit(
mut admin: Admin,
key: &Cert,
sig_fp: Option<String>,
dec_fp: Option<String>,
auth_fp: Option<String>,
) -> Result<()> {
let p = StandardPolicy::new();
if let Some(sig_fp) = sig_fp {
if let Some(sig) =
sq_util::get_subkey_by_fingerprint(&key, &p, &sig_fp)?
{
println!("Uploading {} as signing key", sig.fingerprint());
admin.upload_key(sig, KeyType::Signing, None)?;
} else {
println!("ERROR: Couldn't find {} as signing key", sig_fp);
}
}
if let Some(dec_fp) = dec_fp {
if let Some(dec) =
sq_util::get_subkey_by_fingerprint(&key, &p, &dec_fp)?
{
println!("Uploading {} as decryption key", dec.fingerprint());
admin.upload_key(dec, KeyType::Decryption, None)?;
} else {
println!("ERROR: Couldn't find {} as decryption key", dec_fp);
}
}
if let Some(auth_fp) = auth_fp {
if let Some(auth) =
sq_util::get_subkey_by_fingerprint(&key, &p, &auth_fp)?
{
println!("Uploading {} as authentication key", auth.fingerprint());
admin.upload_key(auth, KeyType::Authentication, None)?;
} else {
println!("ERROR: Couldn't find {} as authentication key", auth_fp);
}
}
Ok(())
}

View file

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
use anyhow::{Context, Result};
use openpgp_card::Error;
use openpgp_card_pcsc::PcscClient;
use openpgp_card_sequoia::card::{Admin, Open, Sign, User};
use std::path::Path;
pub(crate) fn cards() -> Result<Vec<Open>> {
let mut cards = vec![];
for card in PcscClient::cards()? {
cards.push(Open::open_card(card)?);
}
Ok(cards)
}
pub(crate) fn open_card(ident: &str) -> Result<Open, Error> {
let ccb = PcscClient::open_by_ident(ident)?;
Open::open_card(ccb)
}
pub(crate) fn get_user<'a>(
open: &'a mut Open,
pin_file: &Path,
) -> Result<User<'a>, Box<dyn std::error::Error>> {
open.verify_user(&get_pin(pin_file)?)?;
open.user_card()
.ok_or_else(|| anyhow::anyhow!("Couldn't get user access").into())
}
pub(crate) fn get_sign<'a>(
open: &'a mut Open,
pin_file: &Path,
) -> Result<Sign<'a>, Box<dyn std::error::Error>> {
open.verify_user_for_signing(&get_pin(pin_file)?)?;
open.signing_card()
.ok_or_else(|| anyhow::anyhow!("Couldn't get sign access").into())
}
pub(crate) fn get_admin<'a>(
open: &'a mut Open,
pin_file: &Path,
) -> Result<Admin<'a>, Box<dyn std::error::Error>> {
open.verify_admin(&get_pin(pin_file)?)?;
open.admin_card()
.ok_or_else(|| anyhow::anyhow!("Couldn't get admin access").into())
}
fn get_pin(pin_file: &Path) -> Result<String> {
let pin = std::fs::read_to_string(pin_file)?;
Ok(pin.trim().to_string())
}
pub(crate) fn open_or_stdin(
f: Option<&Path>,
) -> Result<Box<dyn std::io::Read + Send + Sync>> {
match f {
Some(f) => Ok(Box::new(
std::fs::File::open(f).context("Failed to open input file")?,
)),
None => Ok(Box::new(std::io::stdin())),
}
}