openpgp_card_scdc/
lib.rs

1// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! This crate implements the experimental `ScdBackend`/`ScdTransaction` backend for the
5//! `openpgp-card` crate.
6//! It uses GnuPG's scdaemon (via GnuPG Agent) to access OpenPGP cards.
7//!
8//! Note that (unlike `openpgp-card-pcsc`), this backend doesn't implement transaction guarantees.
9
10use futures::StreamExt;
11use lazy_static::lazy_static;
12use sequoia_ipc::assuan::Response;
13use sequoia_ipc::gnupg::{Agent, Context};
14use std::sync::Mutex;
15use tokio::runtime::Runtime;
16
17use openpgp_card::{CardBackend, CardCaps, CardTransaction, Error, PinType, SmartcardError};
18
19lazy_static! {
20    static ref RT: Mutex<Runtime> = Mutex::new(tokio::runtime::Runtime::new().unwrap());
21}
22
23/// The Assuan protocol (in GnuPG) limits the length of commands.
24///
25/// See:
26/// https://www.gnupg.org/documentation/manuals/assuan/Client-requests.html#Client-requests
27///
28/// Currently there seems to be no way to send longer commands via Assuan,
29/// the functionality to break one command into multiple lines has
30/// apparently not yet been implemented.
31///
32/// NOTE: This number is probably off by a few bytes (is "SCD " added in
33/// communication within GnuPG? Are \r\n added?)
34const ASSUAN_LINELENGTH: usize = 1000;
35
36/// The maximum number of bytes for a command that we will send to
37/// scdaemon (via Assuan).
38///
39/// Each command byte gets sent via Assuan as a two-character hex string.
40///
41/// 22 characters are used to send "SCD APDU --exlen=abcd "
42/// (So, as a defensive limit, 25 characters are subtracted).
43///
44/// In concrete terms, this limit means that some commands (with big
45/// parameters) cannot be sent to the card, when the card doesn't support
46/// command chaining (like the floss-shop "OpenPGP Smart Card 3.4").
47///
48/// In particular, uploading rsa4096 keys fails via scdaemon, with such cards.
49const APDU_CMD_BYTES_MAX: usize = (ASSUAN_LINELENGTH - 25) / 2;
50
51/// An implementation of the CardBackend trait that uses GnuPG's scdaemon
52/// (via GnuPG Agent) to access OpenPGP card devices.
53pub struct ScdBackend {
54    agent: Agent,
55    card_caps: Option<CardCaps>,
56}
57
58impl ScdBackend {
59    /// Open a CardApp that uses an scdaemon instance as its backend.
60    /// The specific card with AID `serial` is requested from scdaemon.
61    pub fn open_by_serial(agent: Option<Agent>, serial: &str) -> Result<Self, Error> {
62        let mut card = ScdBackend::new(agent, true)?;
63        card.select_card(serial)?;
64
65        card.transaction()?.initialize()?;
66
67        Ok(card)
68    }
69
70    /// Open a CardApp that uses an scdaemon instance as its backend.
71    ///
72    /// If multiple cards are available, scdaemon implicitly selects one.
73    ///
74    /// (NOTE: implicitly picking an unspecified card might be a bad idea.
75    /// You might want to avoid using this function.)
76    pub fn open_yolo(agent: Option<Agent>) -> Result<Self, Error> {
77        let mut card = ScdBackend::new(agent, true)?;
78
79        card.transaction()?.initialize()?;
80
81        Ok(card)
82    }
83
84    /// Helper fn that shuts down scdaemon via GnuPG Agent.
85    /// This may be useful to obtain access to a Smard card via PCSC.
86    pub fn shutdown_scd(agent: Option<Agent>) -> Result<(), Error> {
87        let mut scdc = Self::new(agent, false)?;
88
89        scdc.send("SCD RESTART")?;
90        scdc.send("SCD BYE")?;
91
92        Ok(())
93    }
94
95    /// Initialize an ScdClient object that is connected to an scdaemon
96    /// instance via a GnuPG `agent` instance.
97    ///
98    /// If `agent` is None, a Context with the default GnuPG home directory
99    /// is used.
100    fn new(agent: Option<Agent>, init: bool) -> Result<Self, Error> {
101        let agent = if let Some(agent) = agent {
102            agent
103        } else {
104            // Create and use a new Agent based on a default Context
105            let ctx = Context::new().map_err(|e| {
106                Error::Smartcard(SmartcardError::Error(format!("Context::new failed {}", e)))
107            })?;
108            RT.lock()
109                .unwrap()
110                .block_on(Agent::connect(&ctx))
111                .map_err(|e| {
112                    Error::Smartcard(SmartcardError::Error(format!(
113                        "Agent::connect failed {}",
114                        e
115                    )))
116                })?
117        };
118
119        let mut scdc = Self {
120            agent,
121            card_caps: None,
122        };
123
124        if init {
125            scdc.serialno()?;
126        }
127
128        Ok(scdc)
129    }
130
131    fn send2(&mut self, cmd: &str) -> Result<(), Error> {
132        self.agent.send(cmd).map_err(|e| {
133            Error::Smartcard(SmartcardError::Error(format!(
134                "scdc agent send failed: {}",
135                e
136            )))
137        })
138    }
139
140    /// Call "SCD SERIALNO", which causes scdaemon to be started by gpg
141    /// agent (if it's not running yet).
142    fn serialno(&mut self) -> Result<(), Error> {
143        let rt = RT.lock().unwrap();
144
145        let send = "SCD SERIALNO";
146        self.send2(send)?;
147
148        while let Some(response) = rt.block_on(self.agent.next()) {
149            log::trace!("init res: {:x?}", response);
150
151            if let Ok(Response::Status { .. }) = response {
152                // drop remaining lines
153                while let Some(_drop) = rt.block_on(self.agent.next()) {
154                    log::trace!("init drop: {:x?}", _drop);
155                }
156
157                return Ok(());
158            }
159        }
160
161        Err(Error::Smartcard(SmartcardError::Error(
162            "SCDC init() failed".into(),
163        )))
164    }
165
166    /// Ask scdameon to switch to using a specific OpenPGP card, based on
167    /// its `serial`.
168    fn select_card(&mut self, serial: &str) -> Result<(), Error> {
169        let send = format!("SCD SERIALNO --demand={}", serial);
170        self.send2(&send)?;
171
172        let rt = RT.lock().unwrap();
173
174        while let Some(response) = rt.block_on(self.agent.next()) {
175            log::trace!("select res: {:x?}", response);
176
177            if response.is_err() {
178                return Err(Error::Smartcard(SmartcardError::CardNotFound(
179                    serial.into(),
180                )));
181            }
182
183            if let Ok(Response::Status { .. }) = response {
184                // drop remaining lines
185                while let Some(_drop) = rt.block_on(self.agent.next()) {
186                    log::trace!("select drop: {:x?}", _drop);
187                }
188
189                return Ok(());
190            }
191        }
192
193        Err(Error::Smartcard(SmartcardError::CardNotFound(
194            serial.into(),
195        )))
196    }
197
198    fn send(&mut self, cmd: &str) -> Result<(), Error> {
199        self.send2(cmd)?;
200
201        let rt = RT.lock().unwrap();
202
203        while let Some(response) = rt.block_on(self.agent.next()) {
204            log::trace!("select res: {:x?}", response);
205
206            if let Err(e) = response {
207                return Err(Error::Smartcard(SmartcardError::Error(format!("{:?}", e))));
208            }
209
210            if let Ok(..) = response {
211                // drop remaining lines
212                while let Some(_drop) = rt.block_on(self.agent.next()) {
213                    log::trace!(" drop: {:x?}", _drop);
214                }
215
216                return Ok(());
217            }
218        }
219
220        Err(Error::Smartcard(SmartcardError::Error(format!(
221            "Error sending command {}",
222            cmd
223        ))))
224    }
225}
226
227impl CardBackend for ScdBackend {
228    fn transaction(&mut self) -> Result<Box<dyn CardTransaction + Send + Sync + '_>, Error> {
229        Ok(Box::new(ScdTransaction { scd: self }))
230    }
231}
232
233pub struct ScdTransaction<'a> {
234    scd: &'a mut ScdBackend,
235}
236
237impl CardTransaction for ScdTransaction<'_> {
238    fn transmit(&mut self, cmd: &[u8], _: usize) -> Result<Vec<u8>, Error> {
239        log::trace!("SCDC cmd len {}", cmd.len());
240
241        let hex = hex::encode(cmd);
242
243        // (Unwrap is ok here, not having a card_caps is fine)
244        let ext = if self.card_caps().is_some() && self.card_caps().unwrap().ext_support() {
245            // If we know about card_caps, and can do extended length we
246            // set "exlen" accordingly ...
247            format!("--exlen={} ", self.card_caps().unwrap().max_rsp_bytes())
248        } else {
249            // ... otherwise don't send "exlen" to scdaemon
250            "".to_string()
251        };
252
253        let send = format!("SCD APDU {}{}\n", ext, hex);
254        log::trace!("SCDC command: '{}'", send);
255
256        if send.len() > ASSUAN_LINELENGTH {
257            return Err(Error::Smartcard(SmartcardError::Error(format!(
258                "APDU command is too long ({}) to send via Assuan",
259                send.len()
260            ))));
261        }
262
263        self.scd.send2(&send)?;
264
265        let rt = RT.lock().unwrap();
266
267        while let Some(response) = rt.block_on(self.scd.agent.next()) {
268            log::trace!("res: {:x?}", response);
269            if response.is_err() {
270                return Err(Error::Smartcard(SmartcardError::Error(format!(
271                    "Unexpected error response from SCD {:?}",
272                    response
273                ))));
274            }
275
276            if let Ok(Response::Data { partial }) = response {
277                let res = partial;
278
279                // drop remaining lines
280                while let Some(drop) = rt.block_on(self.scd.agent.next()) {
281                    log::trace!("drop: {:x?}", drop);
282                }
283
284                return Ok(res);
285            }
286        }
287
288        Err(Error::Smartcard(SmartcardError::Error(
289            "no response found".into(),
290        )))
291    }
292
293    fn init_card_caps(&mut self, caps: CardCaps) {
294        self.scd.card_caps = Some(caps);
295    }
296
297    fn card_caps(&self) -> Option<&CardCaps> {
298        self.scd.card_caps.as_ref()
299    }
300
301    /// Return limit for APDU command size via scdaemon (based on Assuan
302    /// maximum line length)
303    fn max_cmd_len(&self) -> Option<usize> {
304        Some(APDU_CMD_BYTES_MAX)
305    }
306
307    /// FIXME: not implemented yet
308    fn feature_pinpad_verify(&self) -> bool {
309        false
310    }
311
312    /// FIXME: not implemented yet
313    fn feature_pinpad_modify(&self) -> bool {
314        false
315    }
316
317    /// FIXME: not implemented yet
318    fn pinpad_verify(&mut self, _id: PinType) -> Result<Vec<u8>, Error> {
319        unimplemented!()
320    }
321
322    /// FIXME: not implemented yet
323    fn pinpad_modify(&mut self, _id: PinType) -> Result<Vec<u8>, Error> {
324        unimplemented!()
325    }
326}