509 lines
15 KiB
Rust
509 lines
15 KiB
Rust
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
|
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
|
|
use anyhow::{anyhow, Result};
|
|
use clap::{Parser, ValueEnum};
|
|
use openpgp_card_sequoia::card::{Admin, Open, Transaction};
|
|
use openpgp_card_sequoia::util::public_key_material_to_key;
|
|
use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm};
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use openpgp_card_sequoia::{sq_util, PublicKey};
|
|
|
|
use sequoia_openpgp::cert::prelude::ValidErasedKeyAmalgamation;
|
|
use sequoia_openpgp::packet::key::{SecretParts, UnspecifiedRole};
|
|
use sequoia_openpgp::packet::Key;
|
|
use sequoia_openpgp::parse::Parse;
|
|
use sequoia_openpgp::policy::Policy;
|
|
use sequoia_openpgp::policy::StandardPolicy;
|
|
use sequoia_openpgp::serialize::SerializeInto;
|
|
use sequoia_openpgp::Cert;
|
|
|
|
use openpgp_card_sequoia::types::AlgoSimple;
|
|
use openpgp_card_sequoia::{card::Card, types::KeyType};
|
|
|
|
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
|
use crate::{output, util, ENTER_ADMIN_PIN, ENTER_USER_PIN};
|
|
|
|
#[derive(Parser, Debug)]
|
|
pub struct AdminCommand {
|
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
pub ident: String,
|
|
|
|
#[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")]
|
|
pub admin_pin: Option<PathBuf>,
|
|
|
|
#[clap(subcommand)]
|
|
pub cmd: AdminSubCommand,
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
pub enum AdminSubCommand {
|
|
/// Set cardholder name
|
|
Name { name: String },
|
|
|
|
/// Set cardholder URL
|
|
Url { url: String },
|
|
|
|
/// Import a Key.
|
|
///
|
|
/// If no fingerprint is provided, the key will only be imported if
|
|
/// there are zero or one (sub)keys for each key slot on the card.
|
|
Import {
|
|
keyfile: PathBuf,
|
|
|
|
#[clap(name = "Signature key fingerprint", short = 's', long = "sig-fp")]
|
|
sig_fp: Option<String>,
|
|
|
|
#[clap(name = "Decryption key fingerprint", short = 'd', long = "dec-fp")]
|
|
dec_fp: Option<String>,
|
|
|
|
#[clap(name = "Authentication key fingerprint", short = 'a', long = "aut-fp")]
|
|
aut_fp: Option<String>,
|
|
},
|
|
|
|
/// Generate a Key.
|
|
///
|
|
/// A signing key is always created, decryption and authentication keys
|
|
/// are optional.
|
|
Generate(AdminGenerateCommand),
|
|
|
|
/// Set touch policy
|
|
Touch {
|
|
#[clap(name = "Key slot", short = 'k', long = "key", value_enum)]
|
|
key: BasePlusAttKeySlot,
|
|
|
|
#[clap(name = "Policy", short = 'p', long = "policy", value_enum)]
|
|
policy: TouchPolicy,
|
|
},
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
pub struct AdminGenerateCommand {
|
|
#[clap(name = "User PIN file", short = 'p', long = "user-pin")]
|
|
user_pin: Option<PathBuf>,
|
|
|
|
/// Output file
|
|
#[clap(name = "output", long = "output", short = 'o')]
|
|
output_file: PathBuf,
|
|
|
|
#[clap(long = "no-decrypt", action = clap::ArgAction::SetFalse)]
|
|
decrypt: bool,
|
|
|
|
#[clap(long = "no-auth", action = clap::ArgAction::SetFalse)]
|
|
auth: bool,
|
|
|
|
/// Algorithm
|
|
#[clap(value_enum)]
|
|
algo: Option<Algo>,
|
|
|
|
/// User ID to add to the exported certificate representation
|
|
#[clap(name = "User ID", short = 'u', long = "userid")]
|
|
user_ids: Vec<String>,
|
|
}
|
|
|
|
#[derive(ValueEnum, Debug, Clone)]
|
|
#[clap(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 {
|
|
#[clap(name = "Off")]
|
|
Off,
|
|
#[clap(name = "On")]
|
|
On,
|
|
#[clap(name = "Fixed")]
|
|
Fixed,
|
|
#[clap(name = "Cached")]
|
|
Cached,
|
|
#[clap(name = "Cached-Fixed")]
|
|
CachedFixed,
|
|
}
|
|
|
|
impl From<TouchPolicy> for openpgp_card_sequoia::types::TouchPolicy {
|
|
fn from(tp: TouchPolicy) -> Self {
|
|
use openpgp_card_sequoia::types::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)]
|
|
#[clap(rename_all = "lower")]
|
|
pub enum Algo {
|
|
Rsa2048,
|
|
Rsa3072,
|
|
Rsa4096,
|
|
Nistp256,
|
|
Nistp384,
|
|
Nistp521,
|
|
Cv25519,
|
|
}
|
|
|
|
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::Cv25519 => AlgoSimple::Curve25519,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn admin(
|
|
output_format: OutputFormat,
|
|
output_version: OutputVersion,
|
|
command: AdminCommand,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let backend = util::open_card(&command.ident)?;
|
|
let mut open: Card<Open> = backend.into();
|
|
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.as_deref())?;
|
|
}
|
|
AdminSubCommand::Url { url } => {
|
|
url_command(&url, card, admin_pin.as_deref())?;
|
|
}
|
|
AdminSubCommand::Import {
|
|
keyfile,
|
|
sig_fp,
|
|
dec_fp,
|
|
aut_fp,
|
|
} => {
|
|
import_command(keyfile, sig_fp, dec_fp, aut_fp, card, admin_pin.as_deref())?;
|
|
}
|
|
AdminSubCommand::Generate(cmd) => {
|
|
generate_command(
|
|
output_format,
|
|
output_version,
|
|
card,
|
|
admin_pin.as_deref(),
|
|
cmd,
|
|
)?;
|
|
}
|
|
AdminSubCommand::Touch { key, policy } => {
|
|
touch_command(card, admin_pin.as_deref(), key, policy)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn keys_pick_yolo<'a>(
|
|
key: &'a Cert,
|
|
policy: &'a dyn Policy,
|
|
) -> Result<[Option<ValidErasedKeyAmalgamation<'a, SecretParts>>; 3]> {
|
|
let key_by_type = |kt| sq_util::subkey_by_type(key, policy, kt);
|
|
|
|
Ok([
|
|
key_by_type(KeyType::Signing)?,
|
|
key_by_type(KeyType::Decryption)?,
|
|
key_by_type(KeyType::Authentication)?,
|
|
])
|
|
}
|
|
|
|
fn keys_pick_explicit<'a>(
|
|
key: &'a Cert,
|
|
policy: &'a dyn Policy,
|
|
sig_fp: Option<String>,
|
|
dec_fp: Option<String>,
|
|
aut_fp: Option<String>,
|
|
) -> Result<[Option<ValidErasedKeyAmalgamation<'a, SecretParts>>; 3]> {
|
|
let key_by_fp = |fp: Option<String>| match fp {
|
|
Some(fp) => sq_util::private_subkey_by_fingerprint(key, policy, &fp),
|
|
None => Ok(None),
|
|
};
|
|
|
|
Ok([key_by_fp(sig_fp)?, key_by_fp(dec_fp)?, key_by_fp(aut_fp)?])
|
|
}
|
|
|
|
fn gen_subkeys(
|
|
admin: &mut Card<Admin>,
|
|
decrypt: bool,
|
|
auth: bool,
|
|
algo: Option<AlgoSimple>,
|
|
) -> Result<(PublicKey, Option<PublicKey>, Option<PublicKey>)> {
|
|
// We begin by generating the signing subkey, which is mandatory.
|
|
println!(" Generate subkey for Signing");
|
|
let (pkm, ts) = admin.generate_key_simple(KeyType::Signing, algo)?;
|
|
let key_sig = public_key_material_to_key(&pkm, KeyType::Signing, &ts, None, None)?;
|
|
|
|
// make decryption subkey (unless disabled), with the same algorithm as
|
|
// the sig key
|
|
let key_dec = if decrypt {
|
|
println!(" Generate subkey for Decryption");
|
|
let (pkm, ts) = admin.generate_key_simple(KeyType::Decryption, algo)?;
|
|
Some(public_key_material_to_key(
|
|
&pkm,
|
|
KeyType::Decryption,
|
|
&ts,
|
|
Some(HashAlgorithm::SHA256), // FIXME
|
|
Some(SymmetricAlgorithm::AES128), // FIXME
|
|
)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// make authentication subkey (unless disabled), with the same
|
|
// algorithm as the sig key
|
|
let key_aut = if auth {
|
|
println!(" Generate subkey for Authentication");
|
|
let (pkm, ts) = admin.generate_key_simple(KeyType::Authentication, algo)?;
|
|
|
|
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<&[u8]>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
|
|
|
admin.set_name(name)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn url_command(
|
|
url: &str,
|
|
mut card: Card<Transaction>,
|
|
admin_pin: Option<&[u8]>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
|
|
|
admin.set_url(url)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn import_command(
|
|
keyfile: PathBuf,
|
|
sig_fp: Option<String>,
|
|
dec_fp: Option<String>,
|
|
aut_fp: Option<String>,
|
|
mut card: Card<Transaction>,
|
|
admin_pin: Option<&[u8]>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let key = Cert::from_file(keyfile)?;
|
|
|
|
let p = StandardPolicy::new();
|
|
|
|
// select the (sub)keys to upload
|
|
let [sig, dec, auth] = match (&sig_fp, &dec_fp, &aut_fp) {
|
|
// No fingerprint has been provided, try to autoselect keys
|
|
// (this fails if there is more than one (sub)key for any keytype).
|
|
(&None, &None, &None) => keys_pick_yolo(&key, &p)?,
|
|
|
|
_ => keys_pick_explicit(&key, &p, sig_fp, dec_fp, aut_fp)?,
|
|
};
|
|
|
|
let mut pws: Vec<String> = vec![];
|
|
|
|
// helper: true, if `pw` decrypts `key`
|
|
let pw_ok = |key: &Key<SecretParts, UnspecifiedRole>, pw: &str| {
|
|
key.clone()
|
|
.decrypt_secret(&sequoia_openpgp::crypto::Password::from(pw))
|
|
.is_ok()
|
|
};
|
|
|
|
// helper: if any password in `pws` decrypts `key`, return that password
|
|
let find_pw = |key: &Key<SecretParts, UnspecifiedRole>, pws: &[String]| {
|
|
pws.iter().find(|pw| pw_ok(key, pw)).cloned()
|
|
};
|
|
|
|
// helper: check if we have the right password for `key` in `pws`,
|
|
// if so return it. otherwise ask the user for the password,
|
|
// add it to `pws` and return it.
|
|
let mut get_pw_for_key = |key: &Option<ValidErasedKeyAmalgamation<SecretParts>>,
|
|
key_type: &str|
|
|
-> Result<Option<String>> {
|
|
if let Some(k) = key {
|
|
if !k.has_secret() {
|
|
// key has no secret key material, it can't be imported
|
|
return Err(anyhow!(
|
|
"(Sub)Key {} contains no private key material",
|
|
k.fingerprint()
|
|
));
|
|
}
|
|
|
|
if k.has_unencrypted_secret() {
|
|
// key is unencrypted, we need no password
|
|
return Ok(None);
|
|
}
|
|
|
|
// key is encrypted, we need the password
|
|
|
|
// do we already have the right password?
|
|
if let Some(pw) = find_pw(k, &pws) {
|
|
return Ok(Some(pw));
|
|
}
|
|
|
|
// no, we need to get the password from user
|
|
let pw = rpassword::prompt_password(format!(
|
|
"Enter password for {} (sub)key {}:",
|
|
key_type,
|
|
k.fingerprint()
|
|
))?;
|
|
|
|
if pw_ok(k, &pw) {
|
|
// remember pw for next subkeys
|
|
pws.push(pw.clone());
|
|
|
|
Ok(Some(pw))
|
|
} else {
|
|
// this password doesn't work, error out
|
|
Err(anyhow!(
|
|
"Password not valid for (Sub)Key {}",
|
|
k.fingerprint()
|
|
))
|
|
}
|
|
} else {
|
|
// we have no key for this slot, so we don't need a password
|
|
Ok(None)
|
|
}
|
|
};
|
|
|
|
// get passwords, if encrypted (try previous pw before asking for user input)
|
|
let sig_p = get_pw_for_key(&sig, "signing")?;
|
|
let dec_p = get_pw_for_key(&dec, "decryption")?;
|
|
let auth_p = get_pw_for_key(&auth, "authentication")?;
|
|
|
|
// upload keys to card
|
|
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
|
|
|
if let Some(sig) = sig {
|
|
println!("Uploading {} as signing key", sig.fingerprint());
|
|
admin.upload_key(sig, KeyType::Signing, sig_p)?;
|
|
}
|
|
if let Some(dec) = dec {
|
|
println!("Uploading {} as decryption key", dec.fingerprint());
|
|
admin.upload_key(dec, KeyType::Decryption, dec_p)?;
|
|
}
|
|
if let Some(auth) = auth {
|
|
println!("Uploading {} as authentication key", auth.fingerprint());
|
|
admin.upload_key(auth, KeyType::Authentication, auth_p)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_command(
|
|
output_format: OutputFormat,
|
|
output_version: OutputVersion,
|
|
mut card: Card<Transaction>,
|
|
|
|
admin_pin: Option<&[u8]>,
|
|
|
|
cmd: AdminGenerateCommand,
|
|
) -> Result<()> {
|
|
let user_pin = util::get_pin(&mut card, cmd.user_pin, ENTER_USER_PIN);
|
|
|
|
let mut output = output::AdminGenerate::default();
|
|
output.ident(card.application_identifier()?.ident());
|
|
|
|
// 1) Interpret the user's choice of algorithm.
|
|
//
|
|
// Unset (None) means that the algorithm that is specified on the card
|
|
// should remain unchanged.
|
|
//
|
|
// For RSA, different cards use different exact algorithm
|
|
// specifications. In particular, the length of the value `e` differs
|
|
// between cards. Some devices use 32 bit length for e, others use 17 bit.
|
|
// In some cases, it's possible to get this information from the card,
|
|
// but I believe this information is not obtainable in all cases.
|
|
// Because of this, for generation of RSA keys, here we take the approach
|
|
// of first trying one variant, and then if that fails, try the other.
|
|
|
|
let algo = cmd.algo.map(AlgoSimple::from);
|
|
log::info!(" Key generation will be attempted with algo: {:?}", algo);
|
|
output.algorithm(format!("{:?}", algo));
|
|
|
|
// 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 card, admin_pin) {
|
|
gen_subkeys(&mut admin, cmd.decrypt, cmd.auth, algo)?
|
|
} else {
|
|
return Err(anyhow!("Failed to open card in admin mode."));
|
|
}
|
|
};
|
|
|
|
// 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 cert = crate::get_cert(
|
|
&mut card,
|
|
key_sig,
|
|
key_dec,
|
|
key_aut,
|
|
user_pin.as_deref(),
|
|
&cmd.user_ids,
|
|
&|| println!("Enter User PIN on card reader pinpad."),
|
|
)?;
|
|
|
|
let armored = String::from_utf8(cert.armored().to_vec()?)?;
|
|
output.public_key(armored);
|
|
|
|
// Write armored certificate to the output file
|
|
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<&[u8]>,
|
|
key: BasePlusAttKeySlot,
|
|
policy: TouchPolicy,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let kt = KeyType::from(key);
|
|
|
|
let pol = openpgp_card_sequoia::types::TouchPolicy::from(policy);
|
|
|
|
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
|
|
|
admin.set_uif(kt, pol)?;
|
|
Ok(())
|
|
}
|