diff --git a/Cargo.toml b/Cargo.toml index 32c40c4..99dcdf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ -# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # 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", ] diff --git a/card-backend/Cargo.toml b/card-backend/Cargo.toml new file mode 100644 index 0000000..17f9266 --- /dev/null +++ b/card-backend/Cargo.toml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Heiko Schaefer +# 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 "] +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" \ No newline at end of file diff --git a/card-backend/README.md b/card-backend/README.md new file mode 100644 index 0000000..69a9591 --- /dev/null +++ b/card-backend/README.md @@ -0,0 +1,12 @@ + + +# 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. diff --git a/card-backend/src/lib.rs b/card-backend/src/lib.rs new file mode 100644 index 0000000..dc90499 --- /dev/null +++ b/card-backend/src/lib.rs @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer +// 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, 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, SmartcardError>; + + /// Select `application` on the card + fn select(&mut self, application: &[u8]) -> Result, 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 { + 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, + ) -> Result, SmartcardError>; + + /// Modify the PIN `pin` via the reader pinpad + fn pinpad_modify( + &mut self, + pin: PinType, + card_caps: &Option, + ) -> Result, 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), +} diff --git a/openpgp-card-sequoia/Cargo.toml b/openpgp-card-sequoia/Cargo.toml index 2d4970c..1936c47 100644 --- a/openpgp-card-sequoia/Cargo.toml +++ b/openpgp-card-sequoia/Cargo.toml @@ -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" diff --git a/openpgp-card/Cargo.toml b/openpgp-card/Cargo.toml index d6dcef6..4b2a448 100644 --- a/openpgp-card/Cargo.toml +++ b/openpgp-card/Cargo.toml @@ -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 "] 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" diff --git a/openpgp-card/README.md b/openpgp-card/README.md index 173946a..7e3a777 100644 --- a/openpgp-card/README.md +++ b/openpgp-card/README.md @@ -1,5 +1,5 @@ @@ -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** diff --git a/openpgp-card/src/apdu.rs b/openpgp-card/src/apdu.rs index 41eb698..6fc9d9d 100644 --- a/openpgp-card/src/apdu.rs +++ b/openpgp-card/src/apdu.rs @@ -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( card_tx: &mut C, cmd: Command, + card_caps: Option, expect_reply: bool, ) -> Result 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( card_tx: &mut C, cmd: Command, + card_caps: Option, expect_response: Expect, ) -> Result, 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!"); diff --git a/openpgp-card/src/apdu/commands.rs b/openpgp-card/src/apdu/commands.rs index c59fd99..cb20ccf 100644 --- a/openpgp-card/src/apdu/commands.rs +++ b/openpgp-card/src/apdu/commands.rs @@ -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 diff --git a/openpgp-card/src/errors.rs b/openpgp-card/src/errors.rs index 76c9c0f..02d93c4 100644 --- a/openpgp-card/src/errors.rs +++ b/openpgp-card/src/errors.rs @@ -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 for Error { } } +impl From 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), -} diff --git a/openpgp-card/src/keys.rs b/openpgp-card/src/keys.rs index b283428..9e77277 100644 --- a/openpgp-card/src/keys.rs +++ b/openpgp-card/src/keys.rs @@ -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)?; diff --git a/openpgp-card/src/lib.rs b/openpgp-card/src/lib.rs index 07e8146..18904fb 100644 --- a/openpgp-card/src/lib.rs +++ b/openpgp-card/src/lib.rs @@ -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, 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, 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 { - 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, Error>; - - /// Modify the PIN `id` via the reader pinpad - fn pinpad_modify(&mut self, pin: PinType) -> Result, Error>; - - /// Select the OpenPGP card application - fn select(&mut self) -> Result, 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, 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 { - 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 for Vec { } } -/// 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] diff --git a/openpgp-card/src/openpgp.rs b/openpgp-card/src/openpgp.rs index 1fcfd3f..5cdf73c 100644 --- a/openpgp-card/src/openpgp.rs +++ b/openpgp-card/src/openpgp.rs @@ -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, + card_caps: Option, } impl OpenPgp { - pub fn new(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(backend: B) -> Result where B: Into>, { - Self { - card: backend.into(), - } + let card: Box = 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 { - 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, + card_caps: &'a mut Option, } 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 { + apdu::send_command(&mut *self.tx, cmd, *self.card_caps, expect_reply) + } + + // SELECT + + /// Select the OpenPGP card application + pub fn select(&mut self) -> Result, 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 { 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, 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, 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, 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, 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, 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, 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 diff --git a/pcsc/Cargo.toml b/pcsc/Cargo.toml index aed8160..e6a69c9 100644 --- a/pcsc/Cargo.toml +++ b/pcsc/Cargo.toml @@ -1,18 +1,18 @@ -# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # 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 "] 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" diff --git a/pcsc/README.md b/pcsc/README.md index 436cc05..82e93bc 100644 --- a/pcsc/README.md +++ b/pcsc/README.md @@ -1,17 +1,19 @@ -# 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. diff --git a/pcsc/src/lib.rs b/pcsc/src/lib.rs index b7f52f8..ace040a 100644 --- a/pcsc/src/lib.rs +++ b/pcsc/src/lib.rs @@ -1,38 +1,30 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // 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 { - 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, reader_caps: HashMap, } @@ -56,118 +48,99 @@ impl From for Box { /// ) pub struct PcscTransaction<'b> { tx: pcsc::Transaction<'b>, - card_caps: Option, // FIXME: manual copy from PcscCard - reader_caps: HashMap, // FIXME: manual copy from PcscCard + reader_caps: HashMap, // 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 { - 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 { + 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 ::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 { - ::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, Error> { + fn features(&mut self) -> Result, 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 { - if let Some(card_caps) = self.card_caps { + fn max_pin_len( + &self, + pin: PinType, + card_caps: &Option, + ) -> Result { + 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, Error> { + fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result, 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, Error> { + fn pinpad_verify( + &mut self, + pin: PinType, + card_caps: &Option, + ) -> Result, 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, Error> { + fn pinpad_modify( + &mut self, + pin: PinType, + card_caps: &Option, + ) -> Result, 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, 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, Error> { - let mut cards: Vec = 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) -> Result, 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, + ) -> Result>, 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) -> Result { - 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, + ) -> Result< + impl Iterator, 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), + 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 { + /// 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 { log::trace!("pcsc initialize_card"); let mut h: HashMap = 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) - ::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 { - self.card_caps - } - fn reader_caps(&self) -> HashMap { - 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 ::select(&mut card_tx) { - Err(Error::CardStatus(openpgp_card::StatusBytes::TerminationState)) => { - let _ = ::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, 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, SmartcardError> { + Ok(Box::new(PcscTransaction::new(self, reselect_application)?)) } } diff --git a/scdc/Cargo.toml b/scdc/Cargo.toml index c395a37..38b5ccd 100644 --- a/scdc/Cargo.toml +++ b/scdc/Cargo.toml @@ -1,19 +1,19 @@ -# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # 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 "] 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"] } diff --git a/scdc/README.md b/scdc/README.md index 420eea1..b204a7b 100644 --- a/scdc/README.md +++ b/scdc/README.md @@ -1,18 +1,19 @@ -**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) diff --git a/scdc/src/lib.rs b/scdc/src/lib.rs index a8914d3..f728213 100644 --- a/scdc/src/lib.rs +++ b/scdc/src/lib.rs @@ -1,17 +1,17 @@ -// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // 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, +} + +/// Boxing helper (for easier consumption of PcscBackend in openpgp_card and openpgp_card_sequoia) +impl From for Box { + fn from(backend: ScdBackend) -> Box { + 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, serial: &str) -> Result { + /// 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, serial: &str) -> Result { 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) -> Result { - 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) -> Result { + 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) -> Result<(), Error> { + pub fn shutdown_scd(agent: Option) -> 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, init: bool) -> Result { - 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, init: bool) -> Result { + 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, Error> { + fn transaction( + &mut self, + _reselect_application: Option<&[u8]>, + ) -> Result, 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, Error> { + fn transmit(&mut self, cmd: &[u8], _: usize) -> Result, 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, Error> { + /// Not implemented yet + fn pinpad_verify( + &mut self, + _id: PinType, + _card_caps: &Option, + ) -> Result, SmartcardError> { unimplemented!() } - /// FIXME: not implemented yet - fn pinpad_modify(&mut self, _id: PinType) -> Result, Error> { + /// Not implemented yet + fn pinpad_modify( + &mut self, + _id: PinType, + _card_caps: &Option, + ) -> Result, SmartcardError> { unimplemented!() } }