diff --git a/scdc/src/lib.rs b/scdc/src/lib.rs index a4f9eb9..27ba9b4 100644 --- a/scdc/src/lib.rs +++ b/scdc/src/lib.rs @@ -9,15 +9,38 @@ use std::sync::Mutex; use tokio::runtime::Runtime; use openpgp_card::errors::OpenpgpCardError; -use openpgp_card::{CardBase, CardClient, CardClientBox}; +use openpgp_card::{CardBase, CardCaps, CardClient, CardClientBox}; lazy_static! { pub(crate) static ref RT: Mutex = Mutex::new(tokio::runtime::Runtime::new().unwrap()); } +/// The Assuan protocol which is used in GnuPG limits the length of commands. +/// Currently there seems to be no way to send longer commands via Assuan. +/// +/// See: +/// https://www.gnupg.org/documentation/manuals/assuan/Client-requests.html#Client-requests +/// +/// FIXME: This number is probably off by a few bytes (is "SCD " added in +/// communication within GnuPG? Are \r\n added?) +const ASSUAN_LINELENGTH: usize = 1000; + +/// The maximum number of bytes for a command that can be sent via Assuan to +/// scdaemon. +/// Each command byte gets sent via Assuan as a two-character hex string, +/// and a few characters are used to send "APDU --exlen=abcd" (as a +/// conservative limit, 20 characters are subtracted). +/// +/// In concrete terms, this limit means that with cards that do not support +/// command chaining (like the floss-shop OpenPGP Card 3.4), some commands +/// cannot be sent to the card. In particular, uploading rsa4096 keys will +/// fail via scdaemon, with such cards. +const CMD_SIZE_MAX: usize = ASSUAN_LINELENGTH / 2 - 20; + pub struct ScdClient { client: Client, + card_caps: Option, } impl ScdClient { @@ -47,7 +70,10 @@ impl ScdClient { pub fn new(socket: &str) -> Result { let client = RT.lock().unwrap().block_on(Client::connect(socket))?; - Ok(Self { client }) + Ok(Self { + client, + card_caps: None, + }) } pub fn select_card(&mut self, serial: &str) -> Result<()> { @@ -57,7 +83,7 @@ impl ScdClient { let mut rt = RT.lock().unwrap(); while let Some(response) = rt.block_on(self.client.next()) { - log::trace!("select res: {:x?}", response); + log::debug!("select res: {:x?}", response); if response.is_err() { return Err(anyhow!("Card not found")); @@ -66,7 +92,7 @@ impl ScdClient { if let Ok(Response::Status { .. }) = response { // drop remaining lines while let Some(_drop) = rt.block_on(self.client.next()) { - log::trace!("select drop: {:x?}", _drop); + log::debug!("select drop: {:x?}", _drop); } return Ok(()); @@ -79,18 +105,38 @@ impl ScdClient { impl CardClient for ScdClient { fn transmit(&mut self, cmd: &[u8], _: usize) -> Result> { + log::debug!("SCDC cmd len {}", cmd.len()); + let hex = hex::encode(cmd); - let send = format!("APDU {}\n", hex); - log::trace!("send: '{}'", send); + let ex = if let Some(caps) = self.card_caps { + if caps.ext_support { + format!("--exlen={} ", caps.max_rsp_bytes) + } else { + "".to_string() + } + } else { + "".to_string() + }; + + let send = format!("APDU {}{}\n", ex, hex); + log::debug!("send: '{}'", send); + + if send.len() > ASSUAN_LINELENGTH { + return Err(anyhow!( + "APDU command is too long ({}) to send via Assuan", + send.len() + )); + } + 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); + log::debug!("res: {:x?}", response); if response.is_err() { - unimplemented!(); + unimplemented!("Err: {:x?}", response); } if let Ok(Response::Data { partial }) = response { @@ -98,7 +144,7 @@ impl CardClient for ScdClient { // drop remaining lines while let Some(drop) = rt.block_on(self.client.next()) { - log::trace!("drop: {:x?}", drop); + log::debug!("drop: {:x?}", drop); } return Ok(res); @@ -107,4 +153,18 @@ impl CardClient for ScdClient { Err(anyhow!("no response found")) } + + fn init_caps(&mut self, caps: CardCaps) { + self.card_caps = Some(caps); + } + + fn get_caps(&self) -> Option<&CardCaps> { + self.card_caps.as_ref() + } + + /// Return limit for APDU command size via scdaemon (based on Assuan + /// maximum line length) + fn max_cmd_len(&self) -> Option { + Some(CMD_SIZE_MAX) + } }