Merge branch 'nora/modules' into 'main'
opgpcard: Split into modules See merge request openpgp-card/openpgp-card!17
This commit is contained in:
commit
497b825c2b
14 changed files with 1678 additions and 1430 deletions
|
@ -2,13 +2,13 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
use clap::{AppSettings, Parser, ValueEnum};
|
use clap::{AppSettings, Parser};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
|
use crate::commands;
|
||||||
use crate::{OutputFormat, OutputVersion};
|
use crate::{OutputFormat, OutputVersion};
|
||||||
|
|
||||||
pub const DEFAULT_OUTPUT_VERSION: &str = "0.9.0";
|
|
||||||
pub const OUTPUT_VERSIONS: &[OutputVersion] = &[OutputVersion::new(0, 9, 0)];
|
pub const OUTPUT_VERSIONS: &[OutputVersion] = &[OutputVersion::new(0, 9, 0)];
|
||||||
|
pub const DEFAULT_OUTPUT_VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[clap(
|
#[clap(
|
||||||
|
@ -20,11 +20,11 @@ pub const OUTPUT_VERSIONS: &[OutputVersion] = &[OutputVersion::new(0, 9, 0)];
|
||||||
)]
|
)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Produce output in the chosen format.
|
/// Produce output in the chosen format.
|
||||||
#[clap(long, value_enum, default_value = "text")]
|
#[clap(long, value_enum, default_value_t = OutputFormat::Text)]
|
||||||
pub output_format: OutputFormat,
|
pub output_format: OutputFormat,
|
||||||
|
|
||||||
/// Pick output version to use, for non-textual formats.
|
/// Pick output version to use, for non-textual formats.
|
||||||
#[clap(long, default_value = DEFAULT_OUTPUT_VERSION)]
|
#[clap(long, default_value_t = DEFAULT_OUTPUT_VERSION)]
|
||||||
pub output_version: OutputVersion,
|
pub output_version: OutputVersion,
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
|
@ -41,368 +41,35 @@ pub enum Command {
|
||||||
List {},
|
List {},
|
||||||
|
|
||||||
/// Show information about the data on a card
|
/// Show information about the data on a card
|
||||||
Status {
|
Status(commands::status::StatusCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: Option<String>,
|
|
||||||
|
|
||||||
#[clap(name = "verbose", short = 'v', long = "verbose")]
|
|
||||||
verbose: bool,
|
|
||||||
|
|
||||||
/// Print public key material for each key slot
|
|
||||||
#[clap(name = "pkm", short = 'K', long = "public-key-material")]
|
|
||||||
pkm: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Show technical details about a card
|
/// Show technical details about a card
|
||||||
Info {
|
Info(commands::info::InfoCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Display a card's authentication key as an SSH public key
|
/// Display a card's authentication key as an SSH public key
|
||||||
Ssh {
|
Ssh(commands::ssh::SshCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Export the key data on a card as an OpenPGP public key
|
/// Export the key data on a card as an OpenPGP public key
|
||||||
Pubkey {
|
Pubkey(commands::pubkey::PubkeyCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: Option<String>,
|
|
||||||
|
|
||||||
#[clap(name = "User PIN file", short = 'p', long = "user-pin")]
|
|
||||||
user_pin: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// User ID to add to the exported certificate representation
|
|
||||||
#[clap(name = "User ID", short = 'u', long = "userid")]
|
|
||||||
user_id: Vec<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Administer data on a card (including keys and metadata)
|
/// Administer data on a card (including keys and metadata)
|
||||||
Admin {
|
Admin(commands::admin::AdminCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: String,
|
|
||||||
|
|
||||||
#[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")]
|
|
||||||
admin_pin: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[clap(subcommand)]
|
|
||||||
cmd: AdminCommand,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// PIN management (change PINs, reset blocked PINs)
|
/// PIN management (change PINs, reset blocked PINs)
|
||||||
Pin {
|
Pin(commands::pin::PinCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: String,
|
|
||||||
|
|
||||||
#[clap(subcommand)]
|
|
||||||
cmd: PinCommand,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Decrypt data using a card
|
/// Decrypt data using a card
|
||||||
Decrypt {
|
Decrypt(commands::decrypt::DecryptCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: String,
|
|
||||||
|
|
||||||
#[clap(name = "User PIN file", short = 'p', long = "user-pin")]
|
|
||||||
user_pin: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Input file (stdin if unset)
|
|
||||||
#[clap(name = "input")]
|
|
||||||
input: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Sign data using a card
|
/// Sign data using a card
|
||||||
Sign {
|
Sign(commands::sign::SignCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: String,
|
|
||||||
|
|
||||||
/// User PIN file
|
|
||||||
#[clap(short = 'p', long = "user-pin")]
|
|
||||||
user_pin: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[clap(name = "detached", short = 'd', long = "detached")]
|
|
||||||
detached: bool,
|
|
||||||
|
|
||||||
/// Input file (stdin if unset)
|
|
||||||
#[clap(name = "input")]
|
|
||||||
input: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Attestation management (Yubico)
|
/// Attestation management (Yubico)
|
||||||
Attestation {
|
Attestation(commands::attestation::AttestationCommand),
|
||||||
#[clap(subcommand)]
|
|
||||||
cmd: AttCommand,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Completely reset a card (deletes all data, including the keys on the card!)
|
/// Completely reset a card (deletes all data, including the keys on the card!)
|
||||||
FactoryReset {
|
FactoryReset(commands::factory_reset::FactoryResetCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Change identity (applies only to Nitrokey Start)
|
/// Change identity (applies only to Nitrokey Start)
|
||||||
SetIdentity {
|
SetIdentity(commands::set_identity::SetIdentityCommand),
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: String,
|
|
||||||
|
|
||||||
#[clap(name = "identity", value_enum)]
|
|
||||||
id: SetIdentityId,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
pub enum AdminCommand {
|
|
||||||
/// 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 = "auth-fp")]
|
|
||||||
auth_fp: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Generate a Key.
|
|
||||||
///
|
|
||||||
/// A signing key is always created, decryption and authentication keys
|
|
||||||
/// are optional.
|
|
||||||
Generate {
|
|
||||||
#[clap(name = "User PIN file", short = 'p', long = "user-pin")]
|
|
||||||
user_pin: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Output file (stdout if unset)
|
|
||||||
#[clap(name = "output", long = "output", short = 'o')]
|
|
||||||
output: Option<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<AdminGenerateAlgo>,
|
|
||||||
|
|
||||||
/// User ID to add to the exported certificate representation
|
|
||||||
#[clap(name = "User ID", short = 'u', long = "userid")]
|
|
||||||
user_id: Vec<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// 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 enum PinCommand {
|
|
||||||
/// Set User PIN
|
|
||||||
SetUser {
|
|
||||||
#[clap(name = "User PIN file old", short = 'p', long = "user-pin-old")]
|
|
||||||
user_pin_old: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[clap(name = "User PIN file new", short = 'q', long = "user-pin-new")]
|
|
||||||
user_pin_new: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Set Admin PIN
|
|
||||||
SetAdmin {
|
|
||||||
#[clap(name = "Admin PIN file old", short = 'P', long = "admin-pin-old")]
|
|
||||||
admin_pin_old: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[clap(name = "Admin PIN file new", short = 'Q', long = "admin-pin-new")]
|
|
||||||
admin_pin_new: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Reset User PIN with Admin PIN
|
|
||||||
ResetUser {
|
|
||||||
#[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")]
|
|
||||||
admin_pin: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[clap(name = "User PIN file new", short = 'p', long = "user-pin-new")]
|
|
||||||
user_pin_new: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Set Resetting Code
|
|
||||||
SetReset {
|
|
||||||
#[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")]
|
|
||||||
admin_pin: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[clap(name = "Resetting code file", short = 'r', long = "reset-code")]
|
|
||||||
reset_code: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Reset User PIN with 'Resetting Code'
|
|
||||||
ResetUserRc {
|
|
||||||
#[clap(name = "Resetting Code file", short = 'r', long = "reset-code")]
|
|
||||||
reset_code: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[clap(name = "User PIN file new", short = 'p', long = "user-pin-new")]
|
|
||||||
user_pin_new: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
pub enum AttCommand {
|
|
||||||
/// Print the card's "Attestation Certificate"
|
|
||||||
Cert {
|
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Generate "Attestation Statement" for one of the key slots on the card
|
|
||||||
Generate {
|
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: String,
|
|
||||||
|
|
||||||
#[clap(name = "Key slot", short = 'k', long = "key", value_enum)]
|
|
||||||
key: BaseKeySlot,
|
|
||||||
|
|
||||||
#[clap(name = "User PIN file", short = 'p', long = "user-pin")]
|
|
||||||
user_pin: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Print a "cardholder certificate" from the card.
|
|
||||||
/// This shows the "Attestation Statement", if one has been generated.
|
|
||||||
Statement {
|
|
||||||
#[clap(name = "card ident", short = 'c', long = "card")]
|
|
||||||
ident: Option<String>,
|
|
||||||
|
|
||||||
#[clap(name = "Key slot", short = 'k', long = "key", value_enum)]
|
|
||||||
key: BaseKeySlot,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ValueEnum, Debug, Clone)]
|
|
||||||
#[clap(rename_all = "UPPER")]
|
|
||||||
pub enum BaseKeySlot {
|
|
||||||
Sig,
|
|
||||||
Dec,
|
|
||||||
Aut,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BaseKeySlot> for openpgp_card_sequoia::types::KeyType {
|
|
||||||
fn from(ks: BaseKeySlot) -> Self {
|
|
||||||
use openpgp_card_sequoia::types::KeyType;
|
|
||||||
match ks {
|
|
||||||
BaseKeySlot::Sig => KeyType::Signing,
|
|
||||||
BaseKeySlot::Dec => KeyType::Decryption,
|
|
||||||
BaseKeySlot::Aut => KeyType::Authentication,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ValueEnum, Debug, Clone)]
|
|
||||||
#[clap(rename_all = "UPPER")]
|
|
||||||
pub enum BasePlusAttKeySlot {
|
|
||||||
Sig,
|
|
||||||
Dec,
|
|
||||||
Aut,
|
|
||||||
Att,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BasePlusAttKeySlot> for openpgp_card_sequoia::types::KeyType {
|
|
||||||
fn from(ks: BasePlusAttKeySlot) -> Self {
|
|
||||||
use openpgp_card_sequoia::types::KeyType;
|
|
||||||
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)]
|
|
||||||
pub enum SetIdentityId {
|
|
||||||
#[clap(name = "0")]
|
|
||||||
Zero,
|
|
||||||
#[clap(name = "1")]
|
|
||||||
One,
|
|
||||||
#[clap(name = "2")]
|
|
||||||
Two,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SetIdentityId> for u8 {
|
|
||||||
fn from(id: SetIdentityId) -> Self {
|
|
||||||
match id {
|
|
||||||
SetIdentityId::Zero => 0,
|
|
||||||
SetIdentityId::One => 1,
|
|
||||||
SetIdentityId::Two => 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ValueEnum, Debug, Clone)]
|
|
||||||
#[clap(rename_all = "lower")]
|
|
||||||
pub enum AdminGenerateAlgo {
|
|
||||||
Rsa2048,
|
|
||||||
Rsa3072,
|
|
||||||
Rsa4096,
|
|
||||||
Nistp256,
|
|
||||||
Nistp384,
|
|
||||||
Nistp521,
|
|
||||||
Curve25519,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<AdminGenerateAlgo> for openpgp_card_sequoia::types::AlgoSimple {
|
|
||||||
fn from(aga: AdminGenerateAlgo) -> Self {
|
|
||||||
use openpgp_card_sequoia::types::AlgoSimple;
|
|
||||||
|
|
||||||
match aga {
|
|
||||||
AdminGenerateAlgo::Rsa2048 => AlgoSimple::RSA2k,
|
|
||||||
AdminGenerateAlgo::Rsa3072 => AlgoSimple::RSA3k,
|
|
||||||
AdminGenerateAlgo::Rsa4096 => AlgoSimple::RSA4k,
|
|
||||||
AdminGenerateAlgo::Nistp256 => AlgoSimple::NIST256,
|
|
||||||
AdminGenerateAlgo::Nistp384 => AlgoSimple::NIST384,
|
|
||||||
AdminGenerateAlgo::Nistp521 => AlgoSimple::NIST521,
|
|
||||||
AdminGenerateAlgo::Curve25519 => AlgoSimple::Curve25519,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
503
tools/src/bin/opgpcard/commands/admin.rs
Normal file
503
tools/src/bin/opgpcard/commands/admin.rs
Normal file
|
@ -0,0 +1,503 @@
|
||||||
|
// 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};
|
||||||
|
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 = "auth-fp")]
|
||||||
|
auth_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 (stdout if unset)
|
||||||
|
#[clap(name = "output", long = "output", short = 'o')]
|
||||||
|
output_file: Option<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 openpgp_card_sequoia::types::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,
|
||||||
|
Curve25519,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Algo> for openpgp_card_sequoia::types::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 backend = util::open_card(&command.ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
let admin_pin = util::get_pin(&mut open, command.admin_pin, ENTER_ADMIN_PIN);
|
||||||
|
|
||||||
|
match command.cmd {
|
||||||
|
AdminSubCommand::Name { name } => {
|
||||||
|
name_command(&name, open, admin_pin.as_deref())?;
|
||||||
|
}
|
||||||
|
AdminSubCommand::Url { url } => {
|
||||||
|
url_command(&url, open, admin_pin.as_deref())?;
|
||||||
|
}
|
||||||
|
AdminSubCommand::Import {
|
||||||
|
keyfile,
|
||||||
|
sig_fp,
|
||||||
|
dec_fp,
|
||||||
|
auth_fp,
|
||||||
|
} => {
|
||||||
|
import_command(keyfile, sig_fp, dec_fp, auth_fp, open, admin_pin.as_deref())?;
|
||||||
|
}
|
||||||
|
AdminSubCommand::Generate(cmd) => {
|
||||||
|
generate_command(output_format, output_version, open, admin_pin.as_deref(), cmd)?;
|
||||||
|
}
|
||||||
|
AdminSubCommand::Touch { key, policy } => {
|
||||||
|
touch_command(open, 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>,
|
||||||
|
auth_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(auth_fp)?])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_subkeys(
|
||||||
|
admin: &mut 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 open: Open,
|
||||||
|
admin_pin: Option<&[u8]>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut admin = util::verify_to_admin(&mut open, admin_pin)?;
|
||||||
|
|
||||||
|
admin.set_name(name)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url_command(
|
||||||
|
url: &str,
|
||||||
|
mut open: Open,
|
||||||
|
admin_pin: Option<&[u8]>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut admin = util::verify_to_admin(&mut open, admin_pin)?;
|
||||||
|
|
||||||
|
admin.set_url(url)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_command(
|
||||||
|
keyfile: PathBuf,
|
||||||
|
sig_fp: Option<String>,
|
||||||
|
dec_fp: Option<String>,
|
||||||
|
auth_fp: Option<String>,
|
||||||
|
mut open: Open,
|
||||||
|
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, &auth_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, auth_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 open, 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 open: Open,
|
||||||
|
|
||||||
|
admin_pin: Option<&[u8]>,
|
||||||
|
|
||||||
|
cmd: AdminGenerateCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user_pin = util::get_pin(&mut open, cmd.user_pin, ENTER_USER_PIN);
|
||||||
|
|
||||||
|
let mut output = output::AdminGenerate::default();
|
||||||
|
output.ident(open.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 open, 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 open,
|
||||||
|
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 (or stdout)
|
||||||
|
let mut handle = util::open_or_stdout(cmd.output_file.as_deref())?;
|
||||||
|
handle.write_all(output.print(output_format, output_version)?.as_bytes())?;
|
||||||
|
let _ = handle.write(b"\n")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn touch_command(
|
||||||
|
mut open: Open,
|
||||||
|
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 open, admin_pin)?;
|
||||||
|
|
||||||
|
admin.set_uif(kt, pol)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
167
tools/src/bin/opgpcard/commands/attestation.rs
Normal file
167
tools/src/bin/opgpcard/commands/attestation.rs
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
// 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 std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
use openpgp_card_sequoia::types::KeyType;
|
||||||
|
|
||||||
|
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||||
|
use crate::ENTER_USER_PIN;
|
||||||
|
use crate::{output, pick_card_for_reading, util};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct AttestationCommand {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub cmd: AttSubCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub enum AttSubCommand {
|
||||||
|
/// Print the card's "Attestation Certificate"
|
||||||
|
Cert {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
ident: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Generate "Attestation Statement" for one of the key slots on the card
|
||||||
|
Generate {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
ident: String,
|
||||||
|
|
||||||
|
#[clap(name = "Key slot", short = 'k', long = "key", value_enum)]
|
||||||
|
key: BaseKeySlot,
|
||||||
|
|
||||||
|
#[clap(name = "User PIN file", short = 'p', long = "user-pin")]
|
||||||
|
user_pin: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Print a "cardholder certificate" from the card.
|
||||||
|
/// This shows the "Attestation Statement", if one has been generated.
|
||||||
|
Statement {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
ident: Option<String>,
|
||||||
|
|
||||||
|
#[clap(name = "Key slot", short = 'k', long = "key", value_enum)]
|
||||||
|
key: BaseKeySlot,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ValueEnum, Debug, Clone)]
|
||||||
|
#[clap(rename_all = "UPPER")]
|
||||||
|
pub enum BaseKeySlot {
|
||||||
|
Sig,
|
||||||
|
Dec,
|
||||||
|
Aut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BaseKeySlot> for openpgp_card_sequoia::types::KeyType {
|
||||||
|
fn from(ks: BaseKeySlot) -> Self {
|
||||||
|
match ks {
|
||||||
|
BaseKeySlot::Sig => KeyType::Signing,
|
||||||
|
BaseKeySlot::Dec => KeyType::Decryption,
|
||||||
|
BaseKeySlot::Aut => KeyType::Authentication,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attestation(
|
||||||
|
output_format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
command: AttestationCommand,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
match command.cmd {
|
||||||
|
AttSubCommand::Cert { ident } => cert(output_format, output_version, ident),
|
||||||
|
AttSubCommand::Generate {
|
||||||
|
ident,
|
||||||
|
key,
|
||||||
|
user_pin,
|
||||||
|
} => generate(&ident, key, user_pin),
|
||||||
|
AttSubCommand::Statement { ident, key } => statement(ident, key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cert(
|
||||||
|
output_format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
ident: Option<String>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut output = output::AttestationCert::default();
|
||||||
|
|
||||||
|
let backend = pick_card_for_reading(ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
output.ident(open.application_identifier()?.ident());
|
||||||
|
|
||||||
|
if let Ok(ac) = open.attestation_certificate() {
|
||||||
|
let pem = util::pem_encode(ac);
|
||||||
|
output.attestation_cert(pem);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", output.print(output_format, output_version)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate(
|
||||||
|
ident: &str,
|
||||||
|
key: BaseKeySlot,
|
||||||
|
user_pin: Option<PathBuf>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let backend = util::open_card(ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN);
|
||||||
|
|
||||||
|
let mut sign = util::verify_to_sign(&mut open, user_pin.as_deref())?;
|
||||||
|
|
||||||
|
let kt = KeyType::from(key);
|
||||||
|
sign.generate_attestation(kt, &|| {
|
||||||
|
println!("Touch confirmation needed to generate an attestation")
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn statement(ident: Option<String>, key: BaseKeySlot) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let backend = pick_card_for_reading(ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
// Get cardholder certificate from card.
|
||||||
|
|
||||||
|
let mut select_data_workaround = false;
|
||||||
|
// Use "select data" workaround if the card reports a
|
||||||
|
// yk firmware version number >= 5 and <= 5.4.3
|
||||||
|
if let Ok(version) = open.firmware_version() {
|
||||||
|
if version.len() == 3
|
||||||
|
&& version[0] == 5
|
||||||
|
&& (version[1] < 4 || (version[1] == 4 && version[2] <= 3))
|
||||||
|
{
|
||||||
|
select_data_workaround = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select cardholder certificate
|
||||||
|
match key {
|
||||||
|
BaseKeySlot::Aut => open.select_data(0, &[0x7F, 0x21], select_data_workaround)?,
|
||||||
|
BaseKeySlot::Dec => open.select_data(1, &[0x7F, 0x21], select_data_workaround)?,
|
||||||
|
BaseKeySlot::Sig => open.select_data(2, &[0x7F, 0x21], select_data_workaround)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get DO "cardholder certificate" (returns the slot that was previously selected)
|
||||||
|
let cert = open.cardholder_certificate()?;
|
||||||
|
|
||||||
|
if !cert.is_empty() {
|
||||||
|
let pem = util::pem_encode(cert);
|
||||||
|
println!("{}", pem);
|
||||||
|
} else {
|
||||||
|
println!("Cardholder certificate slot is empty");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
56
tools/src/bin/opgpcard/commands/decrypt.rs
Normal file
56
tools/src/bin/opgpcard/commands/decrypt.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use sequoia_openpgp::{
|
||||||
|
parse::{stream::DecryptorBuilder, Parse},
|
||||||
|
policy::StandardPolicy,
|
||||||
|
};
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct DecryptCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
ident: String,
|
||||||
|
|
||||||
|
#[clap(name = "User PIN file", short = 'p', long = "user-pin")]
|
||||||
|
pin_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Input file (stdin if unset)
|
||||||
|
#[clap(name = "input")]
|
||||||
|
input: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(command: DecryptCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let p = StandardPolicy::new();
|
||||||
|
|
||||||
|
let input = util::open_or_stdin(command.input.as_deref())?;
|
||||||
|
|
||||||
|
let backend = util::open_card(&command.ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
if open.fingerprints()?.decryption().is_none() {
|
||||||
|
return Err(anyhow!("Can't decrypt: this card has no key in the decryption slot.").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_pin = util::get_pin(&mut open, command.pin_file, crate::ENTER_USER_PIN);
|
||||||
|
|
||||||
|
let mut user = util::verify_to_user(&mut open, user_pin.as_deref())?;
|
||||||
|
let d = user.decryptor(&|| println!("Touch confirmation needed for decryption"))?;
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
25
tools/src/bin/opgpcard/commands/factory_reset.rs
Normal file
25
tools/src/bin/opgpcard/commands/factory_reset.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct FactoryResetCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
ident: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn factory_reset(command: FactoryResetCommand) -> Result<()> {
|
||||||
|
println!("Resetting Card {}", command.ident);
|
||||||
|
let card = util::open_card(&command.ident)?;
|
||||||
|
let mut card = Card::new(card);
|
||||||
|
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
open.factory_reset().map_err(|e| anyhow!(e))
|
||||||
|
}
|
89
tools/src/bin/opgpcard/commands/info.rs
Normal file
89
tools/src/bin/opgpcard/commands/info.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
// 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::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
|
||||||
|
use crate::output;
|
||||||
|
use crate::pick_card_for_reading;
|
||||||
|
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct InfoCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
pub ident: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// print metadata information about a card
|
||||||
|
pub fn print_info(
|
||||||
|
format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
command: InfoCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut output = output::Info::default();
|
||||||
|
|
||||||
|
let backend = pick_card_for_reading(command.ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
let ai = open.application_identifier()?;
|
||||||
|
|
||||||
|
output.ident(ai.ident());
|
||||||
|
|
||||||
|
let version = ai.version().to_be_bytes();
|
||||||
|
output.card_version(format!("{}.{}", version[0], version[1]));
|
||||||
|
|
||||||
|
output.application_id(ai.to_string());
|
||||||
|
output.manufacturer_id(format!("{:04X}", ai.manufacturer()));
|
||||||
|
output.manufacturer_name(ai.manufacturer_name().to_string());
|
||||||
|
|
||||||
|
if let Some(cc) = open.historical_bytes()?.card_capabilities() {
|
||||||
|
for line in cc.to_string().lines() {
|
||||||
|
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||||
|
output.card_capability(line.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(csd) = open.historical_bytes()?.card_service_data() {
|
||||||
|
for line in csd.to_string().lines() {
|
||||||
|
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||||
|
output.card_service_data(line.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(eli) = open.extended_length_information()? {
|
||||||
|
for line in eli.to_string().lines() {
|
||||||
|
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||||
|
output.extended_length_info(line.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ec = open.extended_capabilities()?;
|
||||||
|
for line in ec.to_string().lines() {
|
||||||
|
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||||
|
output.extended_capability(line.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Algorithm information (list of supported algorithms)
|
||||||
|
if let Ok(Some(ai)) = open.algorithm_information() {
|
||||||
|
for line in ai.to_string().lines() {
|
||||||
|
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||||
|
output.algorithm(line.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: print KDF info
|
||||||
|
|
||||||
|
// YubiKey specific (?) firmware version
|
||||||
|
if let Ok(ver) = open.firmware_version() {
|
||||||
|
let ver = ver.iter().map(u8::to_string).collect::<Vec<_>>().join(".");
|
||||||
|
output.firmware_version(ver);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
15
tools/src/bin/opgpcard/commands/mod.rs
Normal file
15
tools/src/bin/opgpcard/commands/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
pub mod admin;
|
||||||
|
pub mod attestation;
|
||||||
|
pub mod decrypt;
|
||||||
|
pub mod factory_reset;
|
||||||
|
pub mod info;
|
||||||
|
pub mod pin;
|
||||||
|
pub mod pubkey;
|
||||||
|
pub mod set_identity;
|
||||||
|
pub mod sign;
|
||||||
|
pub mod ssh;
|
||||||
|
pub mod status;
|
298
tools/src/bin/opgpcard/commands/pin.rs
Normal file
298
tools/src/bin/opgpcard/commands/pin.rs
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::{Card, Open};
|
||||||
|
|
||||||
|
use crate::util;
|
||||||
|
use crate::util::{load_pin, print_gnuk_note};
|
||||||
|
use crate::{ENTER_ADMIN_PIN, ENTER_USER_PIN};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct PinCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
pub ident: String,
|
||||||
|
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub cmd: PinSubCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub enum PinSubCommand {
|
||||||
|
/// Set User PIN
|
||||||
|
SetUser {
|
||||||
|
#[clap(name = "User PIN file old", short = 'p', long = "user-pin-old")]
|
||||||
|
user_pin_old: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[clap(name = "User PIN file new", short = 'q', long = "user-pin-new")]
|
||||||
|
user_pin_new: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Set Admin PIN
|
||||||
|
SetAdmin {
|
||||||
|
#[clap(name = "Admin PIN file old", short = 'P', long = "admin-pin-old")]
|
||||||
|
admin_pin_old: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[clap(name = "Admin PIN file new", short = 'Q', long = "admin-pin-new")]
|
||||||
|
admin_pin_new: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Reset User PIN with Admin PIN
|
||||||
|
ResetUser {
|
||||||
|
#[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")]
|
||||||
|
admin_pin: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[clap(name = "User PIN file new", short = 'p', long = "user-pin-new")]
|
||||||
|
user_pin_new: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Set Resetting Code
|
||||||
|
SetReset {
|
||||||
|
#[clap(name = "Admin PIN file", short = 'P', long = "admin-pin")]
|
||||||
|
admin_pin: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[clap(name = "Resetting code file", short = 'r', long = "reset-code")]
|
||||||
|
reset_code: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Reset User PIN with 'Resetting Code'
|
||||||
|
ResetUserRc {
|
||||||
|
#[clap(name = "Resetting Code file", short = 'r', long = "reset-code")]
|
||||||
|
reset_code: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[clap(name = "User PIN file new", short = 'p', long = "user-pin-new")]
|
||||||
|
user_pin_new: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pin(ident: &str, cmd: PinSubCommand) -> Result<()> {
|
||||||
|
let backend = util::open_card(ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let open = card.transaction()?;
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
PinSubCommand::SetUser {
|
||||||
|
user_pin_old,
|
||||||
|
user_pin_new,
|
||||||
|
} => set_user(user_pin_old, user_pin_new, open),
|
||||||
|
|
||||||
|
PinSubCommand::SetAdmin {
|
||||||
|
admin_pin_old,
|
||||||
|
admin_pin_new,
|
||||||
|
} => set_admin(admin_pin_old, admin_pin_new, open),
|
||||||
|
|
||||||
|
PinSubCommand::ResetUser {
|
||||||
|
admin_pin,
|
||||||
|
user_pin_new,
|
||||||
|
} => reset_user(admin_pin, user_pin_new, open),
|
||||||
|
|
||||||
|
PinSubCommand::SetReset {
|
||||||
|
admin_pin,
|
||||||
|
reset_code,
|
||||||
|
} => set_reset(admin_pin, reset_code, open),
|
||||||
|
|
||||||
|
PinSubCommand::ResetUserRc {
|
||||||
|
reset_code,
|
||||||
|
user_pin_new,
|
||||||
|
} => reset_user_rc(reset_code, user_pin_new, open),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_user(
|
||||||
|
user_pin_old: Option<PathBuf>,
|
||||||
|
user_pin_new: Option<PathBuf>,
|
||||||
|
mut open: Open,
|
||||||
|
) -> Result<()> {
|
||||||
|
let pinpad_modify = open.feature_pinpad_modify();
|
||||||
|
|
||||||
|
let res = if !pinpad_modify {
|
||||||
|
// get current user pin
|
||||||
|
let user_pin1 = util::get_pin(&mut open, user_pin_old, ENTER_USER_PIN)
|
||||||
|
.expect("this should never be None");
|
||||||
|
|
||||||
|
// verify pin
|
||||||
|
open.verify_user(&user_pin1)?;
|
||||||
|
println!("PIN was accepted by the card.\n");
|
||||||
|
|
||||||
|
let pin_new = match user_pin_new {
|
||||||
|
None => {
|
||||||
|
// ask user for new user pin
|
||||||
|
util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?
|
||||||
|
}
|
||||||
|
Some(path) => load_pin(&path)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// set new user pin
|
||||||
|
open.change_user_pin(&user_pin1, &pin_new)
|
||||||
|
} else {
|
||||||
|
// set new user pin via pinpad
|
||||||
|
open.change_user_pin_pinpad(&|| {
|
||||||
|
println!("Enter old User PIN on card reader pinpad, then new User PIN (twice).")
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Err(err) => {
|
||||||
|
println!("\nFailed to change the User PIN!");
|
||||||
|
println!("{:?}", err);
|
||||||
|
print_gnuk_note(err, &open)?;
|
||||||
|
}
|
||||||
|
Ok(_) => println!("\nUser PIN has been set."),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_admin(
|
||||||
|
admin_pin_old: Option<PathBuf>,
|
||||||
|
admin_pin_new: Option<PathBuf>,
|
||||||
|
mut open: Open,
|
||||||
|
) -> Result<()> {
|
||||||
|
let pinpad_modify = open.feature_pinpad_modify();
|
||||||
|
|
||||||
|
if !pinpad_modify {
|
||||||
|
// get current admin pin
|
||||||
|
let admin_pin1 = util::get_pin(&mut open, admin_pin_old, ENTER_ADMIN_PIN)
|
||||||
|
.expect("this should never be None");
|
||||||
|
|
||||||
|
// verify pin
|
||||||
|
open.verify_admin(&admin_pin1)?;
|
||||||
|
// (Verifying the PIN here fixes this class of problems:
|
||||||
|
// https://developers.yubico.com/PGP/PGP_PIN_Change_Behavior.html
|
||||||
|
// It is also just generally more user friendly than failing later)
|
||||||
|
println!("PIN was accepted by the card.\n");
|
||||||
|
|
||||||
|
let pin_new = match admin_pin_new {
|
||||||
|
None => {
|
||||||
|
// ask user for new admin pin
|
||||||
|
util::input_pin_twice("Enter new Admin PIN: ", "Repeat the new Admin PIN: ")?
|
||||||
|
}
|
||||||
|
Some(path) => load_pin(&path)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// set new admin pin
|
||||||
|
open.change_admin_pin(&admin_pin1, &pin_new)?;
|
||||||
|
} else {
|
||||||
|
// set new admin pin via pinpad
|
||||||
|
open.change_admin_pin_pinpad(&|| {
|
||||||
|
println!("Enter old Admin PIN on card reader pinpad, then new Admin PIN (twice).")
|
||||||
|
})?;
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("\nAdmin PIN has been set.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_user(
|
||||||
|
admin_pin: Option<PathBuf>,
|
||||||
|
user_pin_new: Option<PathBuf>,
|
||||||
|
mut open: Open,
|
||||||
|
) -> Result<()> {
|
||||||
|
// verify admin pin
|
||||||
|
match util::get_pin(&mut open, admin_pin, ENTER_ADMIN_PIN) {
|
||||||
|
Some(admin_pin) => {
|
||||||
|
// verify pin
|
||||||
|
open.verify_admin(&admin_pin)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
open.verify_admin_pinpad(&|| println!("Enter Admin PIN on pinpad."))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("PIN was accepted by the card.\n");
|
||||||
|
|
||||||
|
// ask user for new user pin
|
||||||
|
let pin = match user_pin_new {
|
||||||
|
None => util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?,
|
||||||
|
Some(path) => load_pin(&path)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = if let Some(mut admin) = open.admin_card() {
|
||||||
|
admin.reset_user_pin(&pin)
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Failed to use card in admin-mode."));
|
||||||
|
};
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Err(err) => {
|
||||||
|
println!("\nFailed to change the User PIN!");
|
||||||
|
print_gnuk_note(err, &open)?;
|
||||||
|
}
|
||||||
|
Ok(_) => println!("\nUser PIN has been set."),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_reset(
|
||||||
|
admin_pin: Option<PathBuf>,
|
||||||
|
reset_code: Option<PathBuf>,
|
||||||
|
mut open: Open,
|
||||||
|
) -> Result<()> {
|
||||||
|
// verify admin pin
|
||||||
|
match util::get_pin(&mut open, admin_pin, ENTER_ADMIN_PIN) {
|
||||||
|
Some(admin_pin) => {
|
||||||
|
// verify pin
|
||||||
|
open.verify_admin(&admin_pin)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
open.verify_admin_pinpad(&|| println!("Enter Admin PIN on pinpad."))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("PIN was accepted by the card.\n");
|
||||||
|
|
||||||
|
// ask user for new resetting code
|
||||||
|
let code = match reset_code {
|
||||||
|
None => util::input_pin_twice(
|
||||||
|
"Enter new resetting code: ",
|
||||||
|
"Repeat the new resetting code: ",
|
||||||
|
)?,
|
||||||
|
Some(path) => load_pin(&path)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut admin) = open.admin_card() {
|
||||||
|
admin.set_resetting_code(&code)?;
|
||||||
|
println!("\nResetting code has been set.");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Failed to use card in admin-mode."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_user_rc(
|
||||||
|
reset_code: Option<PathBuf>,
|
||||||
|
user_pin_new: Option<PathBuf>,
|
||||||
|
mut open: Open,
|
||||||
|
) -> Result<()> {
|
||||||
|
// reset by presenting resetting code
|
||||||
|
|
||||||
|
let rst = if let Some(path) = reset_code {
|
||||||
|
// load resetting code from file
|
||||||
|
load_pin(&path)?
|
||||||
|
} else {
|
||||||
|
// input resetting code
|
||||||
|
rpassword::prompt_password("Enter resetting code: ")?
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
// ask user for new user pin
|
||||||
|
let pin = match user_pin_new {
|
||||||
|
None => util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?,
|
||||||
|
Some(path) => load_pin(&path)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// reset to new user pin
|
||||||
|
match open.reset_user_pin(&rst, &pin) {
|
||||||
|
Err(err) => {
|
||||||
|
println!("\nFailed to change the User PIN!");
|
||||||
|
print_gnuk_note(err, &open)
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
println!("\nUser PIN has been set.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
tools/src/bin/opgpcard/commands/pubkey.rs
Normal file
102
tools/src/bin/opgpcard/commands/pubkey.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// 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::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use sequoia_openpgp::serialize::SerializeInto;
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
use openpgp_card_sequoia::types::KeyType;
|
||||||
|
use openpgp_card_sequoia::util::public_key_material_and_fp_to_key;
|
||||||
|
|
||||||
|
use crate::output;
|
||||||
|
use crate::pick_card_for_reading;
|
||||||
|
use crate::util;
|
||||||
|
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct PubkeyCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
ident: Option<String>,
|
||||||
|
|
||||||
|
#[clap(name = "User PIN file", short = 'p', long = "user-pin")]
|
||||||
|
user_pin: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// User ID to add to the exported certificate representation
|
||||||
|
#[clap(name = "User ID", short = 'u', long = "userid")]
|
||||||
|
user_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_pubkey(
|
||||||
|
format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
command: PubkeyCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut output = output::PublicKey::default();
|
||||||
|
|
||||||
|
let backend = pick_card_for_reading(command.ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
let ident = open.application_identifier()?.ident();
|
||||||
|
output.ident(ident);
|
||||||
|
|
||||||
|
let user_pin = util::get_pin(&mut open, command.user_pin, crate::ENTER_USER_PIN);
|
||||||
|
|
||||||
|
let pkm = open.public_key(KeyType::Signing)?;
|
||||||
|
let times = open.key_generation_times()?;
|
||||||
|
let fps = open.fingerprints()?;
|
||||||
|
|
||||||
|
let key_sig = public_key_material_and_fp_to_key(
|
||||||
|
&pkm,
|
||||||
|
KeyType::Signing,
|
||||||
|
times.signature().expect("Signature time is unset"),
|
||||||
|
fps.signature().expect("Signature fingerprint is unset"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut key_dec = None;
|
||||||
|
if let Ok(pkm) = open.public_key(KeyType::Decryption) {
|
||||||
|
if let Some(ts) = times.decryption() {
|
||||||
|
key_dec = Some(public_key_material_and_fp_to_key(
|
||||||
|
&pkm,
|
||||||
|
KeyType::Decryption,
|
||||||
|
ts,
|
||||||
|
fps.decryption().expect("Decryption fingerprint is unset"),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut key_aut = None;
|
||||||
|
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
||||||
|
if let Some(ts) = times.authentication() {
|
||||||
|
key_aut = Some(public_key_material_and_fp_to_key(
|
||||||
|
&pkm,
|
||||||
|
KeyType::Authentication,
|
||||||
|
ts,
|
||||||
|
fps.authentication()
|
||||||
|
.expect("Authentication fingerprint is unset"),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cert = crate::get_cert(
|
||||||
|
&mut open,
|
||||||
|
key_sig,
|
||||||
|
key_dec,
|
||||||
|
key_aut,
|
||||||
|
user_pin.as_deref(),
|
||||||
|
&command.user_ids,
|
||||||
|
&|| println!("Enter User PIN on card reader pinpad."),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let armored = String::from_utf8(cert.armored().to_vec()?)?;
|
||||||
|
output.public_key(armored);
|
||||||
|
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
48
tools/src/bin/opgpcard/commands/set_identity.rs
Normal file
48
tools/src/bin/opgpcard/commands/set_identity.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct SetIdentityCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
ident: String,
|
||||||
|
|
||||||
|
#[clap(name = "identity", value_enum)]
|
||||||
|
id: SetIdentityId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ValueEnum, Debug, Clone)]
|
||||||
|
pub enum SetIdentityId {
|
||||||
|
#[clap(name = "0")]
|
||||||
|
Zero,
|
||||||
|
#[clap(name = "1")]
|
||||||
|
One,
|
||||||
|
#[clap(name = "2")]
|
||||||
|
Two,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SetIdentityId> for u8 {
|
||||||
|
fn from(id: SetIdentityId) -> Self {
|
||||||
|
match id {
|
||||||
|
SetIdentityId::Zero => 0,
|
||||||
|
SetIdentityId::One => 1,
|
||||||
|
SetIdentityId::Two => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_identity(command: SetIdentityCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let backend = util::open_card(&command.ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
open.set_identity(u8::from(command.id))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
68
tools/src/bin/opgpcard/commands/sign.rs
Normal file
68
tools/src/bin/opgpcard/commands/sign.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||||
|
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use sequoia_openpgp::serialize::stream::{Armorer, Message, Signer};
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct SignCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
pub ident: String,
|
||||||
|
|
||||||
|
/// User PIN file
|
||||||
|
#[clap(short = 'p', long = "user-pin")]
|
||||||
|
pub user_pin: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[clap(name = "detached", short = 'd', long = "detached")]
|
||||||
|
pub detached: bool,
|
||||||
|
|
||||||
|
/// Input file (stdin if unset)
|
||||||
|
#[clap(name = "input")]
|
||||||
|
pub input: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign(command: SignCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if command.detached {
|
||||||
|
sign_detached(&command.ident, command.user_pin, command.input.as_deref())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Only detached signatures are supported for now").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign_detached(
|
||||||
|
ident: &str,
|
||||||
|
pin_file: Option<PathBuf>,
|
||||||
|
input: Option<&Path>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut input = util::open_or_stdin(input)?;
|
||||||
|
|
||||||
|
let backend = util::open_card(ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
if open.fingerprints()?.signature().is_none() {
|
||||||
|
return Err(anyhow!("Can't sign: this card has no key in the signing slot.").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_pin = util::get_pin(&mut open, pin_file, crate::ENTER_USER_PIN);
|
||||||
|
|
||||||
|
let mut sign = util::verify_to_sign(&mut open, user_pin.as_deref())?;
|
||||||
|
let s = sign.signer(&|| println!("Touch confirmation needed for signing"))?;
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
55
tools/src/bin/opgpcard/commands/ssh.rs
Normal file
55
tools/src/bin/opgpcard/commands/ssh.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// 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::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
use openpgp_card_sequoia::types::KeyType;
|
||||||
|
|
||||||
|
use crate::output;
|
||||||
|
use crate::pick_card_for_reading;
|
||||||
|
use crate::util;
|
||||||
|
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct SshCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
pub ident: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_ssh(
|
||||||
|
format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
command: SshCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut output = output::Ssh::default();
|
||||||
|
|
||||||
|
let ident = command.ident;
|
||||||
|
|
||||||
|
let backend = pick_card_for_reading(ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
let ident = open.application_identifier()?.ident();
|
||||||
|
output.ident(ident.clone());
|
||||||
|
|
||||||
|
// Print fingerprint of authentication subkey
|
||||||
|
let fps = open.fingerprints()?;
|
||||||
|
|
||||||
|
if let Some(fp) = fps.authentication() {
|
||||||
|
output.authentication_key_fingerprint(fp.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show authentication subkey as openssh public key string
|
||||||
|
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
||||||
|
if let Ok(ssh) = util::get_ssh_pubkey_string(&pkm, ident) {
|
||||||
|
output.ssh_public_key(ssh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
206
tools/src/bin/opgpcard/commands/status.rs
Normal file
206
tools/src/bin/opgpcard/commands/status.rs
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
// 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::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use openpgp_card_sequoia::card::Card;
|
||||||
|
use openpgp_card_sequoia::types::KeyType;
|
||||||
|
|
||||||
|
use crate::output;
|
||||||
|
use crate::pick_card_for_reading;
|
||||||
|
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct StatusCommand {
|
||||||
|
#[clap(name = "card ident", short = 'c', long = "card")]
|
||||||
|
pub ident: Option<String>,
|
||||||
|
|
||||||
|
#[clap(name = "verbose", short = 'v', long = "verbose")]
|
||||||
|
pub verbose: bool,
|
||||||
|
|
||||||
|
/// Print public key material for each key slot
|
||||||
|
#[clap(name = "pkm", short = 'K', long = "public-key-material")]
|
||||||
|
pub pkm: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_status(
|
||||||
|
format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
command: StatusCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut output = output::Status::default();
|
||||||
|
output.verbose(command.verbose);
|
||||||
|
|
||||||
|
let backend = pick_card_for_reading(command.ident)?;
|
||||||
|
let mut card = Card::new(backend);
|
||||||
|
let mut open = card.transaction()?;
|
||||||
|
|
||||||
|
output.ident(open.application_identifier()?.ident());
|
||||||
|
|
||||||
|
let ai = open.application_identifier()?;
|
||||||
|
let version = ai.version().to_be_bytes();
|
||||||
|
output.card_version(format!("{}.{}", version[0], version[1]));
|
||||||
|
|
||||||
|
// card / cardholder metadata
|
||||||
|
let crd = open.cardholder_related_data()?;
|
||||||
|
|
||||||
|
if let Some(name) = crd.name() {
|
||||||
|
// FIXME: decoding as utf8 is wrong (the spec defines this field as latin1 encoded)
|
||||||
|
let name = String::from_utf8_lossy(name).to_string();
|
||||||
|
|
||||||
|
// // 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(" ");
|
||||||
|
|
||||||
|
output.card_holder(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = open.url()?;
|
||||||
|
if !url.is_empty() {
|
||||||
|
output.url(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(lang) = crd.lang() {
|
||||||
|
for lang in lang {
|
||||||
|
output.language_preference(format!("{}", lang));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// key information (imported vs. generated on card)
|
||||||
|
let ki = open.key_information().ok().flatten();
|
||||||
|
|
||||||
|
let pws = open.pw_status_bytes()?;
|
||||||
|
|
||||||
|
// information about subkeys
|
||||||
|
|
||||||
|
let fps = open.fingerprints()?;
|
||||||
|
let kgt = open.key_generation_times()?;
|
||||||
|
|
||||||
|
let mut signature_key = output::KeySlotInfo::default();
|
||||||
|
if let Some(fp) = fps.signature() {
|
||||||
|
signature_key.fingerprint(fp.to_spaced_hex());
|
||||||
|
}
|
||||||
|
signature_key.algorithm(format!("{}", open.algorithm_attributes(KeyType::Signing)?));
|
||||||
|
if let Some(kgt) = kgt.signature() {
|
||||||
|
signature_key.created(format!("{}", kgt.to_datetime()));
|
||||||
|
}
|
||||||
|
if let Some(uif) = open.uif_signing()? {
|
||||||
|
signature_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||||
|
signature_key.touch_features(format!("{}", uif.features()));
|
||||||
|
}
|
||||||
|
if let Some(ks) = ki.as_ref().map(|ki| ki.sig_status()) {
|
||||||
|
signature_key.status(format!("{}", ks));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pws.pw1_cds_valid_once() {
|
||||||
|
signature_key.pin_valid_once();
|
||||||
|
}
|
||||||
|
|
||||||
|
if command.pkm {
|
||||||
|
if let Ok(pkm) = open.public_key(KeyType::Signing) {
|
||||||
|
signature_key.public_key_material(pkm.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.signature_key(signature_key);
|
||||||
|
|
||||||
|
let sst = open.security_support_template()?;
|
||||||
|
output.signature_count(sst.signature_count());
|
||||||
|
|
||||||
|
let mut decryption_key = output::KeySlotInfo::default();
|
||||||
|
if let Some(fp) = fps.decryption() {
|
||||||
|
decryption_key.fingerprint(fp.to_spaced_hex());
|
||||||
|
}
|
||||||
|
decryption_key.algorithm(format!(
|
||||||
|
"{}",
|
||||||
|
open.algorithm_attributes(KeyType::Decryption)?
|
||||||
|
));
|
||||||
|
if let Some(kgt) = kgt.decryption() {
|
||||||
|
decryption_key.created(format!("{}", kgt.to_datetime()));
|
||||||
|
}
|
||||||
|
if let Some(uif) = open.uif_decryption()? {
|
||||||
|
decryption_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||||
|
decryption_key.touch_features(format!("{}", uif.features()));
|
||||||
|
}
|
||||||
|
if let Some(ks) = ki.as_ref().map(|ki| ki.dec_status()) {
|
||||||
|
decryption_key.status(format!("{}", ks));
|
||||||
|
}
|
||||||
|
if command.pkm {
|
||||||
|
if let Ok(pkm) = open.public_key(KeyType::Decryption) {
|
||||||
|
decryption_key.public_key_material(pkm.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.decryption_key(decryption_key);
|
||||||
|
|
||||||
|
let mut authentication_key = output::KeySlotInfo::default();
|
||||||
|
if let Some(fp) = fps.authentication() {
|
||||||
|
authentication_key.fingerprint(fp.to_spaced_hex());
|
||||||
|
}
|
||||||
|
authentication_key.algorithm(format!(
|
||||||
|
"{}",
|
||||||
|
open.algorithm_attributes(KeyType::Authentication)?
|
||||||
|
));
|
||||||
|
if let Some(kgt) = kgt.authentication() {
|
||||||
|
authentication_key.created(format!("{}", kgt.to_datetime()));
|
||||||
|
}
|
||||||
|
if let Some(uif) = open.uif_authentication()? {
|
||||||
|
authentication_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||||
|
authentication_key.touch_features(format!("{}", uif.features()));
|
||||||
|
}
|
||||||
|
if let Some(ks) = ki.as_ref().map(|ki| ki.aut_status()) {
|
||||||
|
authentication_key.status(format!("{}", ks));
|
||||||
|
}
|
||||||
|
if command.pkm {
|
||||||
|
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
||||||
|
authentication_key.public_key_material(pkm.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.authentication_key(authentication_key);
|
||||||
|
|
||||||
|
// technical details about the card's state
|
||||||
|
|
||||||
|
output.user_pin_remaining_attempts(pws.err_count_pw1());
|
||||||
|
output.admin_pin_remaining_attempts(pws.err_count_pw3());
|
||||||
|
output.reset_code_remaining_attempts(pws.err_count_rc());
|
||||||
|
|
||||||
|
// FIXME: Handle attestation key information as a separate
|
||||||
|
// KeySlotInfo! Attestation touch information should go into its
|
||||||
|
// own `Option<KeySlotInfo>`, and (if any information about the
|
||||||
|
// attestation key exists at all, which is not the case for most
|
||||||
|
// cards) it should be printed as a fourth KeySlot block.
|
||||||
|
if let Some(uif) = open.uif_attestation()? {
|
||||||
|
output.card_touch_policy(uif.touch_policy().to_string());
|
||||||
|
output.card_touch_features(uif.features().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ki) = ki {
|
||||||
|
let num = ki.num_additional();
|
||||||
|
for i in 0..num {
|
||||||
|
output.key_status(ki.additional_ref(i), ki.additional_status(i).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(fps) = open.ca_fingerprints() {
|
||||||
|
for fp in fps.iter().flatten() {
|
||||||
|
output.ca_fingerprint(fp.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: print "Login Data"
|
||||||
|
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue