Support scdaemon as an alternative backend for interaction with OpenPGP cards.

This commit is contained in:
Heiko Schaefer 2021-07-16 16:30:56 +02:00
parent 8e3c6c0046
commit 01126aabdf
11 changed files with 252 additions and 100 deletions

View file

@ -6,4 +6,5 @@
members = [ members = [
"openpgp-card", "openpgp-card",
"openpgp-card-sequoia", "openpgp-card-sequoia",
"scdc",
] ]

View file

@ -14,6 +14,7 @@ documentation = "https://docs.rs/crate/openpgp-card-sequoia"
[dependencies] [dependencies]
sequoia-openpgp = "1.3" sequoia-openpgp = "1.3"
openpgp-card = { path = "../openpgp-card", version = "0.0.1" } openpgp-card = { path = "../openpgp-card", version = "0.0.1" }
openpgp-card-scdc = { path = "../scdc" }
chrono = "0.4" chrono = "0.4"
anyhow = "1" anyhow = "1"
thiserror = "1" thiserror = "1"

View file

@ -282,7 +282,7 @@ pub fn decrypt(
} }
pub fn sign( pub fn sign(
ocu: &mut CardSign, ocu: CardSign,
cert: &sequoia_openpgp::Cert, cert: &sequoia_openpgp::Cert,
input: &mut dyn io::Read, input: &mut dyn io::Read,
) -> Result<String> { ) -> Result<String> {

View file

@ -8,7 +8,8 @@ use anyhow::Result;
use sequoia_openpgp::parse::Parse; use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::Cert; use sequoia_openpgp::Cert;
use openpgp_card::{CardBase, KeyType}; use openpgp_card::KeyType;
use openpgp_card_scdc::ScdClient;
// Filename of test key and test message to use: // Filename of test key and test message to use:
@ -21,6 +22,8 @@ use openpgp_card::{CardBase, KeyType};
const TEST_KEY_PATH: &str = "example/test25519.sec"; const TEST_KEY_PATH: &str = "example/test25519.sec";
const TEST_ENC_MSG: &str = "example/encrypted_to_25519.asc"; const TEST_ENC_MSG: &str = "example/encrypted_to_25519.asc";
const SOCKET: &str = "/run/user/1000/gnupg/S.scdaemon";
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
env_logger::init(); env_logger::init();
@ -29,7 +32,8 @@ fn main() -> Result<(), Box<dyn Error>> {
if let Ok(test_card_ident) = test_card_ident { if let Ok(test_card_ident) = test_card_ident {
println!("** get card"); println!("** get card");
let mut oc = CardBase::open_by_ident(&test_card_ident)?; // let mut oc = CardBase::open_by_ident(&test_card_ident)?;
let mut oc = ScdClient::open_scdc(SOCKET)?;
// card metadata // card metadata
@ -140,7 +144,9 @@ fn main() -> Result<(), Box<dyn Error>> {
// Open fresh Card for decrypt // Open fresh Card for decrypt
// ----------------------------- // -----------------------------
let mut oc = CardBase::open_by_ident(&test_card_ident)?; // let mut oc = CardBase::open_by_ident(&test_card_ident)?;
let mut oc = ScdClient::open_scdc(SOCKET)?;
let app_id = oc.get_aid()?; let app_id = oc.get_aid()?;
// Check that we're still using the expected card // Check that we're still using the expected card
@ -179,18 +185,19 @@ fn main() -> Result<(), Box<dyn Error>> {
// ----------------------------- // -----------------------------
// Open fresh Card for signing // Open fresh Card for signing
// ----------------------------- // -----------------------------
let oc = CardBase::open_by_ident(&test_card_ident)?; // let oc = CardBase::open_by_ident(&test_card_ident)?;
let oc = ScdClient::open_scdc(SOCKET)?;
// Sign // Sign
match oc.verify_pw1_for_signing("123456") { match oc.verify_pw1_for_signing("123456") {
Ok(mut oc_user) => { Ok(oc_user) => {
println!("pw1 81 verify ok"); println!("pw1 81 verify ok");
let cert = Cert::from_file(TEST_KEY_PATH)?; let cert = Cert::from_file(TEST_KEY_PATH)?;
let text = "Hello world, I am signed."; let text = "Hello world, I am signed.";
let res = openpgp_card_sequoia::sign( let res = openpgp_card_sequoia::sign(
&mut oc_user, oc_user,
&cert, &cert,
&mut text.as_bytes(), &mut text.as_bytes(),
); );
@ -213,7 +220,7 @@ fn main() -> Result<(), Box<dyn Error>> {
println!("The following OpenPGP cards are connected to your system:"); println!("The following OpenPGP cards are connected to your system:");
let cards = openpgp_card::CardBase::list_cards()?; let cards = openpgp_card::CardBase::list_cards_pcsc()?;
for c in cards { for c in cards {
println!(" '{}'", c.get_aid()?.ident()); println!(" '{}'", c.get_aid()?.ident());
} }

View file

@ -16,26 +16,26 @@ use openpgp_card::Hash;
use crate::PublicKey; use crate::PublicKey;
pub(crate) struct CardSigner<'a> { pub(crate) struct CardSigner {
/// The OpenPGP card (authenticated to allow signing operations) /// The OpenPGP card (authenticated to allow signing operations)
ocu: &'a mut CardSign, ocu: CardSign,
/// The matching public key for the card's signing key /// The matching public key for the card's signing key
public: PublicKey, public: PublicKey,
} }
impl<'a> CardSigner<'a> { impl CardSigner {
/// Try to create a CardSigner. /// Try to create a CardSigner.
/// ///
/// An Error is returned if no match between the card's signing /// An Error is returned if no match between the card's signing
/// key and a (sub)key of `cert` can be made. /// key and a (sub)key of `cert` can be made.
pub fn new( pub fn new(
ocs: &'a mut CardSign, cs: CardSign,
cert: &openpgp::Cert, cert: &openpgp::Cert,
policy: &dyn Policy, policy: &dyn Policy,
) -> Result<CardSigner<'a>, OpenpgpCardError> { ) -> Result<CardSigner, OpenpgpCardError> {
// Get the fingerprint for the signing key from the card. // Get the fingerprint for the signing key from the card.
let fps = ocs.get_fingerprints()?; let fps = cs.get_fingerprints()?;
let fp = fps.signature(); let fp = fps.signature();
if let Some(fp) = fp { if let Some(fp) = fp {
@ -58,7 +58,7 @@ impl<'a> CardSigner<'a> {
let public = keys[0].clone(); let public = keys[0].clone();
Ok(CardSigner { Ok(CardSigner {
ocu: ocs, ocu: cs,
public: public.role_as_unspecified().clone(), public: public.role_as_unspecified().clone(),
}) })
} else { } else {
@ -75,7 +75,7 @@ impl<'a> CardSigner<'a> {
} }
} }
impl<'a> crypto::Signer for CardSigner<'a> { impl<'a> crypto::Signer for CardSigner {
fn public(&self) -> &PublicKey { fn public(&self) -> &PublicKey {
&self.public &self.public
} }

View file

@ -5,14 +5,14 @@ pub mod command;
pub mod commands; pub mod commands;
pub mod response; pub mod response;
use anyhow::Result; use anyhow::{anyhow, Result};
use pcsc::Card; use pcsc::Card;
use std::convert::TryFrom; use std::convert::TryFrom;
use crate::apdu::command::Command; use crate::apdu::command::Command;
use crate::apdu::response::Response; use crate::apdu::response::Response;
use crate::errors::{OcErrorStatus, OpenpgpCardError, SmartcardError}; use crate::errors::{OcErrorStatus, OpenpgpCardError, SmartcardError};
use crate::CardCaps; use crate::{CardBase, CardCaps, CardClient, CardClientBox};
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub(crate) enum Le { pub(crate) enum Le {
@ -26,7 +26,7 @@ pub(crate) enum Le {
/// If the reply is truncated, this fn assembles all the parts and returns /// If the reply is truncated, this fn assembles all the parts and returns
/// them as one aggregated Response. /// them as one aggregated Response.
pub(crate) fn send_command( pub(crate) fn send_command(
card_client: &mut Box<dyn CardClient + Send + Sync>, card_client: &mut CardClientBox,
cmd: Command, cmd: Command,
expect_reply: bool, expect_reply: bool,
card_caps: Option<&CardCaps>, card_caps: Option<&CardCaps>,
@ -70,7 +70,7 @@ pub(crate) fn send_command(
/// If the response is chained, this fn only returns one chunk, the caller /// If the response is chained, this fn only returns one chunk, the caller
/// needs take care of chained responses /// needs take care of chained responses
fn send_command_low_level( fn send_command_low_level(
card_client: &mut Box<dyn CardClient + Send + Sync>, card_client: &mut CardClientBox,
cmd: Command, cmd: Command,
expect_reply: bool, expect_reply: bool,
card_caps: Option<&CardCaps>, card_caps: Option<&CardCaps>,
@ -179,18 +179,32 @@ fn send_command_low_level(
} }
} }
pub trait CardClient {
fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result<Vec<u8>>;
}
pub struct PcscClient { pub struct PcscClient {
card: Card, card: Card,
} }
impl PcscClient { impl PcscClient {
pub fn new(card: Card) -> Self { fn new(card: Card) -> Self {
Self { card } Self { card }
} }
/// Take a PCSC Card object and try to open the OpenPGP card applet.
/// If successful, wrap and return the resulting CardClient as a
/// CardBase object (which involves caching the "application related
/// data").
pub fn open(card: Card) -> Result<CardBase, OpenpgpCardError> {
let card_client = PcscClient::new(card);
let mut ccb = Box::new(card_client) as CardClientBox;
let select_openpgp = commands::select_openpgp();
let resp = send_command(&mut ccb, select_openpgp, false, None)?;
if resp.is_ok() {
CardBase::open_card(ccb)
} else {
Err(anyhow!("Couldn't open OpenPGP application").into())
}
}
} }
impl CardClient for PcscClient { impl CardClient for PcscClient {

View file

@ -13,35 +13,28 @@ use std::borrow::BorrowMut;
use std::convert::TryFrom; use std::convert::TryFrom;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use pcsc::*;
use apdu::{commands, response::Response}; use crate::apdu::{commands, response::Response};
use parse::{ use crate::errors::OpenpgpCardError;
use crate::parse::{
algo_attrs::Algo, algo_info::AlgoInfo, application_id::ApplicationId, algo_attrs::Algo, algo_info::AlgoInfo, application_id::ApplicationId,
cardholder::CardHolder, extended_cap::ExtendedCap, cardholder::CardHolder, extended_cap::ExtendedCap,
extended_length_info::ExtendedLengthInfo, fingerprint, extended_length_info::ExtendedLengthInfo, fingerprint,
historical::Historical, pw_status::PWStatus, KeySet, historical::Historical, pw_status::PWStatus, KeySet,
}; };
use tlv::Tlv; use crate::tlv::{tag::Tag, Tlv, TlvEntry};
use crate::errors::OpenpgpCardError;
use crate::tlv::tag::Tag;
use crate::tlv::TlvEntry;
use crate::apdu::CardClient;
use crate::Hash;
use crate::{ use crate::{
apdu, key_upload, parse, tlv, CardCaps, CardUploadableKey, DecryptMe, apdu, key_upload, parse, tlv, CardCaps, CardClientBox, CardUploadableKey,
KeyType, Sex, DecryptMe, Hash, KeyType, Sex,
}; };
pub(crate) struct CardApp { pub struct CardApp {
card_client: Box<dyn CardClient + Send + Sync>, card_client: CardClientBox,
card_caps: Option<CardCaps>, card_caps: Option<CardCaps>,
} }
impl CardApp { impl CardApp {
pub fn new(card_client: Box<dyn CardClient + Send + Sync>) -> Self { pub fn new(card_client: CardClientBox) -> Self {
Self { Self {
card_client, card_client,
card_caps: None, card_caps: None,
@ -55,7 +48,7 @@ impl CardApp {
} }
} }
pub fn card(&mut self) -> &mut Box<dyn CardClient + Send + Sync> { pub fn card(&mut self) -> &mut CardClientBox {
&mut self.card_client &mut self.card_client
} }

View file

@ -3,19 +3,17 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use crate::apdu::command::Command; use crate::apdu::{command::Command, commands};
use crate::apdu::{commands, CardClient};
use crate::card_app::CardApp; use crate::card_app::CardApp;
use crate::errors::OpenpgpCardError; use crate::errors::OpenpgpCardError;
use crate::parse::algo_attrs::{Algo, RsaAttrs}; use crate::parse::algo_attrs::{Algo, RsaAttrs};
use crate::parse::algo_info::AlgoInfo; use crate::parse::algo_info::AlgoInfo;
use crate::tlv::{tag::Tag, Tlv, TlvEntry}; use crate::tlv::{tag::Tag, Tlv, TlvEntry};
use crate::{apdu, CardCaps}; use crate::{apdu, CardCaps, CardClientBox};
use crate::{ use crate::{
tlv, CardUploadableKey, EccKey, EccType, KeyType, PrivateKeyMaterial, tlv, CardUploadableKey, EccKey, EccType, KeyType, PrivateKeyMaterial,
RSAKey, RSAKey,
}; };
use pcsc::Card;
/// Upload an explicitly selected Key to the card as a specific KeyType. /// Upload an explicitly selected Key to the card as a specific KeyType.
/// ///
@ -370,7 +368,7 @@ fn ecc_algo_attrs_cmd(
} }
fn copy_key_to_card( fn copy_key_to_card(
card_client: &mut Box<dyn CardClient + Send + Sync>, card_client: &mut CardClientBox,
key_type: KeyType, key_type: KeyType,
ts: u64, ts: u64,
fp: Vec<u8>, fp: Vec<u8>,

View file

@ -13,28 +13,48 @@ use parse::{
}; };
use tlv::Tlv; use tlv::Tlv;
use crate::apdu::{CardClient, PcscClient}; use crate::apdu::PcscClient;
use crate::card_app::CardApp; use crate::card_app::CardApp;
use crate::errors::{OpenpgpCardError, SmartcardError}; use crate::errors::{OpenpgpCardError, SmartcardError};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
mod apdu; mod apdu;
mod card; mod card;
mod card_app; pub mod card_app;
pub mod errors; pub mod errors;
mod key_upload; mod key_upload;
mod parse; mod parse;
mod tlv; mod tlv;
pub trait CardClient {
fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result<Vec<u8>>;
}
pub type CardClientBox = Box<dyn CardClient + Send + Sync>;
/// Information about the capabilities of the card. /// Information about the capabilities of the card.
/// (feature configuration from card metadata) /// (feature configuration from card metadata)
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(crate) struct CardCaps { pub struct CardCaps {
pub(crate) ext_support: bool, pub(crate) ext_support: bool,
pub(crate) chaining_support: bool, pub(crate) chaining_support: bool,
pub(crate) max_cmd_bytes: u16, pub(crate) max_cmd_bytes: u16,
} }
impl CardCaps {
pub fn new(
ext_support: bool,
chaining_support: bool,
max_cmd_bytes: u16,
) -> CardCaps {
Self {
ext_support,
chaining_support,
max_cmd_bytes,
}
}
}
/// Container for a hash value. /// Container for a hash value.
/// These hash values can be signed by the card. /// These hash values can be signed by the card.
pub enum Hash<'a> { pub enum Hash<'a> {
@ -228,12 +248,16 @@ pub struct CardBase {
} }
impl CardBase { impl CardBase {
/// Get all cards that can be opened as an OpenPGP card applet pub fn new(card_app: CardApp, ard: Tlv) -> Self {
pub fn list_cards() -> Result<Vec<Self>> { Self { card_app, ard }
}
/// Get all cards that can be opened as an OpenPGP card applet via pcsc
pub fn list_cards_pcsc() -> Result<Vec<Self>> {
let cards = card::get_cards().map_err(|err| anyhow!(err))?; let cards = card::get_cards().map_err(|err| anyhow!(err))?;
let ocs: Vec<_> = cards let ocs: Vec<_> = cards
.into_iter() .into_iter()
.map(Self::open_card) .map(PcscClient::open)
.map(|oc| oc.ok()) .map(|oc| oc.ok())
.flatten() .flatten()
.collect(); .collect();
@ -245,7 +269,7 @@ impl CardBase {
/// ///
/// The ident is constructed as a concatenation of manufacturer /// The ident is constructed as a concatenation of manufacturer
/// id, a colon, and the card serial. Example: "1234:5678ABCD". /// id, a colon, and the card serial. Example: "1234:5678ABCD".
pub fn open_by_ident(ident: &str) -> Result<Self, OpenpgpCardError> { pub fn open_by_ident_pcsc(ident: &str) -> Result<Self, OpenpgpCardError> {
let cards = card::get_cards().map_err(|e| { let cards = card::get_cards().map_err(|e| {
OpenpgpCardError::Smartcard(SmartcardError::Error(format!( OpenpgpCardError::Smartcard(SmartcardError::Error(format!(
"{:?}", "{:?}",
@ -254,7 +278,7 @@ impl CardBase {
})?; })?;
for card in cards { for card in cards {
let res = Self::open_card(card); let res = PcscClient::open(card);
if let Ok(opened_card) = res { if let Ok(opened_card) = res {
let res = opened_card.get_aid(); let res = opened_card.get_aid();
if let Ok(aid) = res { if let Ok(aid) = res {
@ -271,7 +295,7 @@ impl CardBase {
} }
/// Open connection to some card and select the openpgp applet /// Open connection to some card and select the openpgp applet
pub fn open_yolo() -> Result<Self, OpenpgpCardError> { pub fn open_yolo_pcsc() -> Result<Self, OpenpgpCardError> {
let mut cards = card::get_cards().map_err(|e| { let mut cards = card::get_cards().map_err(|e| {
OpenpgpCardError::Smartcard(SmartcardError::Error(format!( OpenpgpCardError::Smartcard(SmartcardError::Error(format!(
"{:?}", "{:?}",
@ -282,20 +306,12 @@ impl CardBase {
// randomly use the first card in the list // randomly use the first card in the list
let card = cards.swap_remove(0); let card = cards.swap_remove(0);
Self::open_card(card) PcscClient::open(card)
} }
/// Open connection to a specific card and select the openpgp applet /// Set up connection (cache "application related data") to a
fn open_card(card: Card) -> Result<Self, OpenpgpCardError> { /// CardClient, on which the openpgp applet has already been opened.
let select_openpgp = commands::select_openpgp(); pub fn open_card(ccb: CardClientBox) -> Result<Self, OpenpgpCardError> {
let card_client = PcscClient::new(card);
let mut ccb =
Box::new(card_client) as Box<dyn CardClient + Send + Sync>;
let resp = apdu::send_command(&mut ccb, select_openpgp, false, None)?;
if resp.is_ok() {
// read and cache "application related data" // read and cache "application related data"
let mut card_app = CardApp::new(ccb); let mut card_app = CardApp::new(ccb);
let ard = card_app.get_app_data()?; let ard = card_app.get_app_data()?;
@ -330,9 +346,6 @@ impl CardBase {
let card_app = card_app.set_caps(caps); let card_app = card_app.set_caps(caps);
Ok(Self { card_app, ard }) Ok(Self { card_app, ard })
} else {
Err(anyhow!("Couldn't open OpenPGP application").into())
}
} }
// --- application data --- // --- application data ---

19
scdc/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
# SPDX-License-Identifier: MIT OR Apache-2.0
[package]
name = "openpgp-card-scdc"
description = "Experimental SCDaemon Client, intended for use in the openpgp-card crate"
version = "0.0.1"
edition = "2018"
[dependencies]
openpgp-card = { path = "../openpgp-card" }
sequoia-ipc = { path = "../../sequoia/ipc" }
libc = "0.2"
hex = "0.4"
anyhow = "1"
futures = "0.3"
tokio = "0.2"
lazy_static = "1.4"
log = "0.4"

106
scdc/src/lib.rs Normal file
View file

@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
use anyhow::{anyhow, Result};
use futures::StreamExt;
use lazy_static::lazy_static;
use sequoia_ipc::assuan::{Client, Response};
use std::sync::Mutex;
use tokio::runtime::Runtime;
use openpgp_card::errors::OpenpgpCardError;
use openpgp_card::{CardBase, CardClient, CardClientBox};
lazy_static! {
pub(crate) static ref RT: Mutex<Runtime> =
Mutex::new(tokio::runtime::Runtime::new().unwrap());
}
pub struct ScdClient {
client: Client,
}
impl ScdClient {
/// Create a CardBase object that uses an scdaemon instance as its
/// backend.
pub fn open_scdc(socket: &str) -> Result<CardBase, OpenpgpCardError> {
let card_client = ScdClient::new(socket)?;
let card_client_box = Box::new(card_client) as CardClientBox;
CardBase::open_card(card_client_box)
}
/// Create a CardBase object that uses an scdaemon instance as its
/// backend, asking for a specific card by `serial`.
pub fn open_scdc_by_serial(
socket: &str,
serial: &str,
) -> Result<CardBase, OpenpgpCardError> {
let mut card_client = ScdClient::new(socket)?;
card_client.select_card(serial)?;
let card_client_box = Box::new(card_client) as CardClientBox;
CardBase::open_card(card_client_box)
}
pub fn new(socket: &str) -> Result<Self> {
let client = RT.lock().unwrap().block_on(Client::connect(socket))?;
Ok(Self { client })
}
fn select_card(&mut self, serial: &str) -> Result<()> {
let send = format!("SERIALNO --demand={}\n", serial);
self.client.send(send)?;
let mut rt = RT.lock().unwrap();
while let Some(response) = rt.block_on(self.client.next()) {
if let Err(_) = response {
return Err(anyhow!("Card not found"));
}
if let Ok(Response::Status { .. }) = response {
// drop remaining lines
while let Some(_drop) = rt.block_on(self.client.next()) {}
return Ok(());
}
}
Err(anyhow!("Card not found"))
}
}
impl CardClient for ScdClient {
fn transmit(&mut self, cmd: &[u8], _: usize) -> Result<Vec<u8>> {
let hex = hex::encode(cmd);
let send = format!("APDU {}\n", hex);
println!("send: '{}'", send);
self.client.send(send)?;
let mut rt = RT.lock().unwrap();
while let Some(response) = rt.block_on(self.client.next()) {
log::trace!("res: {:x?}", response);
if let Err(_) = response {
unimplemented!();
}
if let Ok(Response::Data { partial }) = response {
let res = partial;
// drop remaining lines
while let Some(drop) = rt.block_on(self.client.next()) {
log::trace!("drop: {:x?}", drop);
}
return Ok(res);
}
}
Err(anyhow!("no response found"))
}
}