Move backend traits to new card-backend crate
- Move CardBackend, CardTransation traits to card-backend - Break SmartcardErrors out from openpgp-card to card-backend - CardCaps are (mostly) openpgp-card specific, move them (mostly) to openpgp-card - Rename pcsc and scdc backend crates: card-backend-pcsc, card-backend-scdc
This commit is contained in:
parent
a2232ebf86
commit
0e89c4baa6
19 changed files with 710 additions and 773 deletions
|
@ -1,12 +1,13 @@
|
|||
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"openpgp-card",
|
||||
"openpgp-card-sequoia",
|
||||
# "openpgp-card-sequoia",
|
||||
"card-backend",
|
||||
"pcsc",
|
||||
"scdc",
|
||||
"openpgp-card-examples",
|
||||
"card-functionality",
|
||||
# "openpgp-card-examples",
|
||||
# "card-functionality",
|
||||
]
|
||||
|
|
15
card-backend/Cargo.toml
Normal file
15
card-backend/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# SPDX-FileCopyrightText: 2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "card-backend"
|
||||
description = "Card backend trait, for use with the openpgp-card crate"
|
||||
authors = ["Heiko Schaefer <heiko@schaefer.name>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
repository = "https://gitlab.com/openpgp-card/openpgp-card"
|
||||
documentation = "https://docs.rs/crate/card-backend"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
12
card-backend/README.md
Normal file
12
card-backend/README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
# Backend trait for Smart Card crates
|
||||
|
||||
This crate defines the `CardBackend` and `CardTransactions` traits.
|
||||
|
||||
The initial target for this abstraction layer was the
|
||||
[openpgp-card](https://gitlab.com/openpgp-card/openpgp-card) set of client libraries
|
||||
for OpenPGP card. This trait offers an implementation-agnostic means to access cards.
|
192
card-backend/src/lib.rs
Normal file
192
card-backend/src/lib.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! A thin abstraction layer for accessing smart cards, including, but not
|
||||
//! limited to, [openpgp-card](https://gitlab.com/openpgp-card/openpgp-card)
|
||||
//! devices.
|
||||
|
||||
/// This trait defines a connection with a smart card via a
|
||||
/// backend implementation (e.g. via the pcsc backend in the crate
|
||||
/// [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc)).
|
||||
///
|
||||
/// A [CardBackend] is only used to get access to a [CardTransaction] object,
|
||||
/// which supports transmitting commands to the card.
|
||||
pub trait CardBackend {
|
||||
fn transaction(
|
||||
&mut self,
|
||||
reselect_application: Option<&[u8]>,
|
||||
) -> Result<Box<dyn CardTransaction + Send + Sync + '_>, SmartcardError>;
|
||||
}
|
||||
|
||||
/// The CardTransaction trait defines communication with a smart card via a
|
||||
/// backend implementation (e.g. the pcsc backend in the crate
|
||||
/// [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc)),
|
||||
/// after opening a transaction from a CardBackend.
|
||||
pub trait CardTransaction {
|
||||
/// Transmit the command data in `cmd` to the card.
|
||||
///
|
||||
/// `buf_size` is a hint to the backend (the backend may ignore it)
|
||||
/// indicating the expected maximum response size.
|
||||
fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result<Vec<u8>, SmartcardError>;
|
||||
|
||||
/// Select `application` on the card
|
||||
fn select(&mut self, application: &[u8]) -> Result<Vec<u8>, SmartcardError> {
|
||||
let mut cmd = vec![0x00, 0xa4, 0x04, 0x00]; // CLA, INS, P1, P2
|
||||
cmd.push(application.len() as u8); // Lc
|
||||
cmd.extend_from_slice(application); // Data
|
||||
cmd.push(0x00); // Le
|
||||
|
||||
self.transmit(&cmd, 254)
|
||||
}
|
||||
|
||||
/// If a CardTransaction implementation introduces an additional,
|
||||
/// backend-specific limit for maximum number of bytes per command,
|
||||
/// this fn can indicate that limit by returning `Some(max_cmd_len)`.
|
||||
fn max_cmd_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Does the reader support FEATURE_VERIFY_PIN_DIRECT?
|
||||
fn feature_pinpad_verify(&self) -> bool;
|
||||
|
||||
/// Does the reader support FEATURE_MODIFY_PIN_DIRECT?
|
||||
fn feature_pinpad_modify(&self) -> bool;
|
||||
|
||||
/// Verify the PIN `pin` via the reader pinpad
|
||||
fn pinpad_verify(
|
||||
&mut self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError>;
|
||||
|
||||
/// Modify the PIN `pin` via the reader pinpad
|
||||
fn pinpad_modify(
|
||||
&mut self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError>;
|
||||
}
|
||||
|
||||
/// Information about the capabilities of a card.
|
||||
///
|
||||
/// CardCaps is used to signal capabilities (chaining, extended length support, max
|
||||
/// command/response sizes, max PIN lengths) of the current card to backends.
|
||||
///
|
||||
/// CardCaps is not intended for users of this library.
|
||||
///
|
||||
/// (The information is gathered from the "Card Capabilities", "Extended length information" and
|
||||
/// "PWStatus" DOs)
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CardCaps {
|
||||
ext_support: bool,
|
||||
chaining_support: bool,
|
||||
max_cmd_bytes: u16,
|
||||
max_rsp_bytes: u16,
|
||||
pw1_max_len: u8,
|
||||
pw3_max_len: u8,
|
||||
}
|
||||
|
||||
impl CardCaps {
|
||||
pub fn new(
|
||||
ext_support: bool,
|
||||
chaining_support: bool,
|
||||
max_cmd_bytes: u16,
|
||||
max_rsp_bytes: u16,
|
||||
pw1_max_len: u8,
|
||||
pw3_max_len: u8,
|
||||
) -> Self {
|
||||
Self {
|
||||
ext_support,
|
||||
chaining_support,
|
||||
max_cmd_bytes,
|
||||
max_rsp_bytes,
|
||||
pw1_max_len,
|
||||
pw3_max_len,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the card support extended Lc and Le fields?
|
||||
pub fn ext_support(&self) -> bool {
|
||||
self.ext_support
|
||||
}
|
||||
|
||||
/// Does the card support command chaining?
|
||||
pub fn chaining_support(&self) -> bool {
|
||||
self.chaining_support
|
||||
}
|
||||
|
||||
/// Maximum number of bytes in a command APDU
|
||||
pub fn max_cmd_bytes(&self) -> u16 {
|
||||
self.max_cmd_bytes
|
||||
}
|
||||
|
||||
/// Maximum number of bytes in a response APDU
|
||||
pub fn max_rsp_bytes(&self) -> u16 {
|
||||
self.max_rsp_bytes
|
||||
}
|
||||
|
||||
/// Maximum length of PW1
|
||||
pub fn pw1_max_len(&self) -> u8 {
|
||||
self.pw1_max_len
|
||||
}
|
||||
|
||||
/// Maximum length of PW3
|
||||
pub fn pw3_max_len(&self) -> u8 {
|
||||
self.pw3_max_len
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify a PIN to *verify* (distinguishes between `Sign`, `User` and `Admin`).
|
||||
///
|
||||
/// (Note that for PIN *management*, in particular changing a PIN, "signing and user" are
|
||||
/// not distinguished. They always share the same PIN value `PW1`)
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum PinType {
|
||||
/// Verify PW1 in mode P2=81 (for the PSO:CDS operation)
|
||||
Sign,
|
||||
|
||||
/// Verify PW1 in mode P2=82 (for all other User operations)
|
||||
User,
|
||||
|
||||
/// Verify PW3 (for Admin operations)
|
||||
Admin,
|
||||
}
|
||||
|
||||
impl PinType {
|
||||
pub fn id(&self) -> u8 {
|
||||
match self {
|
||||
PinType::Sign => 0x81,
|
||||
PinType::User => 0x82,
|
||||
PinType::Admin => 0x83,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors on the smartcard/reader layer
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum SmartcardError {
|
||||
#[error("Failed to create a pcsc smartcard context {0}")]
|
||||
ContextError(String),
|
||||
|
||||
#[error("Failed to list readers: {0}")]
|
||||
ReaderError(String),
|
||||
|
||||
#[error("No reader found.")]
|
||||
NoReaderFoundError,
|
||||
|
||||
#[error("The requested card '{0}' was not found.")]
|
||||
CardNotFound(String),
|
||||
|
||||
#[error("Failed to connect to the card: {0}")]
|
||||
SmartCardConnectionError(String),
|
||||
|
||||
#[error("Smart card status: [{0}, {1}]")]
|
||||
SmartCardStatus(u8, u8),
|
||||
|
||||
#[error("NotTransacted (SCARD_E_NOT_TRANSACTED)")]
|
||||
NotTransacted,
|
||||
|
||||
#[error("Generic SmartCard Error: {0}")]
|
||||
Error(String),
|
||||
}
|
|
@ -13,7 +13,7 @@ documentation = "https://docs.rs/crate/openpgp-card-sequoia"
|
|||
|
||||
[dependencies]
|
||||
sequoia-openpgp = { version = "1.4", default-features = false }
|
||||
openpgp-card = { path = "../openpgp-card", version = "0.3.7" }
|
||||
openpgp-card = { path = "../openpgp-card", version = "0.4" }
|
||||
chrono = "0.4"
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
name = "openpgp-card"
|
||||
description = "A client implementation for the OpenPGP card specification"
|
||||
license = "MIT OR Apache-2.0"
|
||||
version = "0.3.7"
|
||||
version = "0.4.0"
|
||||
authors = ["Heiko Schaefer <heiko@schaefer.name>"]
|
||||
edition = "2018"
|
||||
repository = "https://gitlab.com/openpgp-card/openpgp-card"
|
||||
documentation = "https://docs.rs/crate/openpgp-card"
|
||||
|
||||
[dependencies]
|
||||
blanket = "0.3"
|
||||
card-backend = { path = "../card-backend", version = "0.1" }
|
||||
nom = "7"
|
||||
hex-slice = "0.1"
|
||||
thiserror = "1"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
|
@ -19,8 +19,8 @@ specification.
|
|||
This crate doesn't contain code to talk to cards. Implementations of the traits
|
||||
`CardBackend`/`CardTransaction` need to be provided for access to cards.
|
||||
|
||||
The crates [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc)
|
||||
and the experimental crate [openpgp-card-scdc](https://crates.io/crates/openpgp-card-scdc)
|
||||
The crates [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc)
|
||||
and the experimental crate [card-backend-scdc](https://crates.io/crates/card-backend-scdc)
|
||||
provide implementations of these traits for use with this crate.
|
||||
|
||||
**Sequoia PGP wrapper**
|
||||
|
|
|
@ -10,9 +10,11 @@ pub mod response;
|
|||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use card_backend::{CardCaps, CardTransaction};
|
||||
|
||||
use crate::apdu::command::Expect;
|
||||
use crate::apdu::{command::Command, response::RawResponse};
|
||||
use crate::{CardTransaction, Error, StatusBytes};
|
||||
use crate::{Error, StatusBytes};
|
||||
|
||||
/// "Maximum amount of bytes in a short APDU command or response" (from pcsc)
|
||||
const MAX_BUFFER_SIZE: usize = 264;
|
||||
|
@ -24,6 +26,7 @@ const MAX_BUFFER_SIZE: usize = 264;
|
|||
pub(crate) fn send_command<C>(
|
||||
card_tx: &mut C,
|
||||
cmd: Command,
|
||||
card_caps: Option<CardCaps>,
|
||||
expect_reply: bool,
|
||||
) -> Result<RawResponse, Error>
|
||||
where
|
||||
|
@ -34,6 +37,7 @@ where
|
|||
let mut resp = RawResponse::try_from(send_command_low_level(
|
||||
card_tx,
|
||||
cmd.clone(),
|
||||
card_caps,
|
||||
if expect_reply {
|
||||
Expect::Some
|
||||
} else {
|
||||
|
@ -42,7 +46,12 @@ where
|
|||
)?)?;
|
||||
|
||||
if let StatusBytes::UnknownStatus(0x6c, size) = resp.status() {
|
||||
resp = RawResponse::try_from(send_command_low_level(card_tx, cmd, Expect::Short(size))?)?;
|
||||
resp = RawResponse::try_from(send_command_low_level(
|
||||
card_tx,
|
||||
cmd,
|
||||
card_caps,
|
||||
Expect::Short(size),
|
||||
)?)?;
|
||||
}
|
||||
|
||||
while let StatusBytes::OkBytesAvailable(bytes) = resp.status() {
|
||||
|
@ -53,6 +62,7 @@ where
|
|||
let next = RawResponse::try_from(send_command_low_level(
|
||||
card_tx,
|
||||
commands::get_response(),
|
||||
card_caps,
|
||||
Expect::Short(bytes),
|
||||
)?)?;
|
||||
|
||||
|
@ -85,20 +95,21 @@ where
|
|||
fn send_command_low_level<C>(
|
||||
card_tx: &mut C,
|
||||
cmd: Command,
|
||||
card_caps: Option<CardCaps>,
|
||||
expect_response: Expect,
|
||||
) -> Result<Vec<u8>, Error>
|
||||
where
|
||||
C: CardTransaction + ?Sized,
|
||||
{
|
||||
let (ext_support, chaining_support, mut max_cmd_bytes, max_rsp_bytes) =
|
||||
if let Some(caps) = card_tx.card_caps() {
|
||||
if let Some(caps) = card_caps {
|
||||
log::trace!("found card caps data!");
|
||||
|
||||
(
|
||||
caps.ext_support,
|
||||
caps.chaining_support,
|
||||
caps.max_cmd_bytes as usize,
|
||||
caps.max_rsp_bytes as usize,
|
||||
caps.ext_support(),
|
||||
caps.chaining_support(),
|
||||
caps.max_cmd_bytes() as usize,
|
||||
caps.max_rsp_bytes() as usize,
|
||||
)
|
||||
} else {
|
||||
log::trace!("found NO card caps data!");
|
||||
|
|
|
@ -4,18 +4,12 @@
|
|||
//! Pre-defined `Command` values for the OpenPGP card application
|
||||
|
||||
use crate::apdu::command::Command;
|
||||
use crate::{KeyType, ShortTag, Tags};
|
||||
use crate::{KeyType, ShortTag, Tags, OP_APP};
|
||||
|
||||
/// 7.2.1 SELECT
|
||||
/// (select the OpenPGP application on the card)
|
||||
pub(crate) fn select_openpgp() -> Command {
|
||||
Command::new(
|
||||
0x00,
|
||||
0xA4,
|
||||
0x04,
|
||||
0x00,
|
||||
vec![0xD2, 0x76, 0x00, 0x01, 0x24, 0x01],
|
||||
)
|
||||
Command::new(0x00, 0xA4, 0x04, 0x00, OP_APP.to_vec())
|
||||
}
|
||||
|
||||
/// 7.2.6 GET DATA
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
//! - [`StatusBytes`], which models error statuses reported by the OpenPGP
|
||||
//! card application
|
||||
|
||||
use card_backend::SmartcardError;
|
||||
|
||||
/// Enum wrapper for the different error types of this crate
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
|
@ -49,6 +51,12 @@ impl From<StatusBytes> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<SmartcardError> for Error {
|
||||
fn from(sce: SmartcardError) -> Self {
|
||||
Error::Smartcard(sce)
|
||||
}
|
||||
}
|
||||
|
||||
/// OpenPGP card "Status Bytes" (ok statuses and errors)
|
||||
#[derive(thiserror::Error, Debug, PartialEq, Eq, Copy, Clone)]
|
||||
#[non_exhaustive]
|
||||
|
@ -161,32 +169,3 @@ impl From<(u8, u8)> for StatusBytes {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors on the smartcard/reader layer
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum SmartcardError {
|
||||
#[error("Failed to create a pcsc smartcard context {0}")]
|
||||
ContextError(String),
|
||||
|
||||
#[error("Failed to list readers: {0}")]
|
||||
ReaderError(String),
|
||||
|
||||
#[error("No reader found.")]
|
||||
NoReaderFoundError,
|
||||
|
||||
#[error("The requested card '{0}' was not found.")]
|
||||
CardNotFound(String),
|
||||
|
||||
#[error("Couldn't select the OpenPGP card application")]
|
||||
SelectOpenPGPCardFailed,
|
||||
|
||||
#[error("Failed to connect to the card: {0}")]
|
||||
SmartCardConnectionError(String),
|
||||
|
||||
#[error("NotTransacted (SCARD_E_NOT_TRANSACTED)")]
|
||||
NotTransacted,
|
||||
|
||||
#[error("Generic SmartCard Error: {0}")]
|
||||
Error(String),
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ use crate::crypto_data::{
|
|||
};
|
||||
use crate::openpgp::OpenPgpTransaction;
|
||||
use crate::tlv::{length::tlv_encode_length, value::Value, Tlv};
|
||||
use crate::{apdu, Error, KeyType, Tag, Tags};
|
||||
use crate::{Error, KeyType, Tag, Tags};
|
||||
|
||||
/// Generate asymmetric key pair on the card.
|
||||
///
|
||||
|
@ -130,7 +130,7 @@ pub(crate) fn generate_asymmetric_key_pair(
|
|||
let crt = control_reference_template(key_type)?;
|
||||
let gen_key_cmd = commands::gen_key(crt.serialize().to_vec());
|
||||
|
||||
let resp = apdu::send_command(card_tx.tx(), gen_key_cmd, true)?;
|
||||
let resp = card_tx.send_command(gen_key_cmd, true)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
let tlv = Tlv::try_from(resp.data()?)?;
|
||||
|
@ -158,7 +158,7 @@ pub(crate) fn public_key(
|
|||
let crt = control_reference_template(key_type)?;
|
||||
let get_pub_key_cmd = commands::get_pub_key(crt.serialize().to_vec());
|
||||
|
||||
let resp = apdu::send_command(card_tx.tx(), get_pub_key_cmd, true)?;
|
||||
let resp = card_tx.send_command(get_pub_key_cmd, true)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
let tlv = Tlv::try_from(resp.data()?)?;
|
||||
|
@ -215,7 +215,7 @@ pub(crate) fn key_import(
|
|||
card_tx.set_algorithm_attributes(key_type, &algo)?;
|
||||
}
|
||||
|
||||
apdu::send_command(card_tx.tx(), key_cmd, false)?.check_ok()?;
|
||||
card_tx.send_command(key_cmd, false)?.check_ok()?;
|
||||
card_tx.set_fingerprint(fp, key_type)?;
|
||||
card_tx.set_creation_time(key.timestamp(), key_type)?;
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
//! [OpenPGP implementation](https://www.openpgp.org/software/developer/).
|
||||
//!
|
||||
//! This library can't directly access cards by itself. Instead, users
|
||||
//! need to supply a backend that implements the [`CardBackend`]
|
||||
//! / [`CardTransaction`] traits. The companion crate
|
||||
//! [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc)
|
||||
//! need to supply a backend that implements the [`card_backend::CardBackend`]
|
||||
//! / [`card_backend::CardTransaction`] traits. The companion crate
|
||||
//! [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc)
|
||||
//! offers a backend that uses [PC/SC](https://en.wikipedia.org/wiki/PC/SC) to
|
||||
//! communicate with Smart Cards.
|
||||
//!
|
||||
|
@ -38,211 +38,11 @@ mod oid;
|
|||
mod openpgp;
|
||||
mod tlv;
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::apdu::commands;
|
||||
use crate::card_do::ApplicationRelatedData;
|
||||
pub use crate::errors::{Error, SmartcardError, StatusBytes};
|
||||
pub use crate::errors::{Error, StatusBytes};
|
||||
pub use crate::openpgp::{OpenPgp, OpenPgpTransaction};
|
||||
use crate::tlv::{tag::Tag, value::Value, Tlv};
|
||||
use crate::tlv::tag::Tag;
|
||||
|
||||
/// The CardBackend trait defines a connection with an OpenPGP card via a
|
||||
/// backend implementation (e.g. via the pcsc backend in the crate
|
||||
/// [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc)),
|
||||
/// A CardBackend is only used to get access to a `CardTransaction` object.
|
||||
#[blanket::blanket(derive(Box))]
|
||||
pub trait CardBackend {
|
||||
fn transaction(&mut self) -> Result<Box<dyn CardTransaction + Send + Sync + '_>, Error>;
|
||||
}
|
||||
|
||||
/// The CardTransaction trait defines communication with an OpenPGP card via a
|
||||
/// backend implementation (e.g. the pcsc backend in the crate
|
||||
/// [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc)),
|
||||
/// after opening a transaction from a CardBackend.
|
||||
#[blanket::blanket(derive(Box))]
|
||||
pub trait CardTransaction {
|
||||
/// Transmit the command data in `cmd` to the card.
|
||||
///
|
||||
/// `buf_size` is a hint to the backend (the backend may ignore it)
|
||||
/// indicating the expected maximum response size.
|
||||
fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result<Vec<u8>, Error>;
|
||||
|
||||
/// Set the card capabilities in the CardTransaction.
|
||||
///
|
||||
/// Setting these capabilities is typically part of a bootstrapping
|
||||
/// process (this fn is typically called from [CardTransaction::initialize].
|
||||
/// When implementing CardTransaction, you probably want to call
|
||||
/// [CardTransaction::initialize] during setup).
|
||||
///
|
||||
/// The information about the card's capabilities is typically
|
||||
/// requested from the card using the same CardTransaction instance,
|
||||
/// before the card's capabilities have been initialized.
|
||||
fn init_card_caps(&mut self, caps: CardCaps);
|
||||
|
||||
/// Request the card's capabilities
|
||||
///
|
||||
/// (apdu serialization makes use of this information, e.g. to
|
||||
/// determine if extended length can be used)
|
||||
fn card_caps(&self) -> Option<&CardCaps>;
|
||||
|
||||
/// If a CardTransaction implementation introduces an additional,
|
||||
/// backend-specific limit for maximum number of bytes per command,
|
||||
/// this fn can indicate that limit by returning `Some(max_cmd_len)`.
|
||||
fn max_cmd_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Does the reader support FEATURE_VERIFY_PIN_DIRECT?
|
||||
fn feature_pinpad_verify(&self) -> bool;
|
||||
|
||||
/// Does the reader support FEATURE_MODIFY_PIN_DIRECT?
|
||||
fn feature_pinpad_modify(&self) -> bool;
|
||||
|
||||
/// Verify the PIN `id` via the reader pinpad
|
||||
fn pinpad_verify(&mut self, pin: PinType) -> Result<Vec<u8>, Error>;
|
||||
|
||||
/// Modify the PIN `id` via the reader pinpad
|
||||
fn pinpad_modify(&mut self, pin: PinType) -> Result<Vec<u8>, Error>;
|
||||
|
||||
/// Select the OpenPGP card application
|
||||
fn select(&mut self) -> Result<Vec<u8>, Error> {
|
||||
log::info!("CardTransaction: select");
|
||||
let select_openpgp = commands::select_openpgp();
|
||||
apdu::send_command(self, select_openpgp, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Activate file
|
||||
fn activate_file(&mut self) -> Result<Vec<u8>, Error> {
|
||||
log::info!("CardTransaction: activate_file");
|
||||
let activate_file = commands::activate_file();
|
||||
apdu::send_command(self, activate_file, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Get the "application related data" from the card.
|
||||
///
|
||||
/// (This data should probably be cached in a higher layer. Some parts of
|
||||
/// it are needed regularly, and it does not usually change during
|
||||
/// normal use of a card.)
|
||||
fn application_related_data(&mut self) -> Result<ApplicationRelatedData, Error> {
|
||||
let ad = commands::application_related_data();
|
||||
let resp = apdu::send_command(self, ad, true)?;
|
||||
let value = Value::from(resp.data()?, true)?;
|
||||
|
||||
log::trace!(" ARD value: {:02x?}", value);
|
||||
|
||||
Ok(ApplicationRelatedData(Tlv::new(
|
||||
Tags::ApplicationRelatedData,
|
||||
value,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Get a CardApp based on a CardTransaction.
|
||||
///
|
||||
/// It is expected that SELECT has already been performed on the card
|
||||
/// beforehand.
|
||||
///
|
||||
/// This fn initializes the CardCaps by requesting
|
||||
/// application_related_data from the card, and setting the
|
||||
/// capabilities accordingly.
|
||||
fn initialize(&mut self) -> Result<(), Error> {
|
||||
let ard = self.application_related_data()?;
|
||||
|
||||
// Determine chaining/extended length support from card
|
||||
// metadata and cache this information in the CardTransaction
|
||||
// implementation (as a CardCaps)
|
||||
let mut ext_support = false;
|
||||
let mut chaining_support = false;
|
||||
|
||||
if let Ok(hist) = ard.historical_bytes() {
|
||||
if let Some(cc) = hist.card_capabilities() {
|
||||
chaining_support = cc.command_chaining();
|
||||
ext_support = cc.extended_lc_le();
|
||||
}
|
||||
}
|
||||
|
||||
let ext_cap = ard.extended_capabilities()?;
|
||||
|
||||
// Get max command/response byte sizes from card
|
||||
let (max_cmd_bytes, max_rsp_bytes) =
|
||||
if let Ok(Some(eli)) = ard.extended_length_information() {
|
||||
// In card 3.x, max lengths come from ExtendedLengthInfo
|
||||
(eli.max_command_bytes(), eli.max_response_bytes())
|
||||
} else if let (Some(cmd), Some(rsp)) = (ext_cap.max_cmd_len(), ext_cap.max_resp_len()) {
|
||||
// In card 2.x, max lengths come from ExtendedCapabilities
|
||||
(cmd, rsp)
|
||||
} else {
|
||||
// Fallback: use 255 if we have no information from the card
|
||||
(255, 255)
|
||||
};
|
||||
|
||||
let pw_status = ard.pw_status_bytes()?;
|
||||
let pw1_max = pw_status.pw1_max_len();
|
||||
let pw3_max = pw_status.pw3_max_len();
|
||||
|
||||
let caps = CardCaps {
|
||||
ext_support,
|
||||
chaining_support,
|
||||
max_cmd_bytes,
|
||||
max_rsp_bytes,
|
||||
pw1_max_len: pw1_max,
|
||||
pw3_max_len: pw3_max,
|
||||
};
|
||||
|
||||
log::trace!("init_card_caps to: {:x?}", caps);
|
||||
|
||||
self.init_card_caps(caps);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the capabilities of a card.
|
||||
///
|
||||
/// CardCaps is used to signal capabilities (chaining, extended length support, max
|
||||
/// command/response sizes, max PIN lengths) of the current card to backends.
|
||||
///
|
||||
/// CardCaps is not intended for users of this library.
|
||||
///
|
||||
/// (The information is gathered from the "Card Capabilities", "Extended length information" and
|
||||
/// "PWStatus" DOs)
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CardCaps {
|
||||
/// Does the card support extended Lc and Le fields?
|
||||
ext_support: bool,
|
||||
|
||||
/// Command chaining support?
|
||||
chaining_support: bool,
|
||||
|
||||
/// Maximum number of bytes in a command APDU
|
||||
max_cmd_bytes: u16,
|
||||
|
||||
/// Maximum number of bytes in a response APDU
|
||||
max_rsp_bytes: u16,
|
||||
|
||||
/// Maximum length of PW1
|
||||
pw1_max_len: u8,
|
||||
|
||||
/// Maximum length of PW3
|
||||
pw3_max_len: u8,
|
||||
}
|
||||
|
||||
impl CardCaps {
|
||||
pub fn ext_support(&self) -> bool {
|
||||
self.ext_support
|
||||
}
|
||||
|
||||
pub fn max_rsp_bytes(&self) -> u16 {
|
||||
self.max_rsp_bytes
|
||||
}
|
||||
|
||||
pub fn pw1_max_len(&self) -> u8 {
|
||||
self.pw1_max_len
|
||||
}
|
||||
|
||||
pub fn pw3_max_len(&self) -> u8 {
|
||||
self.pw3_max_len
|
||||
}
|
||||
}
|
||||
pub(crate) const OP_APP: &[u8] = &[0xD2, 0x76, 0x00, 0x01, 0x24, 0x01];
|
||||
|
||||
/// Tags, as specified and used in the OpenPGP card 3.4.1 spec.
|
||||
/// All tags in OpenPGP card are either 1 or 2 bytes long.
|
||||
|
@ -497,32 +297,6 @@ impl From<ShortTag> for Vec<u8> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Specify a PIN to *verify* (distinguishes between `Sign`, `User` and `Admin`).
|
||||
///
|
||||
/// (Note that for PIN *management*, in particular changing a PIN, "signing and user" are
|
||||
/// not distinguished. They always share the same PIN value `PW1`)
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum PinType {
|
||||
/// Verify PW1 in mode P2=81 (for the PSO:CDS operation)
|
||||
Sign,
|
||||
|
||||
/// Verify PW1 in mode P2=82 (for all other User operations)
|
||||
User,
|
||||
|
||||
/// Verify PW3 (for Admin operations)
|
||||
Admin,
|
||||
}
|
||||
|
||||
impl PinType {
|
||||
pub fn id(&self) -> u8 {
|
||||
match self {
|
||||
PinType::Sign => 0x81,
|
||||
PinType::User => 0x82,
|
||||
PinType::Admin => 0x83,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Identify a Key slot on an OpenPGP card
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[non_exhaustive]
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
use card_backend::{CardBackend, CardCaps, CardTransaction, PinType, SmartcardError};
|
||||
|
||||
use crate::algorithm::{Algo, AlgoInfo, AlgoSimple};
|
||||
use crate::apdu::command::Command;
|
||||
use crate::apdu::commands;
|
||||
use crate::apdu::response::RawResponse;
|
||||
use crate::card_do::{
|
||||
|
@ -12,29 +15,93 @@ use crate::card_do::{
|
|||
};
|
||||
use crate::crypto_data::{CardUploadableKey, Cryptogram, Hash, PublicKeyMaterial};
|
||||
use crate::tlv::{value::Value, Tlv};
|
||||
use crate::{
|
||||
apdu, keys, CardBackend, CardTransaction, Error, KeyType, PinType, SmartcardError, StatusBytes,
|
||||
Tag, Tags,
|
||||
};
|
||||
use crate::{apdu, keys, Error, KeyType, StatusBytes, Tag, Tags, OP_APP};
|
||||
|
||||
/// An OpenPGP card access object, backed by a CardBackend implementation.
|
||||
///
|
||||
/// Most users will probably want to use the `PcscCard` backend from the `openpgp-card-pcsc` crate.
|
||||
/// Most users will probably want to use the `PcscCard` backend from the `card-backend-pcsc` crate.
|
||||
///
|
||||
/// Users of this crate can keep a long lived OpenPgp object. All operations must be performed on
|
||||
/// a short lived `OpenPgpTransaction`.
|
||||
pub struct OpenPgp {
|
||||
card: Box<dyn CardBackend + Send + Sync>,
|
||||
card_caps: Option<CardCaps>,
|
||||
}
|
||||
|
||||
impl OpenPgp {
|
||||
pub fn new<B>(backend: B) -> Self
|
||||
/// Turn a [card_backend::CardBackend] into an [OpenPgp] object:
|
||||
///
|
||||
/// The OpenPGP application is `SELECT`ed, and the card capabilities
|
||||
/// of the card are retrieved from the "Application Related Data".
|
||||
pub fn new<B>(backend: B) -> Result<Self, Error>
|
||||
where
|
||||
B: Into<Box<dyn CardBackend + Send + Sync>>,
|
||||
{
|
||||
Self {
|
||||
card: backend.into(),
|
||||
}
|
||||
let card: Box<dyn CardBackend + Send + Sync> = backend.into();
|
||||
|
||||
let mut op = Self {
|
||||
card,
|
||||
card_caps: None,
|
||||
};
|
||||
|
||||
let caps = {
|
||||
let mut tx = op.transaction()?;
|
||||
tx.select()?;
|
||||
|
||||
// Init card_caps
|
||||
let ard = tx.application_related_data()?;
|
||||
|
||||
// Determine chaining/extended length support from card
|
||||
// metadata and cache this information in the CardTransaction
|
||||
// implementation (as a CardCaps)
|
||||
let mut ext_support = false;
|
||||
let mut chaining_support = false;
|
||||
|
||||
if let Ok(hist) = ard.historical_bytes() {
|
||||
if let Some(cc) = hist.card_capabilities() {
|
||||
chaining_support = cc.command_chaining();
|
||||
ext_support = cc.extended_lc_le();
|
||||
}
|
||||
}
|
||||
|
||||
let ext_cap = ard.extended_capabilities()?;
|
||||
|
||||
// Get max command/response byte sizes from card
|
||||
let (max_cmd_bytes, max_rsp_bytes) = if let Ok(Some(eli)) =
|
||||
ard.extended_length_information()
|
||||
{
|
||||
// In card 3.x, max lengths come from ExtendedLengthInfo
|
||||
(eli.max_command_bytes(), eli.max_response_bytes())
|
||||
} else if let (Some(cmd), Some(rsp)) = (ext_cap.max_cmd_len(), ext_cap.max_resp_len()) {
|
||||
// In card 2.x, max lengths come from ExtendedCapabilities
|
||||
(cmd, rsp)
|
||||
} else {
|
||||
// Fallback: use 255 if we have no information from the card
|
||||
(255, 255)
|
||||
};
|
||||
|
||||
let pw_status = ard.pw_status_bytes()?;
|
||||
let pw1_max = pw_status.pw1_max_len();
|
||||
let pw3_max = pw_status.pw3_max_len();
|
||||
|
||||
let caps = CardCaps::new(
|
||||
ext_support,
|
||||
chaining_support,
|
||||
max_cmd_bytes,
|
||||
max_rsp_bytes,
|
||||
pw1_max,
|
||||
pw3_max,
|
||||
);
|
||||
|
||||
drop(tx);
|
||||
|
||||
caps
|
||||
};
|
||||
|
||||
log::trace!("init_card_caps to: {:x?}", caps);
|
||||
op.card_caps = Some(caps);
|
||||
|
||||
Ok(op)
|
||||
}
|
||||
|
||||
/// Get the internal `CardBackend`.
|
||||
|
@ -50,10 +117,14 @@ impl OpenPgp {
|
|||
///
|
||||
/// Note: transactions on the Card cannot be long running, they will be reset within seconds
|
||||
/// when idle.
|
||||
///
|
||||
/// If the card has been reset, and `reselect_application` is set, then
|
||||
/// that application will be `SELECT`ed after starting the transaction.
|
||||
pub fn transaction(&mut self) -> Result<OpenPgpTransaction, Error> {
|
||||
Ok(OpenPgpTransaction {
|
||||
tx: self.card.transaction()?,
|
||||
})
|
||||
let card_caps = &mut self.card_caps;
|
||||
let tx = self.card.transaction(Some(OP_APP))?;
|
||||
|
||||
Ok(OpenPgpTransaction { tx, card_caps })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,6 +138,7 @@ impl OpenPgp {
|
|||
/// closed.
|
||||
pub struct OpenPgpTransaction<'a> {
|
||||
tx: Box<dyn CardTransaction + Send + Sync + 'a>,
|
||||
card_caps: &'a mut Option<CardCaps>,
|
||||
}
|
||||
|
||||
impl<'a> OpenPgpTransaction<'a> {
|
||||
|
@ -74,6 +146,24 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
self.tx.as_mut()
|
||||
}
|
||||
|
||||
pub(crate) fn send_command(
|
||||
&mut self,
|
||||
cmd: Command,
|
||||
expect_reply: bool,
|
||||
) -> Result<RawResponse, Error> {
|
||||
apdu::send_command(&mut *self.tx, cmd, *self.card_caps, expect_reply)
|
||||
}
|
||||
|
||||
// SELECT
|
||||
|
||||
/// Select the OpenPGP card application
|
||||
pub fn select(&mut self) -> Result<Vec<u8>, Error> {
|
||||
log::info!("OpenPgpTransaction: select");
|
||||
|
||||
self.send_command(commands::select_openpgp(), false)?
|
||||
.try_into()
|
||||
}
|
||||
|
||||
// --- pinpad ---
|
||||
|
||||
/// Does the reader support FEATURE_VERIFY_PIN_DIRECT?
|
||||
|
@ -96,7 +186,15 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn application_related_data(&mut self) -> Result<ApplicationRelatedData, Error> {
|
||||
log::info!("OpenPgpTransaction: application_related_data");
|
||||
|
||||
self.tx.application_related_data()
|
||||
let resp = self.send_command(commands::application_related_data(), true)?;
|
||||
let value = Value::from(resp.data()?, true)?;
|
||||
|
||||
log::trace!(" ARD value: {:02x?}", value);
|
||||
|
||||
Ok(ApplicationRelatedData(Tlv::new(
|
||||
Tags::ApplicationRelatedData,
|
||||
value,
|
||||
)))
|
||||
}
|
||||
|
||||
// --- login data (5e) ---
|
||||
|
@ -105,7 +203,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn url(&mut self) -> Result<Vec<u8>, Error> {
|
||||
log::info!("OpenPgpTransaction: url");
|
||||
|
||||
let resp = apdu::send_command(self.tx(), commands::url(), true)?;
|
||||
let resp = self.send_command(commands::url(), true)?;
|
||||
|
||||
Ok(resp.data()?.to_vec())
|
||||
}
|
||||
|
@ -114,7 +212,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn login_data(&mut self) -> Result<Vec<u8>, Error> {
|
||||
log::info!("OpenPgpTransaction: login_data");
|
||||
|
||||
let resp = apdu::send_command(self.tx(), commands::login_data(), true)?;
|
||||
let resp = self.send_command(commands::login_data(), true)?;
|
||||
|
||||
Ok(resp.data()?.to_vec())
|
||||
}
|
||||
|
@ -124,7 +222,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: cardholder_related_data");
|
||||
|
||||
let crd = commands::cardholder_related_data();
|
||||
let resp = apdu::send_command(self.tx(), crd, true)?;
|
||||
let resp = self.send_command(crd, true)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
CardholderRelatedData::try_from(resp.data()?)
|
||||
|
@ -135,7 +233,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: security_support_template");
|
||||
|
||||
let sst = commands::security_support_template();
|
||||
let resp = apdu::send_command(self.tx(), sst, true)?;
|
||||
let resp = self.send_command(sst, true)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
let tlv = Tlv::try_from(resp.data()?)?;
|
||||
|
@ -183,7 +281,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: cardholder_certificate");
|
||||
|
||||
let cmd = commands::cardholder_certificate();
|
||||
apdu::send_command(self.tx(), cmd, true)?.try_into()
|
||||
self.send_command(cmd, true)?.try_into()
|
||||
}
|
||||
|
||||
/// Call "GET NEXT DATA" for the DO cardholder certificate.
|
||||
|
@ -194,14 +292,14 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: next_cardholder_certificate");
|
||||
|
||||
let cmd = commands::get_next_cardholder_certificate();
|
||||
apdu::send_command(self.tx(), cmd, true)?.try_into()
|
||||
self.send_command(cmd, true)?.try_into()
|
||||
}
|
||||
|
||||
/// Get "Algorithm Information"
|
||||
pub fn algorithm_information(&mut self) -> Result<Option<AlgoInfo>, Error> {
|
||||
log::info!("OpenPgpTransaction: algorithm_information");
|
||||
|
||||
let resp = apdu::send_command(self.tx(), commands::algo_info(), true)?;
|
||||
let resp = self.send_command(commands::algo_info(), true)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
let ai = AlgoInfo::try_from(resp.data()?)?;
|
||||
|
@ -212,7 +310,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn attestation_certificate(&mut self) -> Result<Vec<u8>, Error> {
|
||||
log::info!("OpenPgpTransaction: attestation_certificate");
|
||||
|
||||
let resp = apdu::send_command(self.tx(), commands::attestation_certificate(), true)?;
|
||||
let resp = self.send_command(commands::attestation_certificate(), true)?;
|
||||
|
||||
Ok(resp.data()?.into())
|
||||
}
|
||||
|
@ -221,7 +319,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn firmware_version(&mut self) -> Result<Vec<u8>, Error> {
|
||||
log::info!("OpenPgpTransaction: firmware_version");
|
||||
|
||||
let resp = apdu::send_command(self.tx(), commands::firmware_version(), true)?;
|
||||
let resp = self.send_command(commands::firmware_version(), true)?;
|
||||
|
||||
Ok(resp.data()?.into())
|
||||
}
|
||||
|
@ -233,7 +331,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn set_identity(&mut self, id: u8) -> Result<Vec<u8>, Error> {
|
||||
log::info!("OpenPgpTransaction: set_identity");
|
||||
|
||||
let resp = apdu::send_command(self.tx(), commands::set_identity(id), false);
|
||||
let resp = self.send_command(commands::set_identity(id), false);
|
||||
|
||||
// Apparently it's normal to get "NotTransacted" from pcsclite when
|
||||
// the identity switch was successful.
|
||||
|
@ -291,7 +389,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
|
||||
// Possible response data (Control Parameter = CP) don't need to be evaluated by the
|
||||
// application (See "7.2.5 SELECT DATA")
|
||||
apdu::send_command(self.tx(), cmd, true)?.try_into()?;
|
||||
self.send_command(cmd, true)?.try_into()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -307,7 +405,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
assert!((1..=4).contains(&num));
|
||||
|
||||
let cmd = commands::private_use_do(num);
|
||||
let resp = apdu::send_command(self.tx(), cmd, true)?;
|
||||
let resp = self.send_command(cmd, true)?;
|
||||
|
||||
Ok(resp.data()?.to_vec())
|
||||
}
|
||||
|
@ -337,7 +435,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
for _ in 0..4 {
|
||||
log::info!(" verify_pw1_81");
|
||||
let verify = commands::verify_pw1_81([0x40; 8].to_vec());
|
||||
let resp = apdu::send_command(self.tx(), verify, false)?;
|
||||
let resp = self.send_command(verify, false)?;
|
||||
if !(resp.status() == StatusBytes::SecurityStatusNotSatisfied
|
||||
|| resp.status() == StatusBytes::AuthenticationMethodBlocked
|
||||
|| matches!(resp.status(), StatusBytes::PasswordNotChecked(_)))
|
||||
|
@ -353,7 +451,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
for _ in 0..4 {
|
||||
log::info!(" verify_pw3");
|
||||
let verify = commands::verify_pw3([0x40; 8].to_vec());
|
||||
let resp = apdu::send_command(self.tx(), verify, false)?;
|
||||
let resp = self.send_command(verify, false)?;
|
||||
|
||||
if !(resp.status() == StatusBytes::SecurityStatusNotSatisfied
|
||||
|| resp.status() == StatusBytes::AuthenticationMethodBlocked
|
||||
|
@ -368,13 +466,13 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
// terminate_df [apdu 00 e6 00 00]
|
||||
log::info!(" terminate_df");
|
||||
let term = commands::terminate_df();
|
||||
let resp = apdu::send_command(self.tx(), term, false)?;
|
||||
let resp = self.send_command(term, false)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
// activate_file [apdu 00 44 00 00]
|
||||
log::info!(" activate_file");
|
||||
let act = commands::activate_file();
|
||||
let resp = apdu::send_command(self.tx(), act, false)?;
|
||||
let resp = self.send_command(act, false)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
Ok(())
|
||||
|
@ -391,7 +489,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: verify_pw1_sign");
|
||||
|
||||
let verify = commands::verify_pw1_81(pin.to_vec());
|
||||
apdu::send_command(self.tx(), verify, false)?.try_into()
|
||||
self.send_command(verify, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Verify pw1 (user) for signing operation (mode 81) using a
|
||||
|
@ -404,7 +502,9 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn verify_pw1_sign_pinpad(&mut self) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: verify_pw1_sign_pinpad");
|
||||
|
||||
let res = self.tx().pinpad_verify(PinType::Sign)?;
|
||||
let cc = *self.card_caps;
|
||||
|
||||
let res = self.tx().pinpad_verify(PinType::Sign, &cc)?;
|
||||
RawResponse::try_from(res)?.try_into()
|
||||
}
|
||||
|
||||
|
@ -420,7 +520,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: check_pw1_sign");
|
||||
|
||||
let verify = commands::verify_pw1_81(vec![]);
|
||||
apdu::send_command(self.tx(), verify, false)?.try_into()
|
||||
self.send_command(verify, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Verify PW1 (user).
|
||||
|
@ -429,7 +529,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: verify_pw1_user");
|
||||
|
||||
let verify = commands::verify_pw1_82(pin.to_vec());
|
||||
apdu::send_command(self.tx(), verify, false)?.try_into()
|
||||
self.send_command(verify, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Verify PW1 (user) for operations except signing (mode 82),
|
||||
|
@ -439,7 +539,9 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn verify_pw1_user_pinpad(&mut self) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: verify_pw1_user_pinpad");
|
||||
|
||||
let res = self.tx().pinpad_verify(PinType::User)?;
|
||||
let cc = *self.card_caps;
|
||||
|
||||
let res = self.tx().pinpad_verify(PinType::User, &cc)?;
|
||||
RawResponse::try_from(res)?.try_into()
|
||||
}
|
||||
|
||||
|
@ -456,7 +558,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: check_pw1_user");
|
||||
|
||||
let verify = commands::verify_pw1_82(vec![]);
|
||||
apdu::send_command(self.tx(), verify, false)?.try_into()
|
||||
self.send_command(verify, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Verify PW3 (admin).
|
||||
|
@ -464,7 +566,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: verify_pw3");
|
||||
|
||||
let verify = commands::verify_pw3(pin.to_vec());
|
||||
apdu::send_command(self.tx(), verify, false)?.try_into()
|
||||
self.send_command(verify, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Verify PW3 (admin) using a pinpad on the card reader. If no usable
|
||||
|
@ -472,7 +574,9 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn verify_pw3_pinpad(&mut self) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: verify_pw3_pinpad");
|
||||
|
||||
let res = self.tx().pinpad_verify(PinType::Admin)?;
|
||||
let cc = *self.card_caps;
|
||||
|
||||
let res = self.tx().pinpad_verify(PinType::Admin, &cc)?;
|
||||
RawResponse::try_from(res)?.try_into()
|
||||
}
|
||||
|
||||
|
@ -488,7 +592,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: check_pw3");
|
||||
|
||||
let verify = commands::verify_pw3(vec![]);
|
||||
apdu::send_command(self.tx(), verify, false)?.try_into()
|
||||
self.send_command(verify, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Change the value of PW1 (user password).
|
||||
|
@ -502,7 +606,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
data.extend(new);
|
||||
|
||||
let change = commands::change_pw1(data);
|
||||
apdu::send_command(self.tx(), change, false)?.try_into()
|
||||
self.send_command(change, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Change the value of PW1 (0x81) using a pinpad on the
|
||||
|
@ -510,9 +614,11 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn change_pw1_pinpad(&mut self) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: change_pw1_pinpad");
|
||||
|
||||
let cc = *self.card_caps;
|
||||
|
||||
// Note: for change PW, only 0x81 and 0x83 are used!
|
||||
// 0x82 is implicitly the same as 0x81.
|
||||
let res = self.tx().pinpad_modify(PinType::Sign)?;
|
||||
let res = self.tx().pinpad_modify(PinType::Sign, &cc)?;
|
||||
RawResponse::try_from(res)?.try_into()
|
||||
}
|
||||
|
||||
|
@ -527,7 +633,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
data.extend(new);
|
||||
|
||||
let change = commands::change_pw3(data);
|
||||
apdu::send_command(self.tx(), change, false)?.try_into()
|
||||
self.send_command(change, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Change the value of PW3 (admin password) using a pinpad on the
|
||||
|
@ -535,7 +641,9 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn change_pw3_pinpad(&mut self) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: change_pw3_pinpad");
|
||||
|
||||
let res = self.tx().pinpad_modify(PinType::Admin)?;
|
||||
let cc = *self.card_caps;
|
||||
|
||||
let res = self.tx().pinpad_modify(PinType::Admin, &cc)?;
|
||||
RawResponse::try_from(res)?.try_into()
|
||||
}
|
||||
|
||||
|
@ -554,7 +662,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: reset_retry_counter_pw1");
|
||||
|
||||
let reset = commands::reset_retry_counter_pw1(resetting_code, new_pw1);
|
||||
apdu::send_command(self.tx(), reset, false)?.try_into()
|
||||
self.send_command(reset, false)?.try_into()
|
||||
}
|
||||
|
||||
// --- decrypt ---
|
||||
|
@ -604,7 +712,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
|
||||
// The OpenPGP card is already connected and PW1 82 has been verified
|
||||
let dec_cmd = commands::decryption(data);
|
||||
let resp = apdu::send_command(self.tx(), dec_cmd, true)?;
|
||||
let resp = self.send_command(dec_cmd, true)?;
|
||||
resp.check_ok()?;
|
||||
|
||||
Ok(resp.data().map(|d| d.to_vec())?)
|
||||
|
@ -639,7 +747,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
}
|
||||
|
||||
let cmd = commands::manage_security_environment(for_operation, key_ref);
|
||||
let resp = apdu::send_command(self.tx(), cmd, false)?;
|
||||
let resp = self.send_command(cmd, false)?;
|
||||
resp.check_ok()?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -671,7 +779,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
|
||||
let cds_cmd = commands::signature(data);
|
||||
|
||||
let resp = apdu::send_command(self.tx(), cds_cmd, true)?;
|
||||
let resp = self.send_command(cds_cmd, true)?;
|
||||
|
||||
Ok(resp.data().map(|d| d.to_vec())?)
|
||||
}
|
||||
|
@ -701,7 +809,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: internal_authenticate");
|
||||
|
||||
let ia_cmd = commands::internal_authenticate(data);
|
||||
let resp = apdu::send_command(self.tx(), ia_cmd, true)?;
|
||||
let resp = self.send_command(ia_cmd, true)?;
|
||||
|
||||
Ok(resp.data().map(|d| d.to_vec())?)
|
||||
}
|
||||
|
@ -721,7 +829,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
assert!((1..=4).contains(&num));
|
||||
|
||||
let cmd = commands::put_private_use_do(num, data);
|
||||
let resp = apdu::send_command(self.tx(), cmd, true)?;
|
||||
let resp = self.send_command(cmd, true)?;
|
||||
|
||||
Ok(resp.data()?.to_vec())
|
||||
}
|
||||
|
@ -729,14 +837,14 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
pub fn set_login(&mut self, login: &[u8]) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: set_login");
|
||||
let put_login_data = commands::put_login_data(login.to_vec());
|
||||
apdu::send_command(self.tx(), put_login_data, false)?.try_into()
|
||||
self.send_command(put_login_data, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, name: &[u8]) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: set_name");
|
||||
|
||||
let put_name = commands::put_name(name.to_vec());
|
||||
apdu::send_command(self.tx(), put_name, false)?.try_into()
|
||||
self.send_command(put_name, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_lang(&mut self, lang: &[Lang]) -> Result<(), Error> {
|
||||
|
@ -748,21 +856,21 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
.collect();
|
||||
|
||||
let put_lang = commands::put_lang(bytes);
|
||||
apdu::send_command(self.tx(), put_lang, false)?.try_into()
|
||||
self.send_command(put_lang, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_sex(&mut self, sex: Sex) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: set_sex");
|
||||
|
||||
let put_sex = commands::put_sex((&sex).into());
|
||||
apdu::send_command(self.tx(), put_sex, false)?.try_into()
|
||||
self.send_command(put_sex, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_url(&mut self, url: &[u8]) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: set_url");
|
||||
|
||||
let put_url = commands::put_url(url.to_vec());
|
||||
apdu::send_command(self.tx(), put_url, false)?.try_into()
|
||||
self.send_command(put_url, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Set cardholder certificate (for AUT, DEC or SIG).
|
||||
|
@ -773,7 +881,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: set_cardholder_certificate");
|
||||
|
||||
let cmd = commands::put_cardholder_certificate(data);
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Set algorithm attributes
|
||||
|
@ -788,7 +896,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
// Command to PUT the algorithm attributes
|
||||
let cmd = commands::put_data(key_type.algorithm_tag(), algo.to_data_object()?);
|
||||
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Set PW Status Bytes.
|
||||
|
@ -812,7 +920,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
let data = pw_status.serialize_for_put(long);
|
||||
|
||||
let cmd = commands::put_pw_status(data);
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_fingerprint(&mut self, fp: Fingerprint, key_type: KeyType) -> Result<(), Error> {
|
||||
|
@ -820,28 +928,28 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
|
||||
let fp_cmd = commands::put_data(key_type.fingerprint_put_tag(), fp.as_bytes().to_vec());
|
||||
|
||||
apdu::send_command(self.tx(), fp_cmd, false)?.try_into()
|
||||
self.send_command(fp_cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_ca_fingerprint_1(&mut self, fp: Fingerprint) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: set_ca_fingerprint_1");
|
||||
|
||||
let fp_cmd = commands::put_data(Tags::CaFingerprint1, fp.as_bytes().to_vec());
|
||||
apdu::send_command(self.tx(), fp_cmd, false)?.try_into()
|
||||
self.send_command(fp_cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_ca_fingerprint_2(&mut self, fp: Fingerprint) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: set_ca_fingerprint_2");
|
||||
|
||||
let fp_cmd = commands::put_data(Tags::CaFingerprint2, fp.as_bytes().to_vec());
|
||||
apdu::send_command(self.tx(), fp_cmd, false)?.try_into()
|
||||
self.send_command(fp_cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_ca_fingerprint_3(&mut self, fp: Fingerprint) -> Result<(), Error> {
|
||||
log::info!("OpenPgpTransaction: set_ca_fingerprint_3");
|
||||
|
||||
let fp_cmd = commands::put_data(Tags::CaFingerprint3, fp.as_bytes().to_vec());
|
||||
apdu::send_command(self.tx(), fp_cmd, false)?.try_into()
|
||||
self.send_command(fp_cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
pub fn set_creation_time(
|
||||
|
@ -862,7 +970,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
|
||||
let time_cmd = commands::put_data(key_type.timestamp_put_tag(), time_value);
|
||||
|
||||
apdu::send_command(self.tx(), time_cmd, false)?.try_into()
|
||||
self.send_command(time_cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
// FIXME: optional DO SM-Key-ENC
|
||||
|
@ -875,7 +983,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: set_resetting_code");
|
||||
|
||||
let cmd = commands::put_data(Tags::ResettingCode, resetting_code.to_vec());
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Set AES key for symmetric decryption/encryption operations.
|
||||
|
@ -888,7 +996,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
|
||||
let fp_cmd = commands::put_data(Tags::PsoEncDecKey, key.to_vec());
|
||||
|
||||
apdu::send_command(self.tx(), fp_cmd, false)?.try_into()
|
||||
self.send_command(fp_cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
// FIXME: optional DO for PSO:ENC/DEC with AES
|
||||
|
@ -898,7 +1006,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: set_uif_pso_cds");
|
||||
|
||||
let cmd = commands::put_data(Tags::UifSig, uif.as_bytes().to_vec());
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Set UIF for PSO:DEC
|
||||
|
@ -906,7 +1014,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: set_uif_pso_dec");
|
||||
|
||||
let cmd = commands::put_data(Tags::UifDec, uif.as_bytes().to_vec());
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Set UIF for PSO:AUT
|
||||
|
@ -914,7 +1022,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: set_uif_pso_aut");
|
||||
|
||||
let cmd = commands::put_data(Tags::UifAuth, uif.as_bytes().to_vec());
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Set UIF for Attestation key
|
||||
|
@ -922,7 +1030,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
log::info!("OpenPgpTransaction: set_uif_attestation");
|
||||
|
||||
let cmd = commands::put_data(Tags::UifAttestation, uif.as_bytes().to_vec());
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
/// Generate Attestation (Yubico)
|
||||
|
@ -937,7 +1045,7 @@ impl<'a> OpenPgpTransaction<'a> {
|
|||
};
|
||||
|
||||
let cmd = commands::generate_attestation(key);
|
||||
apdu::send_command(self.tx(), cmd, false)?.try_into()
|
||||
self.send_command(cmd, false)?.try_into()
|
||||
}
|
||||
|
||||
// FIXME: Attestation key algo attr, FP, CA-FP, creation time
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "openpgp-card-pcsc"
|
||||
description = "PCSC OpenPGP card backend, for use with the openpgp-card crate"
|
||||
name = "card-backend-pcsc"
|
||||
description = "PCSC card backend, e.g. for use with the openpgp-card crate"
|
||||
authors = ["Heiko Schaefer <heiko@schaefer.name>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
version = "0.3.1"
|
||||
version = "0.4.0"
|
||||
edition = "2018"
|
||||
repository = "https://gitlab.com/openpgp-card/openpgp-card"
|
||||
documentation = "https://docs.rs/crate/openpgp-card-pcsc"
|
||||
documentation = "https://docs.rs/crate/card-backend-pcsc"
|
||||
|
||||
[dependencies]
|
||||
openpgp-card = { path = "../openpgp-card", version = "0.3.5" }
|
||||
card-backend = { path = "../card-backend", version = "0.1" }
|
||||
iso7816-tlv = "0.4"
|
||||
pcsc = "2.7"
|
||||
log = "0.4"
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
# PC/SC client for the openpgp-card library
|
||||
# PC/SC based smart card backend
|
||||
|
||||
This crate provides `PcscBackend` and `PcscTransaction`, which are implementations of the
|
||||
`CardBackend` and `CardTransactions` traits from the [`openpgp-card`](https://crates.io/crates/openpgp-card) crate.
|
||||
`CardBackend` and `CardTransactions` traits from the [`card-backend`](https://crates.io/crates/card-backend) crate.
|
||||
|
||||
This implementation uses the [pcsc](https://crates.io/crates/pcsc) Rust wrapper crate
|
||||
to access OpenPGP cards.
|
||||
|
||||
## Documentation
|
||||
Mainly intended for use with the [openpgp-card](https://gitlab.com/openpgp-card/openpgp-card) library.
|
||||
|
||||
## Documentation on PC/SC
|
||||
|
||||
[PC/SC](https://en.wikipedia.org/wiki/PC/SC) is a standard for interaction with smartcards and readers.
|
||||
|
||||
|
|
412
pcsc/src/lib.rs
412
pcsc/src/lib.rs
|
@ -1,38 +1,30 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! This crate implements the `CardBackend`/`CardTransaction` backend for
|
||||
//! `openpgp-card`. It uses the PCSC middleware to access the OpenPGP
|
||||
//! application on smart cards.
|
||||
//! This crate implements the traits [CardBackend] and [CardTransaction].
|
||||
//! It uses the PCSC middleware to access smart cards.
|
||||
//!
|
||||
//! This crate is mainly intended for use by the `openpgp-card` crate.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use card_backend::{CardBackend, CardCaps, CardTransaction, PinType, SmartcardError};
|
||||
use iso7816_tlv::simple::Tlv;
|
||||
use openpgp_card::card_do::ApplicationRelatedData;
|
||||
use openpgp_card::{CardBackend, CardCaps, CardTransaction, Error, PinType, SmartcardError};
|
||||
use pcsc::Disposition;
|
||||
|
||||
const FEATURE_VERIFY_PIN_DIRECT: u8 = 0x06;
|
||||
const FEATURE_MODIFY_PIN_DIRECT: u8 = 0x07;
|
||||
|
||||
fn default_mode(mode: Option<pcsc::ShareMode>) -> pcsc::ShareMode {
|
||||
if let Some(mode) = mode {
|
||||
mode
|
||||
} else {
|
||||
pcsc::ShareMode::Shared
|
||||
}
|
||||
}
|
||||
|
||||
/// An opened PCSC Card (without open transaction).
|
||||
/// The OpenPGP application on the card is `select`-ed while setting up a PcscCard object.
|
||||
/// Note: No application is `select`-ed on the card while setting up a PcscCard object.
|
||||
///
|
||||
/// This struct can be used to hold on to a Card, even while no operations
|
||||
/// are performed on the Card. To perform operations on the card, a
|
||||
/// `TxClient` object needs to be obtained (via PcscCard::transaction()).
|
||||
/// [PcscTransaction] object needs to be obtained (via [PcscBackend::transaction]).
|
||||
pub struct PcscBackend {
|
||||
card: pcsc::Card,
|
||||
mode: pcsc::ShareMode,
|
||||
card_caps: Option<CardCaps>,
|
||||
reader_caps: HashMap<u8, Tlv>,
|
||||
}
|
||||
|
||||
|
@ -56,118 +48,99 @@ impl From<PcscBackend> for Box<dyn CardBackend + Sync + Send> {
|
|||
/// <https://docs.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardbegintransaction?redirectedfrom=MSDN#remarks>)
|
||||
pub struct PcscTransaction<'b> {
|
||||
tx: pcsc::Transaction<'b>,
|
||||
card_caps: Option<CardCaps>, // FIXME: manual copy from PcscCard
|
||||
reader_caps: HashMap<u8, Tlv>, // FIXME: manual copy from PcscCard
|
||||
reader_caps: HashMap<u8, Tlv>, // FIXME: gets manually cloned
|
||||
}
|
||||
|
||||
impl<'b> PcscTransaction<'b> {
|
||||
/// Start a transaction on `card`.
|
||||
///
|
||||
/// `reselect` set to `false` is only used internally in this crate,
|
||||
/// during initial setup of cards. Otherwise it must be `true`, to
|
||||
/// cause a select() call on cards that have been reset.
|
||||
fn new(card: &'b mut PcscBackend, reselect: bool) -> Result<Self, Error> {
|
||||
use pcsc::Disposition;
|
||||
/// If `reselect_application` is set, the application is SELECTed,
|
||||
/// if the card reports having been reset.
|
||||
fn new(
|
||||
card: &'b mut PcscBackend,
|
||||
reselect_application: Option<&[u8]>,
|
||||
) -> Result<Self, SmartcardError> {
|
||||
log::trace!("Start a transaction");
|
||||
|
||||
let mut was_reset = false;
|
||||
|
||||
let card_caps = card.card_caps();
|
||||
let reader_caps = card.reader_caps();
|
||||
let mode = card.mode();
|
||||
let mode = card.mode;
|
||||
let reader_caps = card.reader_caps.clone();
|
||||
|
||||
let mut c = card.card();
|
||||
let mut c = &mut card.card;
|
||||
|
||||
loop {
|
||||
match c.transaction2() {
|
||||
Ok(mut tx) => {
|
||||
// A transaction has been successfully started
|
||||
Ok(tx) => {
|
||||
// A pcsc transaction has been successfully started
|
||||
|
||||
let mut pt = Self { tx, reader_caps };
|
||||
|
||||
if was_reset {
|
||||
log::trace!("start_tx: card was reset, select!");
|
||||
log::trace!("Card was reset");
|
||||
|
||||
let mut txc = Self {
|
||||
tx,
|
||||
card_caps,
|
||||
reader_caps: reader_caps.clone(),
|
||||
};
|
||||
// If the caller expects that an application on the
|
||||
// card has been selected, re-select the application
|
||||
// here.
|
||||
//
|
||||
// When initially opening a card, we don't do this
|
||||
// (freshly opened cards don't have an application
|
||||
// "SELECT"ed).
|
||||
if let Some(app) = reselect_application {
|
||||
log::trace!("Will re-select an application after card reset");
|
||||
|
||||
// In contexts where the caller of this fn
|
||||
// expects that the card has already been opened,
|
||||
// re-open the card here.
|
||||
// For initial card-opening, we don't do this, then
|
||||
// the caller always expects a card that has not
|
||||
// been "select"ed yet.
|
||||
if reselect {
|
||||
PcscTransaction::select(&mut txc)?;
|
||||
let mut res = CardTransaction::select(&mut pt, app)?;
|
||||
log::trace!("select res: {:0x?}", res);
|
||||
|
||||
// Drop any bytes before the status code.
|
||||
//
|
||||
// e.g. SELECT on Basic Card 3.4 returns:
|
||||
// [6f, 1d,
|
||||
// 62, 15, 84, 10, d2, 76, 0, 1, 24, 1, 3, 4, 0, 5, 0, 0, a8, 35, 0, 0, 8a, 1, 5, 64, 4, 53, 2, c4, 41,
|
||||
// 90, 0]
|
||||
if res.len() > 2 {
|
||||
res.drain(0..res.len() - 2);
|
||||
}
|
||||
|
||||
if res != [0x90, 0x00] {
|
||||
break Err(SmartcardError::Error(format!(
|
||||
"Error while attempting to (re-)select {:x?}, status code {:x?}",
|
||||
app, res
|
||||
)));
|
||||
}
|
||||
|
||||
log::trace!("re-select ok");
|
||||
}
|
||||
|
||||
tx = txc.tx;
|
||||
}
|
||||
|
||||
let txc = Self {
|
||||
tx,
|
||||
card_caps,
|
||||
reader_caps,
|
||||
};
|
||||
|
||||
break Ok(txc);
|
||||
break Ok(pt);
|
||||
}
|
||||
Err((c_, pcsc::Error::ResetCard)) => {
|
||||
// Card was reset, need to reconnect
|
||||
was_reset = true;
|
||||
|
||||
// drop(res);
|
||||
|
||||
c = c_;
|
||||
|
||||
log::trace!("start_tx: do reconnect");
|
||||
|
||||
{
|
||||
c.reconnect(mode, pcsc::Protocols::ANY, Disposition::ResetCard)
|
||||
.map_err(|e| {
|
||||
Error::Smartcard(SmartcardError::Error(format!(
|
||||
"Reconnect failed: {e:?}"
|
||||
)))
|
||||
})?;
|
||||
}
|
||||
c.reconnect(mode, pcsc::Protocols::ANY, Disposition::ResetCard)
|
||||
.map_err(|e| SmartcardError::Error(format!("Reconnect failed: {e:?}")))?;
|
||||
|
||||
log::trace!("start_tx: reconnected.");
|
||||
|
||||
// -> try opening a transaction again
|
||||
// -> try opening a transaction again, in the next loop run
|
||||
}
|
||||
Err((_, e)) => {
|
||||
log::trace!("start_tx: error {:?}", e);
|
||||
break Err(Error::Smartcard(SmartcardError::Error(format!(
|
||||
"Error: {e:?}"
|
||||
))));
|
||||
break Err(SmartcardError::Error(format!("Error: {e:?}")));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to select the OpenPGP application on a card
|
||||
fn select(card_tx: &mut PcscTransaction) -> Result<(), Error> {
|
||||
if <dyn CardTransaction>::select(card_tx).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Smartcard(SmartcardError::SelectOpenPGPCardFailed))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get application_related_data from card
|
||||
fn application_related_data(
|
||||
card_tx: &mut PcscTransaction,
|
||||
) -> Result<ApplicationRelatedData, Error> {
|
||||
<dyn CardTransaction>::application_related_data(card_tx).map_err(|e| {
|
||||
Error::Smartcard(SmartcardError::Error(format!(
|
||||
"TxClient: failed to get application_related_data {e:x?}"
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
/// GET_FEATURE_REQUEST
|
||||
/// (see http://pcscworkgroup.com/Download/Specifications/pcsc10_v2.02.09.pdf)
|
||||
fn features(&mut self) -> Result<Vec<Tlv>, Error> {
|
||||
fn features(&mut self) -> Result<Vec<Tlv>, SmartcardError> {
|
||||
let mut recv = vec![0; 1024];
|
||||
|
||||
let cm_ioctl_get_feature_request = pcsc::ctl_code(3400);
|
||||
|
@ -175,9 +148,7 @@ impl<'b> PcscTransaction<'b> {
|
|||
.tx
|
||||
.control(cm_ioctl_get_feature_request, &[], &mut recv)
|
||||
.map_err(|e| {
|
||||
Error::Smartcard(SmartcardError::Error(format!(
|
||||
"GET_FEATURE_REQUEST control call failed: {e:?}"
|
||||
)))
|
||||
SmartcardError::Error(format!("GET_FEATURE_REQUEST control call failed: {e:?}"))
|
||||
})?;
|
||||
|
||||
Ok(Tlv::parse_all(res))
|
||||
|
@ -190,42 +161,39 @@ impl<'b> PcscTransaction<'b> {
|
|||
PinType::Admin => 8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the maximum pin length for pin_id.
|
||||
fn max_pin_len(&self, pin: PinType) -> Result<u8, Error> {
|
||||
if let Some(card_caps) = self.card_caps {
|
||||
fn max_pin_len(
|
||||
&self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<u8, SmartcardError> {
|
||||
if let Some(card_caps) = card_caps {
|
||||
match pin {
|
||||
PinType::User | PinType::Sign => Ok(card_caps.pw1_max_len()),
|
||||
PinType::Admin => Ok(card_caps.pw3_max_len()),
|
||||
}
|
||||
} else {
|
||||
Err(Error::InternalError("card_caps is None".into()))
|
||||
Err(SmartcardError::Error("card_caps is None".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CardTransaction for PcscTransaction<'_> {
|
||||
fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result<Vec<u8>, Error> {
|
||||
fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result<Vec<u8>, SmartcardError> {
|
||||
let mut resp_buffer = vec![0; buf_size];
|
||||
|
||||
let resp = self
|
||||
.tx
|
||||
.transmit(cmd, &mut resp_buffer)
|
||||
.map_err(|e| match e {
|
||||
pcsc::Error::NotTransacted => Error::Smartcard(SmartcardError::NotTransacted),
|
||||
_ => Error::Smartcard(SmartcardError::Error(format!("Transmit failed: {e:?}"))),
|
||||
pcsc::Error::NotTransacted => SmartcardError::NotTransacted,
|
||||
_ => SmartcardError::Error(format!("Transmit failed: {e:?}")),
|
||||
})?;
|
||||
|
||||
Ok(resp.to_vec())
|
||||
}
|
||||
|
||||
fn init_card_caps(&mut self, caps: CardCaps) {
|
||||
self.card_caps = Some(caps);
|
||||
}
|
||||
|
||||
fn card_caps(&self) -> Option<&CardCaps> {
|
||||
self.card_caps.as_ref()
|
||||
}
|
||||
|
||||
fn feature_pinpad_verify(&self) -> bool {
|
||||
self.reader_caps.contains_key(&FEATURE_VERIFY_PIN_DIRECT)
|
||||
}
|
||||
|
@ -234,9 +202,13 @@ impl CardTransaction for PcscTransaction<'_> {
|
|||
self.reader_caps.contains_key(&FEATURE_MODIFY_PIN_DIRECT)
|
||||
}
|
||||
|
||||
fn pinpad_verify(&mut self, pin: PinType) -> Result<Vec<u8>, Error> {
|
||||
fn pinpad_verify(
|
||||
&mut self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError> {
|
||||
let pin_min_size = self.min_pin_len(pin);
|
||||
let pin_max_size = self.max_pin_len(pin)?;
|
||||
let pin_max_size = self.max_pin_len(pin, card_caps)?;
|
||||
|
||||
// Default to varlen, for now.
|
||||
// (NOTE: Some readers don't support varlen, and need explicit length
|
||||
|
@ -310,26 +282,28 @@ impl CardTransaction for PcscTransaction<'_> {
|
|||
let verify_ioctl: [u8; 4] = self
|
||||
.reader_caps
|
||||
.get(&FEATURE_VERIFY_PIN_DIRECT)
|
||||
.ok_or_else(|| Error::Smartcard(SmartcardError::Error("no reader_capability".into())))?
|
||||
.ok_or_else(|| SmartcardError::Error("no reader_capability".into()))?
|
||||
.value()
|
||||
.try_into()
|
||||
.map_err(|e| Error::ParseError(format!("unexpected feature data: {e:?}")))?;
|
||||
.map_err(|e| SmartcardError::Error(format!("unexpected feature data: {e:?}")))?;
|
||||
|
||||
let res = self
|
||||
.tx
|
||||
.control(u32::from_be_bytes(verify_ioctl).into(), &send, &mut recv)
|
||||
.map_err(|e: pcsc::Error| {
|
||||
Error::Smartcard(SmartcardError::Error(format!("pcsc Error: {e:?}")))
|
||||
})?;
|
||||
.map_err(|e: pcsc::Error| SmartcardError::Error(format!("pcsc Error: {e:?}")))?;
|
||||
|
||||
log::trace!(" <- pcsc pinpad_verify result: {:x?}", res);
|
||||
|
||||
Ok(res.to_vec())
|
||||
}
|
||||
|
||||
fn pinpad_modify(&mut self, pin: PinType) -> Result<Vec<u8>, Error> {
|
||||
fn pinpad_modify(
|
||||
&mut self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError> {
|
||||
let pin_min_size = self.min_pin_len(pin);
|
||||
let pin_max_size = self.max_pin_len(pin)?;
|
||||
let pin_max_size = self.max_pin_len(pin, card_caps)?;
|
||||
|
||||
// Default to varlen, for now.
|
||||
// (NOTE: Some readers don't support varlen, and need explicit length
|
||||
|
@ -413,17 +387,15 @@ impl CardTransaction for PcscTransaction<'_> {
|
|||
let modify_ioctl: [u8; 4] = self
|
||||
.reader_caps
|
||||
.get(&FEATURE_MODIFY_PIN_DIRECT)
|
||||
.ok_or_else(|| Error::Smartcard(SmartcardError::Error("no reader_capability".into())))?
|
||||
.ok_or_else(|| SmartcardError::Error("no reader_capability".into()))?
|
||||
.value()
|
||||
.try_into()
|
||||
.map_err(|e| Error::ParseError(format!("unexpected feature data: {e:?}")))?;
|
||||
.map_err(|e| SmartcardError::Error(format!("unexpected feature data: {e:?}")))?;
|
||||
|
||||
let res = self
|
||||
.tx
|
||||
.control(u32::from_be_bytes(modify_ioctl).into(), &send, &mut recv)
|
||||
.map_err(|e: pcsc::Error| {
|
||||
Error::Smartcard(SmartcardError::Error(format!("pcsc Error: {e:?}")))
|
||||
})?;
|
||||
.map_err(|e: pcsc::Error| SmartcardError::Error(format!("pcsc Error: {e:?}")))?;
|
||||
|
||||
log::trace!(" <- pcsc pinpad_modify result: {:x?}", res);
|
||||
|
||||
|
@ -432,16 +404,7 @@ impl CardTransaction for PcscTransaction<'_> {
|
|||
}
|
||||
|
||||
impl PcscBackend {
|
||||
fn card(&mut self) -> &mut pcsc::Card {
|
||||
&mut self.card
|
||||
}
|
||||
|
||||
fn mode(&self) -> pcsc::ShareMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
/// A list of "raw" opened PCSC Cards (without selecting the OpenPGP card
|
||||
/// application)
|
||||
/// A list of "raw" opened PCSC Cards (without selecting any application)
|
||||
fn raw_pcsc_cards(mode: pcsc::ShareMode) -> Result<Vec<pcsc::Card>, SmartcardError> {
|
||||
log::trace!("raw_pcsc_cards start");
|
||||
|
||||
|
@ -505,116 +468,52 @@ impl PcscBackend {
|
|||
}
|
||||
}
|
||||
|
||||
/// Starts from a list of all pcsc cards, then compares their OpenPGP
|
||||
/// application identity with `ident` (if `ident` is None, all Cards are
|
||||
/// returned). Returns fully initialized PcscCard structs for all matching
|
||||
/// cards.
|
||||
fn cards_filter(ident: Option<&str>, mode: pcsc::ShareMode) -> Result<Vec<Self>, Error> {
|
||||
let mut cards: Vec<Self> = vec![];
|
||||
|
||||
for mut card in Self::raw_pcsc_cards(mode).map_err(Error::Smartcard)? {
|
||||
log::trace!("cards_filter: next card");
|
||||
log::trace!(" status: {:x?}", card.status2_owned());
|
||||
|
||||
let mut store_card = false;
|
||||
{
|
||||
// start transaction
|
||||
let mut p = PcscBackend::new(card, mode);
|
||||
let mut txc = PcscTransaction::new(&mut p, false)?;
|
||||
|
||||
{
|
||||
if let Err(e) = PcscTransaction::select(&mut txc) {
|
||||
log::trace!(" select error: {:?}", e);
|
||||
} else {
|
||||
// successfully opened the OpenPGP application
|
||||
log::trace!(" select ok, will read ARD");
|
||||
log::trace!(" status: {:x?}", txc.tx.status2_owned());
|
||||
|
||||
if let Some(ident) = ident {
|
||||
if let Ok(ard) = PcscTransaction::application_related_data(&mut txc) {
|
||||
let aid = ard.application_id()?;
|
||||
|
||||
if aid.ident() == ident.to_ascii_uppercase() {
|
||||
// FIXME: handle multiple cards with matching ident
|
||||
log::info!(" found card: {:?} (will use)", ident);
|
||||
|
||||
// we want to return this one card
|
||||
store_card = true;
|
||||
} else {
|
||||
log::info!(" found card: {:?} (won't use)", aid.ident());
|
||||
}
|
||||
} else {
|
||||
// couldn't read ARD for this card.
|
||||
// ignore and move on
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// we want to return all cards
|
||||
store_card = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(txc);
|
||||
card = p.card;
|
||||
}
|
||||
|
||||
if store_card {
|
||||
let pcsc = PcscBackend::new(card, mode);
|
||||
cards.push(pcsc.initialize_card()?);
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("cards_filter: found {} cards", cards.len());
|
||||
|
||||
Ok(cards)
|
||||
}
|
||||
|
||||
/// Return all cards on which the OpenPGP application could be selected.
|
||||
/// Returns an Iterator over Smart Cards that are accessible via PCSC.
|
||||
///
|
||||
/// Each card has the OpenPGP application selected, card_caps and reader_caps have been
|
||||
/// initialized.
|
||||
pub fn cards(mode: Option<pcsc::ShareMode>) -> Result<Vec<Self>, Error> {
|
||||
Self::cards_filter(None, default_mode(mode))
|
||||
/// No application is SELECTed on the cards.
|
||||
/// You can not assume that any particular application is available on the cards.
|
||||
pub fn cards(
|
||||
mode: Option<pcsc::ShareMode>,
|
||||
) -> Result<impl Iterator<Item = Result<Self, SmartcardError>>, SmartcardError> {
|
||||
let mode = mode.unwrap_or(pcsc::ShareMode::Shared);
|
||||
|
||||
let cards = Self::raw_pcsc_cards(mode)?;
|
||||
|
||||
Ok(cards.into_iter().map(move |card| {
|
||||
let backend = PcscBackend {
|
||||
card,
|
||||
mode,
|
||||
reader_caps: Default::default(),
|
||||
};
|
||||
|
||||
backend.initialize_card()
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns the OpenPGP card that matches `ident`, if it is available.
|
||||
/// A fully initialized PcscCard is returned: the OpenPGP application has
|
||||
/// been selected, card_caps and reader_caps have been initialized.
|
||||
pub fn open_by_ident(ident: &str, mode: Option<pcsc::ShareMode>) -> Result<Self, Error> {
|
||||
log::trace!("open_by_ident for {:?}", ident);
|
||||
/// Returns an Iterator over Smart Cards that are accessible via PCSC.
|
||||
/// Like [Self::cards], but returns the cards as [CardBackend].
|
||||
pub fn card_backends(
|
||||
mode: Option<pcsc::ShareMode>,
|
||||
) -> Result<
|
||||
impl Iterator<Item = Result<Box<dyn CardBackend + Send + Sync>, SmartcardError>>,
|
||||
SmartcardError,
|
||||
> {
|
||||
let cards = PcscBackend::cards(mode)?;
|
||||
|
||||
let mut cards = Self::cards_filter(Some(ident), default_mode(mode))?;
|
||||
|
||||
if !cards.is_empty() {
|
||||
// FIXME: handle >1 cards found
|
||||
|
||||
Ok(cards.pop().unwrap())
|
||||
} else {
|
||||
Err(Error::Smartcard(SmartcardError::CardNotFound(
|
||||
ident.to_string(),
|
||||
)))
|
||||
}
|
||||
Ok(cards.map(|c| match c {
|
||||
Ok(c) => Ok(Box::new(c) as Box<dyn CardBackend + Send + Sync>),
|
||||
Err(e) => Err(e),
|
||||
}))
|
||||
}
|
||||
|
||||
fn new(card: pcsc::Card, mode: pcsc::ShareMode) -> Self {
|
||||
Self {
|
||||
card,
|
||||
mode,
|
||||
card_caps: None,
|
||||
reader_caps: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialized a PcscCard:
|
||||
/// - Obtain and store feature lists from reader (pinpad functionality).
|
||||
/// - Get ARD from card, set CardCaps based on ARD.
|
||||
fn initialize_card(mut self) -> Result<Self, Error> {
|
||||
/// Initialize this PcscBackend (obtains and stores feature lists from reader,
|
||||
/// to determine if the reader offers PIN pad functionality).
|
||||
fn initialize_card(mut self) -> Result<Self, SmartcardError> {
|
||||
log::trace!("pcsc initialize_card");
|
||||
|
||||
let mut h: HashMap<u8, Tlv> = HashMap::default();
|
||||
|
||||
let mut txc = PcscTransaction::new(&mut self, true)?;
|
||||
let mut txc = PcscTransaction::new(&mut self, None)?;
|
||||
|
||||
// Get Features from reader (pinpad verify/modify)
|
||||
if let Ok(feat) = txc.features() {
|
||||
|
@ -624,65 +523,22 @@ impl PcscBackend {
|
|||
}
|
||||
}
|
||||
|
||||
// Initialize CardTransaction (set CardCaps from ARD)
|
||||
<dyn CardTransaction>::initialize(&mut txc)?;
|
||||
|
||||
let cc = txc.card_caps().cloned();
|
||||
|
||||
drop(txc);
|
||||
|
||||
self.card_caps = cc;
|
||||
|
||||
for (a, b) in h {
|
||||
self.reader_caps.insert(a, b);
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn card_caps(&self) -> Option<CardCaps> {
|
||||
self.card_caps
|
||||
}
|
||||
fn reader_caps(&self) -> HashMap<u8, Tlv> {
|
||||
self.reader_caps.clone()
|
||||
}
|
||||
|
||||
/// This command will try to activate an OpenPGP card, if:
|
||||
/// - exactly one card is connected to the system
|
||||
/// - that card replies to SELECT with Status 6285
|
||||
///
|
||||
/// See OpenPGP card spec (version 3.4.1): 7.2.17 ACTIVATE FILE
|
||||
pub fn activate_terminated_card() -> Result<(), Error> {
|
||||
let mut cards =
|
||||
Self::raw_pcsc_cards(pcsc::ShareMode::Exclusive).map_err(Error::Smartcard)?;
|
||||
if cards.len() != 1 {
|
||||
return Err(Error::InternalError(format!(
|
||||
"This command is only allowed if exactly one card is connected, found {}.",
|
||||
cards.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let card = cards.pop().unwrap();
|
||||
|
||||
let mut backend = PcscBackend::new(card, pcsc::ShareMode::Exclusive);
|
||||
let mut card_tx = Box::new(PcscTransaction::new(&mut backend, false)?);
|
||||
|
||||
match <dyn CardTransaction>::select(&mut card_tx) {
|
||||
Err(Error::CardStatus(openpgp_card::StatusBytes::TerminationState)) => {
|
||||
let _ = <dyn CardTransaction>::activate_file(&mut card_tx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
_ => Err(Error::InternalError(
|
||||
"Card doesn't appear to be terminated.".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CardBackend for PcscBackend {
|
||||
/// Get a TxClient for this PcscCard (this starts a transaction)
|
||||
fn transaction(&mut self) -> Result<Box<dyn CardTransaction + Send + Sync + '_>, Error> {
|
||||
Ok(Box::new(PcscTransaction::new(self, true)?))
|
||||
/// Get a CardTransaction for this PcscBackend (this starts a transaction)
|
||||
fn transaction(
|
||||
&mut self,
|
||||
reselect_application: Option<&[u8]>,
|
||||
) -> Result<Box<dyn CardTransaction + Send + Sync + '_>, SmartcardError> {
|
||||
Ok(Box::new(PcscTransaction::new(self, reselect_application)?))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "openpgp-card-scdc"
|
||||
description = "Experimental SCDaemon Client, for use with the openpgp-card crate"
|
||||
name = "card-backend-scdc"
|
||||
description = "Experimental SCDaemon Client, e.g. for use with the openpgp-card crate"
|
||||
authors = ["Heiko Schaefer <heiko@schaefer.name>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
version = "0.2.2"
|
||||
version = "0.4.0"
|
||||
edition = "2018"
|
||||
repository = "https://gitlab.com/openpgp-card/openpgp-card"
|
||||
documentation = "https://docs.rs/crate/openpgp-card-scdc"
|
||||
|
||||
[dependencies]
|
||||
openpgp-card = { path = "../openpgp-card", version = "0.3" }
|
||||
sequoia-ipc = "0.27"
|
||||
card-backend = { path = "../card-backend", version = "0.1" }
|
||||
sequoia-ipc = "0.29"
|
||||
hex = "0.4"
|
||||
futures = "0.3"
|
||||
tokio = { version = "1.13.1", features = ["rt-multi-thread"] }
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
**scdaemon client for the openpgp-card library**
|
||||
# scdaemon based backend (e.g., for the openpgp-card library)
|
||||
|
||||
This crate provides `ScdBackend`/`ScdTransaction`, which is an implementation of the
|
||||
`CardBackend`/`CardTransaction` traits that uses an instance of GnuPG's
|
||||
This crate provides `ScdBackend`/`ScdTransaction`, which is an implementation
|
||||
of the `CardBackend`/`CardTransaction` traits that uses an instance of GnuPG's
|
||||
[scdaemon](https://www.gnupg.org/documentation/manuals/gnupg/Invoking-SCDAEMON.html)
|
||||
to access OpenPGP cards.
|
||||
|
||||
Note that (unlike `openpgp-card-pcsc`), this backend doesn't implement transaction guarantees.
|
||||
Note that (unlike `card-backend-pcsc`), this backend doesn't implement
|
||||
transaction guarantees.
|
||||
|
||||
**Known limitations**
|
||||
## Known limitations
|
||||
|
||||
- Uploading RSA 4096 keys via `scdaemon` doesn't work with cards that don't
|
||||
support Command Chaining (e.g. the "Floss Shop OpenPGP Smart Card").
|
||||
|
@ -24,14 +25,14 @@ Note that (unlike `openpgp-card-pcsc`), this backend doesn't implement transacti
|
|||
OpenPGP card operations fit into this constraint).
|
||||
|
||||
- When using `scdaemon` via pcsc (by configuring `scdaemon` with
|
||||
`disable-ccid`), choosing a specific card of multiple plugged in OpenPGP
|
||||
`disable-ccid`), choosing a specific card of multiple plugged-in OpenPGP
|
||||
cards seems to be broken.
|
||||
So you probably want to plug in only one OpenPGP card at a time when using
|
||||
`openpgp-card-scdc` combined with `disable-ccid`.
|
||||
|
||||
- When using `scdaemon` via its default `ccid` driver, choosing a
|
||||
specific one of multiple plugged in OpenPGP cards seems to only work up
|
||||
to 4 plugged in cards.
|
||||
specific one of multiple plugged-in OpenPGP cards seems to only work up
|
||||
to 4 plugged-in cards.
|
||||
So you probably want to plug in at most four OpenPGP cards at a time when
|
||||
using `openpgp-card-scdc` with its ccid driver.
|
||||
using `card-backend-scdc` with its ccid driver.
|
||||
(This limit has been raised in GnuPG 2.3.x)
|
||||
|
|
214
scdc/src/lib.rs
214
scdc/src/lib.rs
|
@ -1,17 +1,17 @@
|
|||
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
//! This crate implements the experimental `ScdBackend`/`ScdTransaction` backend for the
|
||||
//! `openpgp-card` crate.
|
||||
//! It uses GnuPG's scdaemon (via GnuPG Agent) to access OpenPGP cards.
|
||||
//! It uses GnuPG's scdaemon (via GnuPG Agent) to access smart cards (including OpenPGP cards).
|
||||
//!
|
||||
//! Note that (unlike `openpgp-card-pcsc`), this backend doesn't implement transaction guarantees.
|
||||
//! Note that (unlike `card-backend-pcsc`), this backend doesn't implement transaction guarantees.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use card_backend::{CardBackend, CardCaps, CardTransaction, PinType, SmartcardError};
|
||||
use futures::StreamExt;
|
||||
use lazy_static::lazy_static;
|
||||
use openpgp_card::{CardBackend, CardCaps, CardTransaction, Error, PinType, SmartcardError};
|
||||
use sequoia_ipc::assuan::Response;
|
||||
use sequoia_ipc::gnupg::{Agent, Context};
|
||||
use tokio::runtime::Runtime;
|
||||
|
@ -33,36 +33,44 @@ lazy_static! {
|
|||
/// communication within GnuPG? Are \r\n added?)
|
||||
const ASSUAN_LINELENGTH: usize = 1000;
|
||||
|
||||
/// The maximum number of bytes for a command that we will send to
|
||||
/// The maximum number of bytes for a command that we can send to
|
||||
/// scdaemon (via Assuan).
|
||||
///
|
||||
/// Each command byte gets sent via Assuan as a two-character hex string.
|
||||
///
|
||||
/// 22 characters are used to send "SCD APDU --exlen=abcd "
|
||||
/// (So, as a defensive limit, 25 characters are subtracted).
|
||||
/// 17 characters are used to send "SCD APDU --exlen "
|
||||
///
|
||||
/// In concrete terms, this limit means that some commands (with big
|
||||
/// parameters) cannot be sent to the card, when the card doesn't support
|
||||
/// command chaining (like the floss-shop "OpenPGP Smart Card 3.4").
|
||||
///
|
||||
/// In particular, uploading rsa4096 keys fails via scdaemon, with such cards.
|
||||
const APDU_CMD_BYTES_MAX: usize = (ASSUAN_LINELENGTH - 25) / 2;
|
||||
///
|
||||
/// The value of "36" was experimentally determined:
|
||||
/// This value results in the biggest APDU_CMD_BYTES_MAX that still allows
|
||||
/// uploading an RSA4k key onto a YubiKey 5.
|
||||
const APDU_CMD_BYTES_MAX: usize = (ASSUAN_LINELENGTH - 36) / 2;
|
||||
|
||||
/// An implementation of the CardBackend trait that uses GnuPG's scdaemon
|
||||
/// (via GnuPG Agent) to access OpenPGP card devices.
|
||||
/// (via GnuPG Agent) to access smart cards.
|
||||
pub struct ScdBackend {
|
||||
agent: Agent,
|
||||
card_caps: Option<CardCaps>,
|
||||
}
|
||||
|
||||
/// Boxing helper (for easier consumption of PcscBackend in openpgp_card and openpgp_card_sequoia)
|
||||
impl From<ScdBackend> for Box<dyn CardBackend + Sync + Send> {
|
||||
fn from(backend: ScdBackend) -> Box<dyn CardBackend + Sync + Send> {
|
||||
Box::new(backend)
|
||||
}
|
||||
}
|
||||
|
||||
impl ScdBackend {
|
||||
/// Open a CardApp that uses an scdaemon instance as its backend.
|
||||
/// The specific card with AID `serial` is requested from scdaemon.
|
||||
pub fn open_by_serial(agent: Option<Agent>, serial: &str) -> Result<Self, Error> {
|
||||
/// Request card with AID `serial` from scdaemon, and return it as a ScdBackend.
|
||||
///
|
||||
/// The client may provide a GnuPG `agent` to use.
|
||||
pub fn open_by_serial(agent: Option<Agent>, serial: &str) -> Result<Self, SmartcardError> {
|
||||
let mut card = ScdBackend::new(agent, true)?;
|
||||
card.select_card(serial)?;
|
||||
|
||||
card.transaction()?.initialize()?;
|
||||
card.select_by_serial(serial)?;
|
||||
|
||||
Ok(card)
|
||||
}
|
||||
|
@ -72,22 +80,21 @@ impl ScdBackend {
|
|||
/// If multiple cards are available, scdaemon implicitly selects one.
|
||||
///
|
||||
/// (NOTE: implicitly picking an unspecified card might be a bad idea.
|
||||
/// You might want to avoid using this function.)
|
||||
pub fn open_yolo(agent: Option<Agent>) -> Result<Self, Error> {
|
||||
let mut card = ScdBackend::new(agent, true)?;
|
||||
|
||||
card.transaction()?.initialize()?;
|
||||
/// You might want to avoid using this function, or check which card
|
||||
/// you received.)
|
||||
pub fn open_yolo(agent: Option<Agent>) -> Result<Self, SmartcardError> {
|
||||
let card = ScdBackend::new(agent, true)?;
|
||||
|
||||
Ok(card)
|
||||
}
|
||||
|
||||
/// Helper fn that shuts down scdaemon via GnuPG Agent.
|
||||
/// This may be useful to obtain access to a Smard card via PCSC.
|
||||
pub fn shutdown_scd(agent: Option<Agent>) -> Result<(), Error> {
|
||||
pub fn shutdown_scd(agent: Option<Agent>) -> Result<(), SmartcardError> {
|
||||
let mut scdc = Self::new(agent, false)?;
|
||||
|
||||
scdc.send("SCD RESTART")?;
|
||||
scdc.send("SCD BYE")?;
|
||||
scdc.execute("SCD RESTART")?;
|
||||
scdc.execute("SCD BYE")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -97,26 +104,18 @@ impl ScdBackend {
|
|||
///
|
||||
/// If `agent` is None, a Context with the default GnuPG home directory
|
||||
/// is used.
|
||||
fn new(agent: Option<Agent>, init: bool) -> Result<Self, Error> {
|
||||
let agent = if let Some(agent) = agent {
|
||||
agent
|
||||
} else {
|
||||
// Create and use a new Agent based on a default Context
|
||||
let ctx = Context::new().map_err(|e| {
|
||||
Error::Smartcard(SmartcardError::Error(format!("Context::new failed {e}")))
|
||||
})?;
|
||||
RT.lock()
|
||||
.unwrap()
|
||||
.block_on(Agent::connect(&ctx))
|
||||
.map_err(|e| {
|
||||
Error::Smartcard(SmartcardError::Error(format!("Agent::connect failed {e}")))
|
||||
})?
|
||||
};
|
||||
fn new(agent: Option<Agent>, init: bool) -> Result<Self, SmartcardError> {
|
||||
let agent = agent.unwrap_or({
|
||||
let rt = RT.lock().unwrap();
|
||||
|
||||
let mut scdc = Self {
|
||||
agent,
|
||||
card_caps: None,
|
||||
};
|
||||
// Create and use a new Agent based on a default Context
|
||||
let ctx = Context::new()
|
||||
.map_err(|e| SmartcardError::Error(format!("Context::new failed {e}")))?;
|
||||
rt.block_on(Agent::connect(&ctx))
|
||||
.map_err(|e| SmartcardError::Error(format!("Agent::connect failed {e}")))?
|
||||
});
|
||||
|
||||
let mut scdc = Self { agent };
|
||||
|
||||
if init {
|
||||
scdc.serialno()?;
|
||||
|
@ -125,24 +124,23 @@ impl ScdBackend {
|
|||
Ok(scdc)
|
||||
}
|
||||
|
||||
fn send2(&mut self, cmd: &str) -> Result<(), Error> {
|
||||
self.agent.send(cmd).map_err(|e| {
|
||||
Error::Smartcard(SmartcardError::Error(format!(
|
||||
"scdc agent send failed: {e}"
|
||||
)))
|
||||
})
|
||||
/// Just send a command, without looking at the results at all
|
||||
fn send_cmd(&mut self, cmd: &str) -> Result<(), SmartcardError> {
|
||||
self.agent
|
||||
.send(cmd)
|
||||
.map_err(|e| SmartcardError::Error(format!("scdc agent send failed: {e}")))
|
||||
}
|
||||
|
||||
/// Call "SCD SERIALNO", which causes scdaemon to be started by gpg
|
||||
/// agent (if it's not running yet).
|
||||
fn serialno(&mut self) -> Result<(), Error> {
|
||||
/// Call "SCD SERIALNO", which causes scdaemon to be started by gpg-agent
|
||||
/// (if it's not running yet).
|
||||
fn serialno(&mut self) -> Result<(), SmartcardError> {
|
||||
let rt = RT.lock().unwrap();
|
||||
|
||||
let send = "SCD SERIALNO";
|
||||
self.send2(send)?;
|
||||
let cmd = "SCD SERIALNO";
|
||||
self.send_cmd(cmd)?;
|
||||
|
||||
while let Some(response) = rt.block_on(self.agent.next()) {
|
||||
log::trace!("init res: {:x?}", response);
|
||||
log::trace!("SCD SERIALNO res: {:x?}", response);
|
||||
|
||||
if let Ok(Response::Status { .. }) = response {
|
||||
// drop remaining lines
|
||||
|
@ -154,26 +152,22 @@ impl ScdBackend {
|
|||
}
|
||||
}
|
||||
|
||||
Err(Error::Smartcard(SmartcardError::Error(
|
||||
"SCDC init() failed".into(),
|
||||
)))
|
||||
Err(SmartcardError::Error("SCDC init() failed".into()))
|
||||
}
|
||||
|
||||
/// Ask scdameon to switch to using a specific OpenPGP card, based on
|
||||
/// Ask scdaemon to switch to using a specific OpenPGP card, based on
|
||||
/// its `serial`.
|
||||
fn select_card(&mut self, serial: &str) -> Result<(), Error> {
|
||||
let send = format!("SCD SERIALNO --demand={serial}");
|
||||
self.send2(&send)?;
|
||||
|
||||
fn select_by_serial(&mut self, serial: &str) -> Result<(), SmartcardError> {
|
||||
let rt = RT.lock().unwrap();
|
||||
|
||||
let send = format!("SCD SERIALNO --demand={serial}");
|
||||
self.send_cmd(&send)?;
|
||||
|
||||
while let Some(response) = rt.block_on(self.agent.next()) {
|
||||
log::trace!("select res: {:x?}", response);
|
||||
|
||||
if response.is_err() {
|
||||
return Err(Error::Smartcard(SmartcardError::CardNotFound(
|
||||
serial.into(),
|
||||
)));
|
||||
return Err(SmartcardError::CardNotFound(serial.into()));
|
||||
}
|
||||
|
||||
if let Ok(Response::Status { .. }) = response {
|
||||
|
@ -186,21 +180,19 @@ impl ScdBackend {
|
|||
}
|
||||
}
|
||||
|
||||
Err(Error::Smartcard(SmartcardError::CardNotFound(
|
||||
serial.into(),
|
||||
)))
|
||||
Err(SmartcardError::CardNotFound(serial.into()))
|
||||
}
|
||||
|
||||
fn send(&mut self, cmd: &str) -> Result<(), Error> {
|
||||
self.send2(cmd)?;
|
||||
|
||||
fn execute(&mut self, cmd: &str) -> Result<(), SmartcardError> {
|
||||
let rt = RT.lock().unwrap();
|
||||
|
||||
self.send_cmd(cmd)?;
|
||||
|
||||
while let Some(response) = rt.block_on(self.agent.next()) {
|
||||
log::trace!("select res: {:x?}", response);
|
||||
|
||||
if let Err(e) = response {
|
||||
return Err(Error::Smartcard(SmartcardError::Error(format!("{e:?}"))));
|
||||
return Err(SmartcardError::Error(format!("{e:?}")));
|
||||
}
|
||||
|
||||
if response.is_ok() {
|
||||
|
@ -213,14 +205,17 @@ impl ScdBackend {
|
|||
}
|
||||
}
|
||||
|
||||
Err(Error::Smartcard(SmartcardError::Error(format!(
|
||||
Err(SmartcardError::Error(format!(
|
||||
"Error sending command {cmd}"
|
||||
))))
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl CardBackend for ScdBackend {
|
||||
fn transaction(&mut self) -> Result<Box<dyn CardTransaction + Send + Sync + '_>, Error> {
|
||||
fn transaction(
|
||||
&mut self,
|
||||
_reselect_application: Option<&[u8]>,
|
||||
) -> Result<Box<dyn CardTransaction + Send + Sync + '_>, SmartcardError> {
|
||||
Ok(Box::new(ScdTransaction { scd: self }))
|
||||
}
|
||||
}
|
||||
|
@ -230,41 +225,40 @@ pub struct ScdTransaction<'a> {
|
|||
}
|
||||
|
||||
impl CardTransaction for ScdTransaction<'_> {
|
||||
fn transmit(&mut self, cmd: &[u8], _: usize) -> Result<Vec<u8>, Error> {
|
||||
fn transmit(&mut self, cmd: &[u8], _: usize) -> Result<Vec<u8>, SmartcardError> {
|
||||
log::trace!("SCDC cmd len {}", cmd.len());
|
||||
|
||||
let hex = hex::encode(cmd);
|
||||
|
||||
// (Unwrap is ok here, not having a card_caps is fine)
|
||||
let ext = if self.card_caps().is_some() && self.card_caps().unwrap().ext_support() {
|
||||
// If we know about card_caps, and can do extended length we
|
||||
// set "exlen" accordingly ...
|
||||
format!("--exlen={} ", self.card_caps().unwrap().max_rsp_bytes())
|
||||
} else {
|
||||
// ... otherwise don't send "exlen" to scdaemon
|
||||
"".to_string()
|
||||
};
|
||||
// Always set "--exlen" (without explicit parameter for length)
|
||||
//
|
||||
// FIXME: Does this ever cause problems?
|
||||
//
|
||||
// Hypothesis: this should be ok.
|
||||
// Allowing too big of a return value should not do any effective harm
|
||||
// (just maybe cause some slightly too large memory allocations).
|
||||
let ext = "--exlen ".to_string();
|
||||
|
||||
let send = format!("SCD APDU {ext}{hex}\n");
|
||||
let send = format!("SCD APDU {ext}{hex}");
|
||||
log::trace!("SCDC command: '{}'", send);
|
||||
|
||||
if send.len() > ASSUAN_LINELENGTH {
|
||||
return Err(Error::Smartcard(SmartcardError::Error(format!(
|
||||
return Err(SmartcardError::Error(format!(
|
||||
"APDU command is too long ({}) to send via Assuan",
|
||||
send.len()
|
||||
))));
|
||||
)));
|
||||
}
|
||||
|
||||
self.scd.send2(&send)?;
|
||||
|
||||
let rt = RT.lock().unwrap();
|
||||
|
||||
self.scd.send_cmd(&send)?;
|
||||
|
||||
while let Some(response) = rt.block_on(self.scd.agent.next()) {
|
||||
log::trace!("res: {:x?}", response);
|
||||
log::trace!("transmit res: {:x?}", response);
|
||||
if response.is_err() {
|
||||
return Err(Error::Smartcard(SmartcardError::Error(format!(
|
||||
return Err(SmartcardError::Error(format!(
|
||||
"Unexpected error response from SCD {response:?}"
|
||||
))));
|
||||
)));
|
||||
}
|
||||
|
||||
if let Ok(Response::Data { partial }) = response {
|
||||
|
@ -279,17 +273,7 @@ impl CardTransaction for ScdTransaction<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
Err(Error::Smartcard(SmartcardError::Error(
|
||||
"no response found".into(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn init_card_caps(&mut self, caps: CardCaps) {
|
||||
self.scd.card_caps = Some(caps);
|
||||
}
|
||||
|
||||
fn card_caps(&self) -> Option<&CardCaps> {
|
||||
self.scd.card_caps.as_ref()
|
||||
Err(SmartcardError::Error("no response found".into()))
|
||||
}
|
||||
|
||||
/// Return limit for APDU command size via scdaemon (based on Assuan
|
||||
|
@ -298,23 +282,31 @@ impl CardTransaction for ScdTransaction<'_> {
|
|||
Some(APDU_CMD_BYTES_MAX)
|
||||
}
|
||||
|
||||
/// FIXME: not implemented yet
|
||||
/// Not implemented yet
|
||||
fn feature_pinpad_verify(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// FIXME: not implemented yet
|
||||
/// Not implemented yet
|
||||
fn feature_pinpad_modify(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// FIXME: not implemented yet
|
||||
fn pinpad_verify(&mut self, _id: PinType) -> Result<Vec<u8>, Error> {
|
||||
/// Not implemented yet
|
||||
fn pinpad_verify(
|
||||
&mut self,
|
||||
_id: PinType,
|
||||
_card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// FIXME: not implemented yet
|
||||
fn pinpad_modify(&mut self, _id: PinType) -> Result<Vec<u8>, Error> {
|
||||
/// Not implemented yet
|
||||
fn pinpad_modify(
|
||||
&mut self,
|
||||
_id: PinType,
|
||||
_card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue