openpgp-card/pcsc/src/lib.rs
Heiko Schaefer 31eee9e738
backend: add CardBackend::limit_card_caps
This mechanism allows the pcsc backend to signal to openpgp-card that a reader doesn't support "extended length".
2023-09-06 01:16:32 +02:00

575 lines
20 KiB
Rust

// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
//! 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 pcsc::Disposition;
const FEATURE_VERIFY_PIN_DIRECT: u8 = 0x06;
const FEATURE_MODIFY_PIN_DIRECT: u8 = 0x07;
/// An opened PCSC Card (without open transaction).
/// 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
/// [PcscTransaction] object needs to be obtained (via [PcscBackend::transaction]).
pub struct PcscBackend {
card: pcsc::Card,
mode: pcsc::ShareMode,
reader_caps: HashMap<u8, Tlv>,
// The reader name could be used as a hint about capabilities
// (e.g. readers that don't support extended length)
#[allow(dead_code)]
reader_name: String,
// FIXME: add a "adjust_card_caps" fn to card-backend? (could replace `max_cmd_len`)
}
/// Boxing helper (for easier consumption of PcscBackend in openpgp_card and openpgp_card_sequoia)
impl From<PcscBackend> for Box<dyn CardBackend + Sync + Send> {
fn from(backend: PcscBackend) -> Box<dyn CardBackend + Sync + Send> {
Box::new(backend)
}
}
/// An implementation of the CardTransaction trait that uses the PCSC lite
/// middleware to access the OpenPGP card application on smart cards, via a
/// PCSC "transaction".
///
/// This struct is created from a PcscCard by opening a transaction, using
/// PcscCard::transaction().
///
/// Transactions on a card cannot be opened and left idle
/// (e.g. Microsoft documents that on Windows, they will be closed after
/// 5s without a command:
/// <https://docs.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardbegintransaction?redirectedfrom=MSDN#remarks>)
pub struct PcscTransaction<'b> {
tx: pcsc::Transaction<'b>,
reader_caps: HashMap<u8, Tlv>, // FIXME: gets manually cloned
was_reset: bool,
}
impl<'b> PcscTransaction<'b> {
/// Start a transaction on `card`.
///
/// 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 mode = card.mode;
let reader_caps = card.reader_caps.clone();
let mut c = &mut card.card;
loop {
match c.transaction2() {
Ok(tx) => {
// A pcsc transaction has been successfully started
let mut pt = Self {
tx,
reader_caps,
was_reset: false,
};
if was_reset {
log::trace!("Card was reset");
pt.was_reset = true;
// 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");
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");
}
}
break Ok(pt);
}
Err((c_, pcsc::Error::ResetCard)) => {
// Card was reset, need to reconnect
was_reset = true;
c = c_;
log::trace!("start_tx: do reconnect");
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, in the next loop run
}
Err((_, e)) => {
log::trace!("start_tx: error {:?}", e);
break Err(SmartcardError::Error(format!("Error: {e:?}")));
}
};
}
}
/// GET_FEATURE_REQUEST
/// (see http://pcscworkgroup.com/Download/Specifications/pcsc10_v2.02.09.pdf)
fn features(&mut self) -> Result<Vec<Tlv>, SmartcardError> {
let mut recv = vec![0; 1024];
let cm_ioctl_get_feature_request = pcsc::ctl_code(3400);
let res = self
.tx
.control(cm_ioctl_get_feature_request, &[], &mut recv)
.map_err(|e| {
SmartcardError::Error(format!("GET_FEATURE_REQUEST control call failed: {e:?}"))
})?;
Ok(Tlv::parse_all(res))
}
/// Get the minimum pin length for pin_id.
fn min_pin_len(&self, pin: PinType) -> u8 {
match pin {
PinType::User | PinType::Sign => 6,
PinType::Admin => 8,
}
}
/// Get the maximum pin length for pin_id.
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(SmartcardError::Error("card_caps is None".into()))
}
}
}
impl CardTransaction for PcscTransaction<'_> {
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 => SmartcardError::NotTransacted,
_ => SmartcardError::Error(format!("Transmit failed: {e:?}")),
})?;
Ok(resp.to_vec())
}
fn feature_pinpad_verify(&self) -> bool {
self.reader_caps.contains_key(&FEATURE_VERIFY_PIN_DIRECT)
}
fn feature_pinpad_modify(&self) -> bool {
self.reader_caps.contains_key(&FEATURE_MODIFY_PIN_DIRECT)
}
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, card_caps)?;
// Default to varlen, for now.
// (NOTE: Some readers don't support varlen, and need explicit length
// information. Also see https://wiki.gnupg.org/CardReader/PinpadInput)
let fixedlen: u8 = 0;
// APDU: 00 20 00 pin_id <len> (ff)*
let mut ab_data = vec![
0x00, /* CLA */
0x20, /* INS: VERIFY */
0x00, /* P1 */
pin.id(), /* P2 */
fixedlen, /* Lc: 'fixedlen' data bytes */
];
ab_data.extend([0xff].repeat(fixedlen as usize));
// PC/SC v2.02.05 Part 10 PIN verification data structure
let mut send: Vec<u8> = vec![
// 0 bTimeOut BYTE timeout in seconds (00 means use default
// timeout)
0x00,
// 1 bTimeOut2 BYTE timeout in seconds after first key stroke
0x00,
// 2 bmFormatString BYTE formatting options USB_CCID_PIN_FORMAT_xxx
0x82,
// 3 bmPINBlockString BYTE
// bits 7-4 bit size of PIN length in APDU
// bits 3-0 PIN block size in bytes after justification and formatting
fixedlen,
// 4 bmPINLengthFormat BYTE
// bits 7-5 RFU, bit 4 set if system units are bytes clear if
// system units are bits,
// bits 3-0 PIN length position in system units
0x00,
// 5 wPINMaxExtraDigit USHORT XXYY, where XX is minimum PIN size
// in digits, YY is maximum
pin_max_size,
pin_min_size,
// 7 bEntryValidationCondition BYTE Conditions under which PIN
// entry should be considered complete.
//
// table for bEntryValidationCondition:
// 0x01: Max size reached
// 0x02: Validation key pressed
// 0x04: Timeout occurred
0x07,
// 8 bNumberMessage BYTE Number of messages to display for PIN
// verification
0x01,
// 9 wLangIdU SHORT Language for messages
0x04,
0x09, // US english
// 11 bMsgIndex BYTE Message index (should be 00)
0x00,
// 12 bTeoPrologue BYTE[3] T=1 I-block prologue field to use (fill with 00)
0x00,
0x00,
0x00,
];
// 15 ulDataLength ULONG length of Data to be sent to the ICC
send.extend((ab_data.len() as u32).to_le_bytes());
// 19 abData BYTE[] Data to send to the ICC
send.extend(ab_data);
log::trace!("pcsc pinpad_verify send: {:x?}", send);
let mut recv = vec![0xAA; 256];
let verify_ioctl: [u8; 4] = self
.reader_caps
.get(&FEATURE_VERIFY_PIN_DIRECT)
.ok_or_else(|| SmartcardError::Error("no reader_capability".into()))?
.value()
.try_into()
.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| 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,
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, card_caps)?;
// Default to varlen, for now.
// (NOTE: Some readers don't support varlen, and need explicit length
// information. Also see https://wiki.gnupg.org/CardReader/PinpadInput)
let fixedlen: u8 = 0;
// APDU: 00 24 00 pin_id <len> [(ff)* x2]
let mut ab_data = vec![
0x00, /* CLA */
0x24, /* INS: CHANGE_REFERENCE_DATA */
0x00, /* P1 */
pin.id(), /* P2 */
fixedlen * 2, /* Lc: 'fixedlen' data bytes */
];
ab_data.extend([0xff].repeat(fixedlen as usize * 2));
// PC/SC v2.02.05 Part 10 PIN modification data structure
let mut send: Vec<u8> = vec![
// 0 bTimeOut BYTE timeout in seconds (00 means use default
// timeout)
0x00,
// 1 bTimeOut2 BYTE timeout in seconds after first key stroke
0x00,
// 2 bmFormatString BYTE formatting options USB_CCID_PIN_FORMAT_xxx
0x82,
// 3 bmPINBlockString BYTE
// bits 7-4 bit size of PIN length in APDU
// bits 3-0 PIN block size in bytes after justification and formatting
fixedlen,
// 4 bmPINLengthFormat BYTE
// bits 7-5 RFU, bit 4 set if system units are bytes clear if
// system units are bits,
// bits 3-0 PIN length position in system units
0x00,
// 5 bInsertionOffsetOld BYTE Insertion position offset in bytes for
// the current PIN
0x00,
// 6 bInsertionOffsetNew BYTE Insertion position offset in bytes for
// the new PIN
fixedlen,
// 7 wPINMaxExtraDigit USHORT XXYY, where XX is minimum PIN size
// in digits, YY is maximum
pin_max_size,
pin_min_size,
// 9 bConfirmPIN
0x03, // TODO check?
// 10 bEntryValidationCondition BYTE Conditions under which PIN
// entry should be considered complete.
//
// table for bEntryValidationCondition:
// 0x01: Max size reached
// 0x02: Validation key pressed
// 0x04: Timeout occurred
0x07,
// 11 bNumberMessage BYTE Number of messages to display for PIN
// verification
0x03, // TODO check? (match with bConfirmPIN?)
// 12 wLangId USHORT Language for messages
0x04,
0x09, // US english
// 14 bMsgIndex1-3
0x00,
0x01,
0x02,
// 17 bTeoPrologue BYTE[3] T=1 I-block prologue field to use (fill with 00)
0x00,
0x00,
0x00,
];
// 15 ulDataLength ULONG length of Data to be sent to the ICC
send.extend((ab_data.len() as u32).to_le_bytes());
// 19 abData BYTE[] Data to send to the ICC
send.extend(ab_data);
log::trace!("pcsc pinpad_modify send: {:x?}", send);
let mut recv = vec![0xAA; 256];
let modify_ioctl: [u8; 4] = self
.reader_caps
.get(&FEATURE_MODIFY_PIN_DIRECT)
.ok_or_else(|| SmartcardError::Error("no reader_capability".into()))?
.value()
.try_into()
.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| SmartcardError::Error(format!("pcsc Error: {e:?}")))?;
log::trace!(" <- pcsc pinpad_modify result: {:x?}", res);
Ok(res.to_vec())
}
fn was_reset(&self) -> bool {
self.was_reset
}
}
impl PcscBackend {
/// A list of "raw" opened PCSC Cards and reader names
/// (No application is selected)
fn raw_pcsc_cards(mode: pcsc::ShareMode) -> Result<Vec<(pcsc::Card, String)>, SmartcardError> {
log::trace!("raw_pcsc_cards start");
let ctx = match pcsc::Context::establish(pcsc::Scope::User) {
Ok(ctx) => ctx,
Err(err) => {
log::trace!("Context::establish failed: {:?}", err);
return Err(SmartcardError::ContextError(err.to_string()));
}
};
log::trace!("raw_pcsc_cards got context");
// List available readers.
let mut readers_buf = [0; 2048];
let readers = match ctx.list_readers(&mut readers_buf) {
Ok(readers) => readers,
Err(err) => {
log::trace!("list_readers failed: {:?}", err);
return Err(SmartcardError::ReaderError(err.to_string()));
}
};
log::trace!(" readers: {:?}", readers);
let mut cards = vec![];
// Find a reader with a SmartCard.
for reader in readers {
let name = String::from_utf8_lossy(reader.to_bytes());
log::trace!("Checking reader: {:?}", name);
// Try connecting to card in this reader
let card = match ctx.connect(reader, mode, pcsc::Protocols::ANY) {
Ok(card) => card,
Err(pcsc::Error::NoSmartcard) => {
log::trace!("No Smartcard");
continue; // try next reader
}
Err(err) => {
log::warn!("Error connecting to card in reader: {:x?}", err);
continue;
}
};
log::trace!("Found card");
cards.push((card, name.to_string()));
}
Ok(cards)
}
/// Returns an Iterator over Smart Cards that are accessible via PCSC.
///
/// 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: card.0,
mode,
reader_caps: Default::default(),
reader_name: card.1,
};
backend.initialize_card()
}))
}
/// 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)?;
Ok(cards.map(|c| match c {
Ok(c) => Ok(Box::new(c) as Box<dyn CardBackend + Send + Sync>),
Err(e) => Err(e),
}))
}
/// 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, None)?;
// Get Features from reader (pinpad verify/modify)
if let Ok(feat) = txc.features() {
for tlv in feat {
log::trace!(" Found reader feature {:?}", tlv);
h.insert(tlv.tag().into(), tlv);
}
}
drop(txc);
for (a, b) in h {
self.reader_caps.insert(a, b);
}
Ok(self)
}
}
impl CardBackend for PcscBackend {
fn limit_card_caps(&self, card_caps: CardCaps) -> CardCaps {
let mut ext = card_caps.ext_support();
// Disable "extended length" support when the card reader is known not to support it
if self.reader_name.starts_with("ACS ACR122U") {
log::debug!("Disabling ext_support for reader {}", self.reader_name);
ext = false;
}
CardCaps::new(
ext,
card_caps.chaining_support(),
card_caps.max_cmd_bytes(),
card_caps.max_rsp_bytes(),
card_caps.pw1_max_len(),
card_caps.pw3_max_len(),
)
}
/// 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)?))
}
}