use std::path::PathBuf;
use anyhow::{anyhow, Result};
use clap::{Parser, ValueEnum};
use openpgp_card::ocard::algorithm::AlgoSimple;
use openpgp_card::ocard::crypto::CardUploadableKey;
use openpgp_card::ocard::KeyType;
use openpgp_card::state::Admin;
use openpgp_card::state::Transaction;
use openpgp_card::Card;
use openpgp_card_rpgp::public_key_material_to_key;
use openpgp_card_rpgp::UploadableKey;
use pgp::packet::PublicKey;
use pgp::types::{Fingerprint, KeyDetails, KeyVersion};
use rpgpie::certificate::{Certificate, Checked};
use rpgpie::tsk::Tsk;
use secrecy::SecretString;
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
use crate::{output, util, ENTER_ADMIN_PIN, ENTER_USER_PIN};
#[derive(Parser, Debug)]
pub struct AdminCommand {
#[arg(
name = "card ident",
short = 'c',
long = "card",
help = "Identifier of the card to use"
)]
pub ident: String,
#[arg(
name = "Admin PIN file",
short = 'P',
long = "admin-pin",
help = "Optionally, get Admin PIN from a file"
)]
pub admin_pin: Option<PathBuf>,
#[command(subcommand)]
pub cmd: AdminSubCommand,
}
#[derive(Parser, Debug)]
pub enum AdminSubCommand {
Name {
#[arg(help = "cardholder name to set on the card")]
name: String,
},
Url {
#[arg(help = "URL that provides the certificate for the key material on this card")]
url: String,
},
SigningPinValidity {
#[arg(help = "Validity of user PIN presentation for signing operations")]
state: PinValidity,
},
Import {
#[arg(help = "File that contains the PGP private key")]
keyfile: PathBuf,
#[arg(name = "SIG subkey fingerprint", short = 's', long = "sig-fp")]
sig_fp: Option<String>,
#[arg(name = "DEC subkey fingerprint", short = 'd', long = "dec-fp")]
dec_fp: Option<String>,
#[arg(name = "AUT subkey fingerprint", short = 'a', long = "aut-fp")]
aut_fp: Option<String>,
#[arg(long = "key-passphrase")]
key_passphrase: Vec<PathBuf>,
},
Generate(AdminGenerateCommand),
Touch {
#[arg(name = "Key slot", short = 'k', long = "key", value_enum)]
key: BasePlusAttKeySlot,
#[arg(
name = "Policy",
short = 'p',
long = "policy",
value_enum,
long_help = "Touch policy to set on this key slot
Off: No touch confirmation required.
On: Touch confirmation required for each operation.
Fixed: Like 'On', but the policy can only be changed by a reset.
Cached: Like 'On', but touch confirmation is valid for 15 seconds.
Cached-Fixed: Combines 'Cached' and 'Fixed'."
)]
policy: TouchPolicy,
},
}
#[derive(Parser, Debug)]
pub struct AdminGenerateCommand {
#[arg(name = "output", long = "output", short = 'o')]
output_file: PathBuf,
#[arg(long = "no-dec", action = clap::ArgAction::SetFalse)]
decrypt: bool,
#[arg(long = "no-aut", action = clap::ArgAction::SetFalse)]
auth: bool,
#[arg(
name = "algorithm",
value_enum,
help = "Choose the algorithm for the key material to generate on the card.",
long_help = format!("Choose the algorithm for the key material to generate on the card.
If the parameter is not given, use the algorithm currently set on the card.
Specific cards support a set of algorithms that can differ between models. On modern cards,
use '{} info' to see the list of supported algorithms.", env!("CARGO_BIN_NAME"))
)]
algo: Option<Algo>,
#[arg(name = "User ID", short = 'u', long = "userid", required = true)]
user_ids: Vec<String>,
#[arg(
name = "User PIN file",
short = 'p',
long = "user-pin",
help = "Optionally, get User PIN from a file"
)]
user_pin: Option<PathBuf>,
}
#[derive(ValueEnum, Debug, Clone)]
pub enum PinValidity {
Once,
Unlimited,
}
impl From<PinValidity> for bool {
fn from(state: PinValidity) -> Self {
match state {
PinValidity::Unlimited => false,
PinValidity::Once => true,
}
}
}
#[derive(ValueEnum, Debug, Clone)]
#[value(rename_all = "UPPER")]
pub enum BasePlusAttKeySlot {
Sig,
Dec,
Aut,
Att,
}
impl From<BasePlusAttKeySlot> for KeyType {
fn from(ks: BasePlusAttKeySlot) -> Self {
match ks {
BasePlusAttKeySlot::Sig => KeyType::Signing,
BasePlusAttKeySlot::Dec => KeyType::Decryption,
BasePlusAttKeySlot::Aut => KeyType::Authentication,
BasePlusAttKeySlot::Att => KeyType::Attestation,
}
}
}
#[derive(ValueEnum, Debug, Clone)]
pub enum TouchPolicy {
#[value(name = "Off")]
Off,
#[value(name = "On")]
On,
#[value(name = "Fixed")]
Fixed,
#[value(name = "Cached")]
Cached,
#[value(name = "Cached-Fixed")]
CachedFixed,
}
impl From<TouchPolicy> for openpgp_card::ocard::data::TouchPolicy {
fn from(tp: TouchPolicy) -> Self {
use openpgp_card::ocard::data::TouchPolicy as OCTouchPolicy;
match tp {
TouchPolicy::On => OCTouchPolicy::On,
TouchPolicy::Off => OCTouchPolicy::Off,
TouchPolicy::Fixed => OCTouchPolicy::Fixed,
TouchPolicy::Cached => OCTouchPolicy::Cached,
TouchPolicy::CachedFixed => OCTouchPolicy::CachedFixed,
}
}
}
#[derive(ValueEnum, Debug, Clone)]
#[value(rename_all = "lower")]
pub enum Algo {
Rsa2048,
Rsa3072,
Rsa4096,
Nistp256,
Nistp384,
Nistp521,
Curve25519,
}
impl From<Algo> for AlgoSimple {
fn from(a: Algo) -> Self {
match a {
Algo::Rsa2048 => AlgoSimple::RSA2k,
Algo::Rsa3072 => AlgoSimple::RSA3k,
Algo::Rsa4096 => AlgoSimple::RSA4k,
Algo::Nistp256 => AlgoSimple::NIST256,
Algo::Nistp384 => AlgoSimple::NIST384,
Algo::Nistp521 => AlgoSimple::NIST521,
Algo::Curve25519 => AlgoSimple::Curve25519,
}
}
}
pub fn admin(
output_format: OutputFormat,
output_version: OutputVersion,
command: AdminCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let mut open = util::open_card(&command.ident)?;
let mut card = open.transaction()?;
let admin_pin = util::get_pin(&mut card, command.admin_pin, ENTER_ADMIN_PIN)?;
match command.cmd {
AdminSubCommand::Name { name } => {
name_command(&name, card, admin_pin)?;
}
AdminSubCommand::Url { url } => {
url_command(&url, card, admin_pin)?;
}
AdminSubCommand::SigningPinValidity { state } => {
signing_pin_validity(state, card, admin_pin)?;
}
AdminSubCommand::Import {
keyfile,
sig_fp,
dec_fp,
aut_fp,
key_passphrase,
} => {
import_command(
keyfile,
sig_fp,
dec_fp,
aut_fp,
key_passphrase,
card,
admin_pin,
)?;
}
AdminSubCommand::Generate(cmd) => {
generate_command(output_format, output_version, card, admin_pin, cmd)?;
}
AdminSubCommand::Touch { key, policy } => {
touch_command(card, admin_pin, key, policy)?;
}
}
Ok(())
}
fn keys_pick_yolo(tsk: &Tsk) -> Result<[Option<UploadableKey>; 3]> {
let cert = Certificate::from(tsk.clone());
let ccert = Checked::from(cert);
let now = chrono::offset::Utc::now();
let sig = ccert.valid_signing_capable_component_keys_at(&now);
let sig_fp = match sig.len() {
0 => None,
1 => Some(sig[0].as_componentkey().fingerprint()),
_ => {
return Err(anyhow::anyhow!(
"More than one valid signing component key found"
))
}
};
let dec = ccert.valid_encryption_capable_component_keys();
let dec_fp = match dec.len() {
0 => None,
1 => Some(dec[0].fingerprint()),
_ => {
return Err(anyhow::anyhow!(
"More than one valid encryption component key found"
))
}
};
let aut = ccert.valid_authentication_capable_component_keys(&now);
let aut_fp = match aut.len() {
0 => None,
1 => Some(aut[0].fingerprint()),
_ => {
return Err(anyhow::anyhow!(
"More than one valid authentication component key found"
))
}
};
keys_pick_explicit(tsk, sig_fp, dec_fp, aut_fp)
}
fn keys_pick_explicit(
tsk: &Tsk,
sig_fp: Option<Fingerprint>,
dec_fp: Option<Fingerprint>,
aut_fp: Option<Fingerprint>,
) -> Result<[Option<UploadableKey>; 3]> {
let key_by_fp = |fp: Option<Fingerprint>| match fp {
Some(fp) => component_key_by_fingerprint(tsk, fp),
None => Ok(None),
};
Ok([key_by_fp(sig_fp)?, key_by_fp(dec_fp)?, key_by_fp(aut_fp)?])
}
fn component_key_by_fingerprint(tsk: &Tsk, search: Fingerprint) -> Result<Option<UploadableKey>> {
let ssk = tsk.key();
let pri = &ssk.primary_key;
if pri.fingerprint() == search {
return Ok(Some(UploadableKey::from(pri.clone())));
}
for sk in &ssk.secret_subkeys {
if sk.fingerprint() == search {
return Ok(Some(UploadableKey::from(sk.key.clone())));
}
}
Ok(None)
}
fn gen_subkeys(
admin: &mut Card<Admin>,
decrypt: bool,
auth: bool,
algo: Option<AlgoSimple>,
) -> Result<(PublicKey, Option<PublicKey>, Option<PublicKey>)> {
eprintln!(" Generate subkey for Signing");
algo.map(|a| admin.set_algorithm(KeyType::Signing, a));
let (pkm, ts) =
admin.generate_key(openpgp_card_rpgp::public_to_fingerprint, KeyType::Signing)?;
let key_sig = public_key_material_to_key(&pkm, KeyType::Signing, &ts, None, None)?;
let key_dec = if decrypt {
eprintln!(" Generate subkey for Decryption");
algo.map(|a| admin.set_algorithm(KeyType::Decryption, a));
let (pkm, ts) = admin.generate_key(
openpgp_card_rpgp::public_to_fingerprint,
KeyType::Decryption,
)?;
Some(public_key_material_to_key(
&pkm,
KeyType::Decryption,
&ts,
None,
None,
)?)
} else {
None
};
let key_aut = if auth {
eprintln!(" Generate subkey for Authentication");
algo.map(|a| admin.set_algorithm(KeyType::Authentication, a));
let (pkm, ts) = admin.generate_key(
openpgp_card_rpgp::public_to_fingerprint,
KeyType::Authentication,
)?;
Some(public_key_material_to_key(
&pkm,
KeyType::Authentication,
&ts,
None,
None,
)?)
} else {
None
};
Ok((key_sig, key_dec, key_aut))
}
fn name_command(
name: &str,
mut card: Card<Transaction>,
admin_pin: Option<SecretString>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
admin.set_cardholder_name(name)?;
Ok(())
}
fn url_command(
url: &str,
mut card: Card<Transaction>,
admin_pin: Option<SecretString>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
admin.set_url(url)?;
Ok(())
}
fn signing_pin_validity(
state: PinValidity,
mut card: Card<Transaction>,
admin_pin: Option<SecretString>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
eprintln!("User PIN validity duration for signing: {state:?}");
admin.set_user_pin_signing_validity(bool::from(state))?;
Ok(())
}
fn import_command(
keyfile: PathBuf,
sig_fp: Option<String>,
dec_fp: Option<String>,
aut_fp: Option<String>,
key_passphrase: Vec<PathBuf>,
mut card: Card<Transaction>,
admin_pin: Option<SecretString>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut file = std::fs::File::open(keyfile)?;
let mut key = Tsk::load(&mut file)?;
if key.len() != 1 {
return Err(anyhow!("Expected one OpenPGP key, found {}", key.len()).into());
}
let key: Tsk = key.remove(0);
fn to_fp(fp: Option<String>) -> Result<Option<Fingerprint>> {
let fp = fp.map(hex::decode).transpose()?;
Ok(fp
.map(|v| Fingerprint::new(KeyVersion::V4, &v))
.transpose()?)
}
let [mut sig, mut dec, mut auth] = match (sig_fp, dec_fp, aut_fp) {
(None, None, None) => keys_pick_yolo(&key)?,
(sig_fp, dec_fp, aut_fp) => {
keys_pick_explicit(&key, to_fp(sig_fp)?, to_fp(dec_fp)?, to_fp(aut_fp)?)?
}
};
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
{
let mut pws: Vec<String> = vec![];
for pw in key_passphrase {
let pw = std::fs::read_to_string(pw)?;
pws.push(pw);
}
let mut unlock_key = |key: &mut Option<UploadableKey>, key_type: &str| -> Result<()> {
if let Some(k) = key {
if !k.is_locked() {
return Ok(());
}
for pw in &pws {
if k.try_unlock(pw).is_ok() {
return Ok(());
}
}
let pw = rpassword::prompt_password(format!(
"Enter password for {} (sub)key {}:",
key_type,
k.fingerprint()?.to_hex()
))?;
if k.try_unlock(&pw).is_ok() {
pws.push(pw.clone());
Ok(())
} else {
Err(anyhow!(
"Password not valid for (Sub)Key {}",
k.fingerprint()?.to_hex()
))
}
} else {
Ok(())
}
};
unlock_key(&mut sig, "signing")?;
unlock_key(&mut dec, "decryption")?;
unlock_key(&mut auth, "authentication")?;
}
if let Some(sig) = sig {
eprintln!("Uploading {} as signing key", sig.fingerprint()?);
admin.import_key(Box::new(sig), KeyType::Signing)?;
}
if let Some(dec) = dec {
eprintln!("Uploading {} as decryption key", dec.fingerprint()?);
admin.import_key(Box::new(dec), KeyType::Decryption)?;
}
if let Some(auth) = auth {
eprintln!("Uploading {} as authentication key", auth.fingerprint()?);
admin.import_key(Box::new(auth), KeyType::Authentication)?;
}
Ok(())
}
fn generate_command(
output_format: OutputFormat,
output_version: OutputVersion,
mut card: Card<Transaction>,
admin_pin: Option<SecretString>,
cmd: AdminGenerateCommand,
) -> Result<()> {
let _ = util::verify_to_admin(&mut card, admin_pin)
.map_err(|e| anyhow!("Failed to open card in admin mode ({}).", e))?;
let user_pin = util::get_pin(&mut card, cmd.user_pin, ENTER_USER_PIN)?;
if let Some(user_pin) = user_pin.clone() {
card.verify_user_signing_pin(user_pin)
.map_err(|e| anyhow!("Failed to open card in user mode ({}).", e))?;
}
let mut output = output::AdminGenerate::default();
output.ident(card.application_identifier()?.ident());
let algo = cmd.algo.map(AlgoSimple::from);
log::info!(" Key generation will be attempted with algo: {algo:?}");
output.algorithm(format!("{algo:?}"));
let (key_sig, key_dec, key_aut) = {
let mut admin = card.to_admin_card(None)?;
gen_subkeys(&mut admin, cmd.decrypt, cmd.auth, algo)?
};
let cert = crate::get_cert(
&mut card,
key_sig,
key_dec,
key_aut,
user_pin,
&cmd.user_ids,
&|| eprintln!("Enter User PIN on card reader pinpad."),
)?;
let mut buf = Vec::new();
cert.save(true, &mut buf)?;
let armored = std::str::from_utf8(buf.as_slice())?.to_string();
output.public_key(armored);
let mut handle = util::open_or_stdout(Some(&cmd.output_file))?;
handle.write_all(output.print(output_format, output_version)?.as_bytes())?;
let _ = handle.write(b"\n")?;
Ok(())
}
fn touch_command(
mut card: Card<Transaction>,
admin_pin: Option<SecretString>,
key: BasePlusAttKeySlot,
policy: TouchPolicy,
) -> Result<(), Box<dyn std::error::Error>> {
let kt = KeyType::from(key);
let pol = openpgp_card::ocard::data::TouchPolicy::from(policy);
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
admin.set_touch_policy(kt, pol)?;
Ok(())
}