Skip to main content

sioul_core/
securitykey.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Your OpenPGP keys on a security key: an OpenPGP card, such as a YubiKey or
5//! a Nitrokey (OpenPGP card specification 3.4).
6//!
7//! The private keys never leave the security key. Sioul hands it what must be
8//! signed (a digest) or opened (a session key, wrapped), after its PIN, and
9//! gets the result back; the key may also wait for a touch. Everything public
10//! Sioul keeps itself, the certificate above all: a security key holds no
11//! certificate, only its keys' fingerprints and public parts.
12//!
13//! - [`Readers`] is how Sioul reaches security keys: the system's smart card
14//!   service (PC/SC) on a computer, a software stand-in ([`SoftReaders`]) in
15//!   tests. A key is opened for one operation, then closed, which resets it:
16//!   no other program finds it unlocked afterwards.
17//! - [`CardSigner`] and [`CardDecryptor`] are Sequoia's own `Signer` and
18//!   `Decryptor` for a key on a security key. Each opens the security key when
19//!   Sequoia asks for a signature or a session key, not before. The glue
20//!   follows Heiko Schaefer's `openpgp-card-sequoia`, as Sequoia's key store
21//!   carries it (`sequoia-keystore-openpgp-card` 0.2.2, its `decrypt_ciphertext`
22//!   and `sign`, MIT OR Apache-2.0).
23//! - [`check`] tells whether a certificate belongs to a security key: its
24//!   fingerprints, its public keys, what each key may do, until when.
25//! - [`KnownCard`] is what Sioul remembers of a security key, in
26//!   `pgp/cards.toml`: no secret.
27//! - [`PinMemory`] holds a PIN in memory only, kept encrypted there by
28//!   Sequoia, for fifteen minutes without use.
29//! - [`CardError`] says each failure in words, with what to do.
30//!
31//! Sioul only uses a security key: it never asks for its Admin PIN, never
32//! changes or unblocks a PIN, never loads or makes keys on it.
33
34use crate::config::data_dir;
35use crate::i18n::Translator;
36use openpgp::Cert;
37use openpgp::cert::prelude::*;
38use openpgp::crypto::{self, SessionKey, mpi};
39use openpgp::packet::{Key, key};
40use openpgp::policy::StandardPolicy;
41use openpgp::types::Curve;
42use sequoia_openpgp as openpgp;
43use serde::{Deserialize, Serialize};
44use std::path::{Path, PathBuf};
45use std::sync::Mutex;
46use std::time::{Duration, Instant, SystemTime};
47
48/// A certificate, as the crates that do not use Sequoia themselves name it.
49pub use openpgp::Cert as Certificate;
50/// A PIN, or a passphrase: kept encrypted in memory by Sequoia, wiped when dropped.
51pub use openpgp::crypto::Password;
52/// Bytes kept in protected memory, wiped when dropped: what a card opened.
53pub use openpgp::crypto::mem::Protected;
54/// A hash, as a card's RSA signature names it.
55pub use openpgp::types::HashAlgorithm;
56
57/// A public key as Sequoia holds it, whatever its role in its certificate.
58pub type PublicKey = Key<key::PublicParts, key::UnspecifiedRole>;
59
60/// Whether a key on the security key waits for a touch before it works: its
61/// "user interaction flag", the touch policy of a YubiKey.
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "kebab-case")]
64pub enum Touch {
65    /// Never.
66    #[default]
67    Off,
68    /// For each operation.
69    On,
70    /// For each operation, and it cannot be turned off.
71    Fixed,
72    /// Once for the operations of the next fifteen seconds.
73    Cached,
74    /// Cached, and it cannot be turned off.
75    CachedFixed,
76    /// A setting Sioul does not know: taken as asking.
77    Unknown,
78}
79
80impl Touch {
81    /// From the card's own number (OpenPGP card specification 3.4, §4.1.3.2,
82    /// and YubiKey's additions).
83    pub fn from_flag(flag: u8) -> Touch {
84        match flag {
85            0 => Touch::Off,
86            1 => Touch::On,
87            2 => Touch::Fixed,
88            3 => Touch::Cached,
89            4 => Touch::CachedFixed,
90            _ => Touch::Unknown,
91        }
92    }
93
94    /// The key may wait for a finger: Sioul says so before it asks.
95    pub fn asked(self) -> bool {
96        self != Touch::Off
97    }
98
99    /// What is said while the key may wait for a touch, at the moment it is
100    /// asked: to touch it now and hold the finger a second or two, as a key
101    /// may wait about fifteen seconds and its light says little; "If your key
102    /// asks for a touch…" when its setting could not be read (a wrong guess
103    /// costs nothing). None when it asks for no touch.
104    pub fn words(self, tr: &Translator) -> Option<String> {
105        match self {
106            Touch::Off => None,
107            Touch::Unknown => Some(tr.text("seckey-touch-maybe", None)),
108            _ => Some(tr.text("seckey-touch", None)),
109        }
110    }
111}
112
113/// Which check of the PIN an operation needs. A card keeps two from the one
114/// PIN: for signing (PW1 in mode 81), and for the rest (mode 82).
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum PinMode {
117    /// Before a signature.
118    Sign,
119    /// Before opening a session key.
120    Decrypt,
121}
122
123/// What Sioul was doing with the key, for the words of a failure.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum Purpose {
126    /// Signing a message at Send.
127    Sign,
128    /// Opening an encrypted message.
129    Open,
130    /// Reading what the key says, for its setup.
131    Read,
132}
133
134/// A key's public part as the card gives it.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum CardPublic {
137    /// RSA: its modulus and its exponent.
138    Rsa {
139        /// The modulus.
140        n: Vec<u8>,
141        /// The public exponent.
142        e: Vec<u8>,
143    },
144    /// An elliptic curve's point (Curve25519's without the 0x40 that OpenPGP puts before it).
145    Ecc {
146        /// The point.
147        point: Vec<u8>,
148    },
149}
150
151/// One key of the security key, as it describes it.
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct SlotInfo {
154    /// Its fingerprint as the card keeps it: 40 hexadecimal digits, upper
155    /// case (a card holds version 4 keys).
156    pub fingerprint: String,
157    /// When the key was made, as the card keeps it (Unix seconds).
158    pub created: u32,
159    /// "Ed25519", "Curve25519", "RSA 4096": its algorithm, by the card's attributes.
160    pub algorithm: String,
161    /// Whether it waits for a touch.
162    pub touch: Touch,
163    /// Its public part, when the card gives it.
164    pub public: Option<CardPublic>,
165}
166
167/// What a security key says without its PIN.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct CardInfo {
170    /// Its maker's number and its serial number, as `openpgp-card` names a
171    /// card: "0006:12345678".
172    pub ident: String,
173    /// Its maker's number (OpenPGP card specification, §4.2.1): 0006 is Yubico.
174    pub manufacturer: u16,
175    /// Its serial number.
176    pub serial: u32,
177    /// The name written on it, maybe empty.
178    pub holder: String,
179    /// The address of its public key, written on it, maybe empty.
180    pub url: String,
181    /// Its signing key, when it holds one.
182    pub sign: Option<SlotInfo>,
183    /// Its decryption key, when it holds one.
184    pub decrypt: Option<SlotInfo>,
185    /// Tries left for its PIN.
186    pub pin_tries: u8,
187    /// Tries left for its reset code: 0 when it has none.
188    pub reset_tries: u8,
189    /// Tries left for its Admin PIN.
190    pub admin_tries: u8,
191    /// The PIN is asked for each signature ("Signature PIN: forced").
192    pub pin_each_signature: bool,
193    /// Signatures it made so far.
194    pub signatures: u32,
195    /// The reader it is in, by the system's name: Sioul watches it to forget
196    /// the PIN when the key goes.
197    pub reader: String,
198}
199
200impl CardInfo {
201    /// "YubiKey 12 345 678": the key as sentences name it.
202    pub fn label(&self, tr: &Translator) -> String {
203        label(self.manufacturer, self.serial, tr)
204    }
205
206    /// The key's slot of `fingerprint`, if it holds that key.
207    pub fn slot_of(&self, fingerprint: &str) -> Option<&SlotInfo> {
208        [&self.sign, &self.decrypt].into_iter().flatten().find(|s| s.fingerprint.eq_ignore_ascii_case(fingerprint))
209    }
210}
211
212/// The name of a security key by its maker, then its serial number as printed
213/// on it: "YubiKey 12 345 678", "Nitrokey 000F 1A2B".
214pub fn label(manufacturer: u16, serial: u32, tr: &Translator) -> String {
215    let maker = match manufacturer {
216        0x0006 => "YubiKey".to_string(),
217        0x000F => "Nitrokey".to_string(),
218        0xF1D0 => "CanoKey".to_string(),
219        0xF517 => "Gnuk".to_string(),
220        0x0005 => "ZeitControl".to_string(),
221        _ => tr.text("seckey-generic", None),
222    };
223    format!("{maker} {}", serial_text(serial))
224}
225
226/// A serial number as printed on the key. Yubico writes its serials in decimal
227/// digits inside the card's hexadecimal ones ("12345678" reads 0x12345678), so
228/// they are shown in groups of three, as a number; others stay hexadecimal.
229pub fn serial_text(serial: u32) -> String {
230    let digits = format!("{serial:08X}");
231    let digits = digits.trim_start_matches('0');
232    let digits = if digits.is_empty() { "0" } else { digits };
233    if !digits.chars().all(|c| c.is_ascii_digit()) {
234        return digits.to_string();
235    }
236    let mut out = String::new();
237    for (i, c) in digits.chars().enumerate() {
238        if i > 0 && (digits.len() - i) % 3 == 0 {
239            out.push('\u{202F}');
240        }
241        out.push(c);
242    }
243    out
244}
245
246/// "Ed25519", "Curve25519", "RSA 4096", "NIST P-256": a key's algorithm, as
247/// GnuPG and YubiKey Manager name it.
248pub fn algorithm_name(key: &PublicKey) -> String {
249    let curve = |curve: &Curve| match curve {
250        Curve::Ed25519 => "Ed25519".to_string(),
251        Curve::Cv25519 => "Curve25519".to_string(),
252        Curve::NistP256 => "NIST P-256".to_string(),
253        Curve::NistP384 => "NIST P-384".to_string(),
254        Curve::NistP521 => "NIST P-521".to_string(),
255        Curve::BrainpoolP256 => "brainpoolP256r1".to_string(),
256        Curve::BrainpoolP384 => "brainpoolP384r1".to_string(),
257        Curve::BrainpoolP512 => "brainpoolP512r1".to_string(),
258        other => other.to_string(),
259    };
260    match key.mpis() {
261        mpi::PublicKey::RSA { n, .. } => format!("RSA {}", n.bits()),
262        mpi::PublicKey::EdDSA { curve: c, .. } | mpi::PublicKey::ECDSA { curve: c, .. } | mpi::PublicKey::ECDH { curve: c, .. } => curve(c),
263        mpi::PublicKey::Ed25519 { .. } => "Ed25519".into(),
264        mpi::PublicKey::X25519 { .. } => "X25519".into(),
265        _ => key.pk_algo().to_string(),
266    }
267}
268
269/// Why a security key could not be used, each with its sentence.
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub enum CardError {
272    /// This build of Sioul reaches no security key.
273    Unsupported,
274    /// The system's smart card service does not answer: pcscd on Linux, the
275    /// Smart Card service on Windows.
276    NoService,
277    /// No security key is plugged in, or not the one wanted.
278    Absent,
279    /// Another program holds the key for itself: GnuPG, most often (a
280    /// "sharing violation").
281    Busy,
282    /// The PIN was wrong; tries left.
283    WrongPin {
284        /// Tries left.
285        left: u8,
286    },
287    /// The PIN is locked, after three wrong ones.
288    Blocked,
289    /// The key waited for a touch that did not come.
290    TouchMissed,
291    /// The key went away in the middle.
292    Removed,
293    /// What is plugged in holds no OpenPGP keys.
294    NotOpenPgp,
295    /// The key no longer holds the keys Sioul knows: its fingerprints changed.
296    OtherKeys,
297    /// The key signed, but not with the key its certificate names.
298    BadSignature,
299    /// An algorithm Sioul does not use with a security key.
300    Algorithm(String),
301    /// Anything else, as the system said it.
302    Other(String),
303}
304
305/// What the window offers after a failure.
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum Remedy {
308    /// Nothing but to go on without the key.
309    None,
310    /// "Try again".
311    Again,
312    /// "Let GnuPG release it", then try again.
313    Release,
314    /// The PIN asked again.
315    Pin,
316}
317
318impl CardError {
319    /// The failure in words, with what to do.
320    pub fn sentence(&self, tr: &Translator, purpose: Purpose) -> String {
321        match self {
322            CardError::Unsupported => tr.text("seckey-unsupported", None),
323            CardError::NoService => say(tr, "seckey-no-service", &[("hint", no_service_hint(tr))]),
324            CardError::Absent => tr.text(
325                match purpose {
326                    Purpose::Sign => "seckey-absent-sign",
327                    Purpose::Open => "seckey-absent-open",
328                    Purpose::Read => "seckey-absent",
329                },
330                None,
331            ),
332            CardError::Busy => tr.text("seckey-busy", None),
333            CardError::WrongPin { left: 0 } | CardError::Blocked => tr.text("seckey-blocked", None),
334            CardError::WrongPin { left } => tr.text("seckey-wrong-pin", Some(&tr.counted(usize::from(*left)))),
335            CardError::TouchMissed => tr.text("seckey-touch-missed", None),
336            CardError::Removed => tr.text(if purpose == Purpose::Sign { "seckey-removed-sign" } else { "seckey-removed" }, None),
337            CardError::NotOpenPgp => tr.text("seckey-not-openpgp", None),
338            CardError::OtherKeys => tr.text("seckey-other-keys", None),
339            CardError::BadSignature => tr.text("seckey-bad-signature", None),
340            CardError::Algorithm(algorithm) => say(tr, "seckey-algorithm", &[("algorithm", algorithm.clone())]),
341            CardError::Other(detail) => say(tr, "seckey-other", &[("detail", detail.clone())]),
342        }
343    }
344
345    /// What the window offers next.
346    pub fn remedy(&self) -> Remedy {
347        match self {
348            CardError::Busy => Remedy::Release,
349            CardError::WrongPin { left } if *left > 0 => Remedy::Pin,
350            CardError::NoService | CardError::Absent | CardError::TouchMissed | CardError::Removed | CardError::Other(_) => Remedy::Again,
351            _ => Remedy::None,
352        }
353    }
354}
355
356impl std::fmt::Display for CardError {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        write!(f, "{self:?}")
359    }
360}
361
362impl std::error::Error for CardError {}
363
364fn say(tr: &Translator, id: &str, pairs: &[(&str, String)]) -> String {
365    let mut args = crate::i18n::args();
366    for (name, value) in pairs {
367        args.set(*name, value.clone());
368    }
369    tr.text(id, Some(&args))
370}
371
372/// How to start the smart card service, for the system Sioul runs on.
373fn no_service_hint(tr: &Translator) -> String {
374    let id = if cfg!(windows) {
375        "seckey-hint-windows"
376    } else if cfg!(target_os = "macos") {
377        "seckey-hint-macos"
378    } else if Path::new("/.flatpak-info").exists() {
379        "seckey-hint-flatpak"
380    } else {
381        hint_for_os_release(&std::fs::read_to_string("/etc/os-release").unwrap_or_default())
382    };
383    tr.text(id, None)
384}
385
386/// The hint for a Linux system, by its `/etc/os-release` (ID, then ID_LIKE).
387fn hint_for_os_release(text: &str) -> &'static str {
388    let value = |name: &str| text.lines().find_map(|l| l.strip_prefix(name)?.strip_prefix('=')).map(|v| v.trim_matches('"').to_ascii_lowercase()).unwrap_or_default();
389    let family = format!("{} {}", value("ID"), value("ID_LIKE"));
390    let has = |words: &[&str]| family.split_whitespace().any(|w| words.contains(&w));
391    if has(&["fedora", "rhel", "centos"]) {
392        "seckey-hint-fedora"
393    } else if has(&["debian", "ubuntu"]) {
394        "seckey-hint-debian"
395    } else if has(&["arch"]) {
396        "seckey-hint-arch"
397    } else if has(&["suse", "opensuse", "opensuse-leap", "opensuse-tumbleweed"]) {
398        "seckey-hint-suse"
399    } else {
400        "seckey-hint-linux"
401    }
402}
403
404/// What the card is given to sign: a digest as it is (EdDSA, ECDSA), or, for
405/// RSA, a digest and its hash, which `openpgp-card` builds into a DigestInfo
406/// (`signature_for_hash`, specification §7.2.10.2) before the card pads and
407/// signs it.
408#[derive(Debug, Clone, Copy)]
409pub enum ToSign<'a> {
410    /// EdDSA and ECDSA: the digest itself (cut to the curve's size for ECDSA).
411    Digest(&'a [u8]),
412    /// RSA: the digest and its hash.
413    Rsa {
414        /// SHA-256, SHA-384 or SHA-512.
415        hash: HashAlgorithm,
416        /// The digest.
417        digest: &'a [u8],
418    },
419}
420
421/// What the card opens (specification §7.2.11): RSA's ciphertext, giving the
422/// session key; or, for ECDH, the sender's ephemeral point, giving the shared
423/// secret, from which Sioul unwraps the session key.
424#[derive(Debug, Clone, Copy)]
425pub enum ToDecipher<'a> {
426    /// RSA's ciphertext.
427    Rsa(&'a [u8]),
428    /// The ephemeral point (Curve25519's without its 0x40).
429    Ecdh(&'a [u8]),
430}
431
432/// A security key, open for one operation: what it says, its PIN, and its two
433/// private operations. The opening and the closing are the [`Readers`]'.
434pub trait Card {
435    /// What it says without its PIN.
436    fn info(&mut self) -> Result<CardInfo, CardError>;
437    /// Gives it its PIN; a wrong one costs a try.
438    fn verify(&mut self, pin: &Password, mode: PinMode) -> Result<(), CardError>;
439    /// Signs with its signing key: r ‖ s for EdDSA and ECDSA, s for RSA.
440    fn sign(&mut self, input: ToSign<'_>) -> Result<Vec<u8>, CardError>;
441    /// Opens with its decryption key.
442    fn decipher(&mut self, input: ToDecipher<'_>) -> Result<Protected, CardError>;
443}
444
445/// Whether [`Readers::visit`] goes on to the next key.
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum Visit {
448    /// The next key, if any.
449    Next,
450    /// Done: the others stay closed.
451    Done,
452}
453
454/// What a visit met.
455#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
456pub struct Visited {
457    /// Keys opened and visited.
458    pub opened: usize,
459    /// Keys another program held for itself: not opened.
460    pub busy: usize,
461}
462
463/// Where Sioul finds security keys: the system's smart card service on a
464/// computer, a phone's NFC or USB, a software stand-in in tests.
465pub trait Readers: Send + Sync {
466    /// The readers holding a key now, by name, without opening any: to see a
467    /// key go away.
468    fn present(&self) -> Result<Vec<String>, CardError>;
469    /// Opens each key present in turn, in shared mode, and gives it to `visit`
470    /// until `visit` is done; each is closed afterwards, which resets it.
471    fn visit(&self, visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError>;
472}
473
474/// A build without a way to security keys: each use says so.
475pub struct NoReaders;
476
477impl Readers for NoReaders {
478    fn present(&self) -> Result<Vec<String>, CardError> {
479        Err(CardError::Unsupported)
480    }
481
482    fn visit(&self, _visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError> {
483        Err(CardError::Unsupported)
484    }
485}
486
487/// What every security key present says.
488pub fn cards(readers: &dyn Readers) -> Result<Vec<CardInfo>, CardError> {
489    let mut found = Vec::new();
490    let mut failure = None;
491    let visited = readers.visit(&mut |card| {
492        match card.info() {
493            Ok(info) => found.push(info),
494            Err(e) => failure = Some(e),
495        }
496        Visit::Next
497    })?;
498    if found.is_empty() {
499        return Err(if visited.busy > 0 {
500            CardError::Busy
501        } else if let Some(e) = failure {
502            e
503        } else {
504            CardError::Absent
505        });
506    }
507    Ok(found)
508}
509
510/// Opens the key `ident` for `work`, given what it says; Absent when it is not
511/// there, Busy when another program holds a key Sioul could not open.
512pub fn with_card<R>(readers: &dyn Readers, ident: &str, work: impl FnOnce(&mut dyn Card, &CardInfo) -> Result<R, CardError>) -> Result<R, CardError> {
513    let mut work = Some(work);
514    let mut result = None;
515    let visited = readers.visit(&mut |card| {
516        let Ok(info) = card.info() else { return Visit::Next };
517        if !info.ident.eq_ignore_ascii_case(ident) {
518            return Visit::Next;
519        }
520        if let Some(work) = work.take() {
521            result = Some(work(card, &info));
522        }
523        Visit::Done
524    })?;
525    match result {
526        Some(result) => result,
527        None if visited.busy > 0 => Err(CardError::Busy),
528        None => Err(CardError::Absent),
529    }
530}
531
532/// What it takes to use a security key now: where to find it, its PIN, and
533/// what to say when it waits for a touch.
534pub struct CardAccess<'a> {
535    /// Where security keys are.
536    pub readers: &'a dyn Readers,
537    /// The PIN, given or held.
538    pub pin: &'a Password,
539    /// Called when the key is about to wait for a touch.
540    pub touch: &'a (dyn Fn() + Sync),
541    /// What went wrong: Sequoia carries only its own errors.
542    failure: Mutex<Option<CardError>>,
543    /// The reader the key was in, once used.
544    reader: Mutex<Option<String>>,
545    /// The key took the PIN, whatever came after.
546    accepted: std::sync::atomic::AtomicBool,
547}
548
549impl<'a> CardAccess<'a> {
550    /// Access to the security keys of `readers`, with `pin`.
551    pub fn new(readers: &'a dyn Readers, pin: &'a Password, touch: &'a (dyn Fn() + Sync)) -> CardAccess<'a> {
552        CardAccess { readers, pin, touch, failure: Mutex::new(None), reader: Mutex::new(None), accepted: std::sync::atomic::AtomicBool::new(false) }
553    }
554
555    /// What went wrong at the last use, taken.
556    pub fn failure(&self) -> Option<CardError> {
557        self.failure.lock().ok().and_then(|mut f| f.take())
558    }
559
560    /// The reader the key was in when it worked: to forget the PIN when it goes.
561    pub fn reader(&self) -> Option<String> {
562        self.reader.lock().ok().and_then(|r| r.clone())
563    }
564
565    /// Whether the key took the PIN, even when what came after failed (a
566    /// touch missed): the PIN may then be held, not asked again.
567    pub fn pin_accepted(&self) -> bool {
568        self.accepted.load(std::sync::atomic::Ordering::Relaxed)
569    }
570
571    /// Keeps `error` for the caller, and gives it back for Sequoia to carry.
572    fn fail(&self, error: CardError) -> CardError {
573        if let Ok(mut failure) = self.failure.lock() {
574            *failure = Some(error.clone());
575        }
576        error
577    }
578
579    /// Opens the key `ident`, checks that its slot still holds `public`, gives
580    /// the PIN in `mode`, says when it waits for a touch, and runs `work`.
581    fn run<R>(&self, ident: &str, public: &PublicKey, mode: PinMode, work: impl FnOnce(&mut dyn Card) -> Result<R, CardError>) -> Result<R, CardError> {
582        let fingerprint = public.fingerprint().to_hex();
583        with_card(self.readers, ident, |card, info| {
584            let slot = match mode {
585                PinMode::Sign => info.sign.as_ref(),
586                PinMode::Decrypt => info.decrypt.as_ref(),
587            };
588            let slot = slot.ok_or(CardError::OtherKeys)?;
589            if !slot.fingerprint.eq_ignore_ascii_case(&fingerprint) {
590                return Err(CardError::OtherKeys);
591            }
592            card.verify(self.pin, mode)?;
593            self.accepted.store(true, std::sync::atomic::Ordering::Relaxed);
594            if let Ok(mut reader) = self.reader.lock() {
595                *reader = Some(info.reader.clone());
596            }
597            if slot.touch.asked() {
598                (self.touch)();
599            }
600            work(card)
601        })
602    }
603}
604
605/// Signs with the key of a security key, as Sequoia's `Signer`: the key is
606/// opened when Sequoia asks for the signature, and each signature is checked
607/// with the certificate's key before it is given back.
608pub struct CardSigner<'a> {
609    access: &'a CardAccess<'a>,
610    ident: String,
611    public: PublicKey,
612}
613
614impl<'a> CardSigner<'a> {
615    /// Signs with `public`, the key of the security key `ident`.
616    pub fn new(access: &'a CardAccess<'a>, ident: &str, public: PublicKey) -> CardSigner<'a> {
617        CardSigner { access, ident: ident.to_string(), public }
618    }
619
620    fn sign_on_card(&self, hash: HashAlgorithm, digest: &[u8]) -> Result<mpi::Signature, CardError> {
621        let input = match self.public.mpis() {
622            mpi::PublicKey::RSA { .. } => match hash {
623                HashAlgorithm::SHA256 | HashAlgorithm::SHA384 | HashAlgorithm::SHA512 => ToSign::Rsa { hash, digest },
624                other => return Err(CardError::Algorithm(format!("RSA, {other}"))),
625            },
626            mpi::PublicKey::EdDSA { .. } | mpi::PublicKey::Ed25519 { .. } => ToSign::Digest(digest),
627            mpi::PublicKey::ECDSA { curve, .. } => {
628                let size = curve.field_size().map_err(|e| CardError::Algorithm(e.to_string()))?;
629                ToSign::Digest(&digest[..digest.len().min(size)])
630            }
631            _ => return Err(CardError::Algorithm(algorithm_name(&self.public))),
632        };
633        let raw = self.access.run(&self.ident, &self.public, PinMode::Sign, |card| card.sign(input))?;
634        let half = raw.len() / 2;
635        let signature = match self.public.mpis() {
636            mpi::PublicKey::RSA { .. } => mpi::Signature::RSA { s: mpi::MPI::new(&raw) },
637            mpi::PublicKey::EdDSA { .. } if raw.len() == 64 => mpi::Signature::EdDSA { r: mpi::MPI::new(&raw[..32]), s: mpi::MPI::new(&raw[32..]) },
638            mpi::PublicKey::Ed25519 { .. } => mpi::Signature::Ed25519 { s: Box::new(<[u8; 64]>::try_from(raw.as_slice()).map_err(|_| CardError::BadSignature)?) },
639            mpi::PublicKey::ECDSA { .. } if raw.len() % 2 == 0 => mpi::Signature::ECDSA { r: mpi::MPI::new(&raw[..half]), s: mpi::MPI::new(&raw[half..]) },
640            _ => return Err(CardError::BadSignature),
641        };
642        // A key that signs with another key never sends a broken signature.
643        self.public.verify(&signature, hash, digest).map_err(|_| CardError::BadSignature)?;
644        Ok(signature)
645    }
646}
647
648impl crypto::Signer for CardSigner<'_> {
649    fn public(&self) -> &PublicKey {
650        &self.public
651    }
652
653    fn acceptable_hashes(&self) -> &[HashAlgorithm] {
654        match self.public.mpis() {
655            // `openpgp-card` builds the DigestInfo (`signature_for_hash`) for the hashes it knows: SHA-2 only.
656            mpi::PublicKey::RSA { .. } => &[HashAlgorithm::SHA512, HashAlgorithm::SHA384, HashAlgorithm::SHA256],
657            _ => &[HashAlgorithm::SHA512, HashAlgorithm::SHA384, HashAlgorithm::SHA256, HashAlgorithm::SHA224],
658        }
659    }
660
661    fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result<mpi::Signature> {
662        self.sign_on_card(hash_algo, digest).map_err(|e| self.access.fail(e).into())
663    }
664}
665
666/// Opens session keys with the key of a security key, as Sequoia's
667/// `Decryptor`: the key is opened when Sequoia asks.
668pub struct CardDecryptor<'a> {
669    access: &'a CardAccess<'a>,
670    ident: String,
671    public: PublicKey,
672}
673
674impl<'a> CardDecryptor<'a> {
675    /// Opens with `public`, the decryption key of the security key `ident`.
676    pub fn new(access: &'a CardAccess<'a>, ident: &str, public: PublicKey) -> CardDecryptor<'a> {
677        CardDecryptor { access, ident: ident.to_string(), public }
678    }
679
680    fn decrypt_on_card(&self, ciphertext: &mpi::Ciphertext, plaintext_len: Option<usize>) -> Result<SessionKey, CardError> {
681        match (ciphertext, self.public.mpis()) {
682            (mpi::Ciphertext::RSA { c }, mpi::PublicKey::RSA { .. }) => {
683                let opened = self.access.run(&self.ident, &self.public, PinMode::Decrypt, |card| card.decipher(ToDecipher::Rsa(c.value())))?;
684                Ok(SessionKey::from(&opened[..]))
685            }
686            (mpi::Ciphertext::ECDH { e, .. }, mpi::PublicKey::ECDH { curve, .. }) => {
687                // Curve25519's point is sent with OpenPGP's 0x40 before it, which the card does not take.
688                let point = match curve {
689                    Curve::Cv25519 => e.value().strip_prefix(&[0x40]).ok_or_else(|| CardError::Other("malformed Curve25519 point".into()))?,
690                    _ => e.value(),
691                };
692                let shared = self.access.run(&self.ident, &self.public, PinMode::Decrypt, |card| card.decipher(ToDecipher::Ecdh(point)))?;
693                // Gnuk gives back 0x04 ‖ x ‖ y for NIST P-256: the shared secret is x.
694                let shared = if *curve == Curve::NistP256 && shared.len() == 65 && shared[0] == 0x04 { Protected::from(&shared[1..33]) } else { shared };
695                crypto::ecdh::decrypt_unwrap(&self.public, &shared, ciphertext, plaintext_len).map_err(|e| CardError::Other(e.to_string()))
696            }
697            _ => Err(CardError::Algorithm(algorithm_name(&self.public))),
698        }
699    }
700}
701
702impl crypto::Decryptor for CardDecryptor<'_> {
703    fn public(&self) -> &PublicKey {
704        &self.public
705    }
706
707    fn decrypt(&mut self, ciphertext: &mpi::Ciphertext, plaintext_len: Option<usize>) -> openpgp::Result<SessionKey> {
708        self.decrypt_on_card(ciphertext, plaintext_len).map_err(|e| self.access.fail(e).into())
709    }
710}
711
712/// How long a PIN stays in memory after its last use (the owner's choice: 15 minutes).
713pub const PIN_QUIET: Duration = Duration::from_secs(15 * 60);
714
715/// The PIN of one security key, held in memory only, encrypted there by
716/// Sequoia (`Password`), for [`PIN_QUIET`] after its last use. Never on disk,
717/// in the keyring or in a log. Forgotten when Sioul closes, when the key goes
718/// away (on a computer), or on "Forget the PIN now".
719#[derive(Default)]
720pub struct PinMemory {
721    held: Option<Held>,
722}
723
724struct Held {
725    ident: String,
726    pin: Password,
727    used: Instant,
728    reader: String,
729}
730
731impl PinMemory {
732    /// Nothing held.
733    pub const fn new() -> PinMemory {
734        PinMemory { held: None }
735    }
736
737    /// Holds `pin` for the key `ident`, in `reader`, from `now`.
738    pub fn keep(&mut self, ident: &str, pin: Password, reader: &str, now: Instant) {
739        self.held = Some(Held { ident: ident.to_string(), pin, used: now, reader: reader.to_string() });
740    }
741
742    /// The PIN held for `ident`, when its quiet time is not over.
743    pub fn get(&mut self, ident: &str, now: Instant) -> Option<Password> {
744        self.expire(now);
745        self.held.as_ref().filter(|h| h.ident.eq_ignore_ascii_case(ident)).map(|h| h.pin.clone())
746    }
747
748    /// The PIN of `ident` worked again: its quiet time starts again.
749    pub fn used(&mut self, ident: &str, now: Instant) {
750        if let Some(held) = self.held.as_mut().filter(|h| h.ident.eq_ignore_ascii_case(ident)) {
751            held.used = now;
752        }
753    }
754
755    /// Forgets the PIN.
756    pub fn forget(&mut self) {
757        self.held = None;
758    }
759
760    /// Forgets the PIN when its quiet time is over; whether it did.
761    pub fn expire(&mut self, now: Instant) -> bool {
762        let stale = self.held.as_ref().is_some_and(|h| now.saturating_duration_since(h.used) >= PIN_QUIET);
763        if stale {
764            self.held = None;
765        }
766        stale
767    }
768
769    /// Forgets the PIN when its key's reader is no longer among those `present`; whether it did.
770    pub fn went_away(&mut self, present: &[String]) -> bool {
771        let gone = self.held.as_ref().is_some_and(|h| !h.reader.is_empty() && !present.iter().any(|r| *r == h.reader));
772        if gone {
773            self.held = None;
774        }
775        gone
776    }
777
778    /// The key whose PIN is held, if any.
779    pub fn holder(&self) -> Option<&str> {
780        self.held.as_ref().map(|h| h.ident.as_str())
781    }
782}
783
784/// A security key Sioul knows, in `pgp/cards.toml`: which keys it holds, the
785/// certificate they belong to and where it came from. No secret.
786#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
787pub struct KnownCard {
788    /// "0006:12345678", as [`CardInfo::ident`].
789    pub ident: String,
790    /// Its maker's number.
791    #[serde(default)]
792    pub manufacturer: u16,
793    /// Its serial number.
794    #[serde(default)]
795    pub serial: u32,
796    /// The name written on it.
797    #[serde(default)]
798    pub holder: String,
799    /// The fingerprint of its signing key; empty when it holds none.
800    #[serde(default)]
801    pub sign: String,
802    /// The fingerprint of its decryption key; empty when it holds none.
803    #[serde(default)]
804    pub decrypt: String,
805    /// The certificate both belong to, by its primary key's fingerprint.
806    pub cert: String,
807    /// Your addresses it signs for (decision 2: it alone signs for them).
808    #[serde(default)]
809    pub addresses: Vec<String>,
810    /// Where its certificate came from: an address, or "file".
811    #[serde(default)]
812    pub source: String,
813    /// When the certificate was checked against the key (Unix seconds).
814    #[serde(default)]
815    pub checked: i64,
816    /// The most PIN tries the key was seen with: fewer are said.
817    #[serde(default)]
818    pub pin_tries_full: u8,
819    /// Whether its signing key waits for a touch, as last read.
820    #[serde(default)]
821    pub touch_sign: Touch,
822    /// Whether its decryption key waits for a touch, as last read.
823    #[serde(default)]
824    pub touch_decrypt: Touch,
825    /// The PIN is asked for each signature.
826    #[serde(default)]
827    pub pin_each_signature: bool,
828}
829
830impl KnownCard {
831    /// Whether it signs for `address`.
832    pub fn serves(&self, address: &str) -> bool {
833        self.addresses.iter().any(|a| a.eq_ignore_ascii_case(address.trim()))
834    }
835
836    /// "YubiKey 12 345 678".
837    pub fn label(&self, tr: &Translator) -> String {
838        label(self.manufacturer, self.serial, tr)
839    }
840
841    /// What the key said now, kept: touch settings, the PIN for each
842    /// signature, the most tries seen.
843    pub fn update(&mut self, info: &CardInfo) {
844        self.holder = info.holder.clone();
845        self.touch_sign = info.sign.as_ref().map(|s| s.touch).unwrap_or_default();
846        self.touch_decrypt = info.decrypt.as_ref().map(|s| s.touch).unwrap_or_default();
847        self.pin_each_signature = info.pin_each_signature;
848        self.pin_tries_full = self.pin_tries_full.max(info.pin_tries).max(3);
849    }
850
851    /// The tries left, said when fewer than the most seen: "2 tries left."; else nothing.
852    pub fn tries_line(&self, left: u8, tr: &Translator) -> Option<String> {
853        (left < self.pin_tries_full.max(3)).then(|| tr.text("seckey-tries-left", Some(&tr.counted(usize::from(left)))))
854    }
855}
856
857#[derive(Default, Serialize, Deserialize)]
858struct CardsFile {
859    #[serde(default, rename = "card")]
860    cards: Vec<KnownCard>,
861}
862
863/// Where the known security keys are listed.
864pub fn cards_path() -> PathBuf {
865    data_dir().join("pgp").join("cards.toml")
866}
867
868/// The security keys Sioul knows.
869pub fn known_cards() -> Vec<KnownCard> {
870    read_known(&cards_path())
871}
872
873fn read_known(path: &Path) -> Vec<KnownCard> {
874    std::fs::read_to_string(path).ok().and_then(|text| toml::from_str::<CardsFile>(&text).ok()).map(|f| f.cards).unwrap_or_default()
875}
876
877/// Changes the list of known security keys: read, changed, written, one writer at a time.
878pub fn change_known<T>(change: impl FnOnce(&mut Vec<KnownCard>) -> T) -> Result<T, String> {
879    change_known_at(&cards_path(), change)
880}
881
882fn change_known_at<T>(path: &Path, change: impl FnOnce(&mut Vec<KnownCard>) -> T) -> Result<T, String> {
883    crate::filelock::with_lock(path, || {
884        let mut cards = read_known(path);
885        let result = change(&mut cards);
886        let text = toml::to_string(&CardsFile { cards }).map_err(|e| e.to_string())?;
887        let fail = |e: std::io::Error| format!("{}: {e}", path.display());
888        if let Some(folder) = path.parent() {
889            std::fs::create_dir_all(folder).map_err(fail)?;
890        }
891        let temporary = path.with_extension("toml.new");
892        std::fs::write(&temporary, text).map_err(fail)?;
893        std::fs::rename(&temporary, path).map_err(fail)?;
894        Ok(result)
895    })
896}
897
898/// A certificate checked against a security key: it belongs to it.
899#[derive(Debug, Clone, PartialEq, Eq)]
900pub struct CertCheck {
901    /// The certificate's fingerprint.
902    pub cert: String,
903    /// The addresses its user IDs name, lower case.
904    pub addresses: Vec<String>,
905    /// When it stops being valid, its primary key or the keys on the
906    /// security key, the earliest (Unix seconds); None when it does not expire.
907    pub expires: Option<i64>,
908    /// It already expired.
909    pub expired: bool,
910    /// The security key signs for it.
911    pub signs: bool,
912    /// The security key opens what is encrypted to it.
913    pub decrypts: bool,
914}
915
916/// Why a certificate is not the one of a security key.
917#[derive(Debug, Clone, PartialEq, Eq)]
918pub enum Mismatch {
919    /// The security key holds no key to sign or to open with.
920    Empty,
921    /// The certificate lacks a key the security key holds.
922    Missing,
923    /// The certificate, or a key of it, was revoked.
924    Revoked,
925    /// The key in the security key's signing slot may not sign, by its certificate.
926    NotForSigning,
927    /// The key in the decryption slot may not encrypt, by its certificate.
928    NotForEncryption,
929    /// The fingerprints agree, but the public keys differ: the fingerprints on
930    /// the key are only data written to it.
931    OtherPublicKey,
932    /// Sequoia's standard policy refuses the certificate (its reason).
933    Invalid(String),
934}
935
936impl Mismatch {
937    /// The reason in words.
938    pub fn sentence(&self, tr: &Translator) -> String {
939        match self {
940            Mismatch::Empty => tr.text("seckey-mismatch-empty", None),
941            Mismatch::Missing | Mismatch::OtherPublicKey => tr.text("seckey-mismatch", None),
942            Mismatch::Revoked => tr.text("seckey-mismatch-revoked", None),
943            Mismatch::NotForSigning | Mismatch::NotForEncryption => tr.text("seckey-mismatch-use", None),
944            Mismatch::Invalid(why) => say(tr, "seckey-mismatch-invalid", &[("detail", why.clone())]),
945        }
946    }
947}
948
949/// Whether `cert` is the certificate of the security key `card`: each key on
950/// the card is in it, alive or expired but not revoked, allowed to do what its
951/// slot does, and its public part is the card's own.
952pub fn check(cert: &Cert, card: &CardInfo) -> Result<CertCheck, Mismatch> {
953    let policy = StandardPolicy::new();
954    let valid = cert.with_policy(&policy, None).map_err(|e| Mismatch::Invalid(e.to_string()))?;
955    if matches!(valid.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
956        return Err(Mismatch::Revoked);
957    }
958    if card.sign.is_none() && card.decrypt.is_none() {
959        return Err(Mismatch::Empty);
960    }
961    let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
962    let mut expires: Option<i64> = valid.primary_key().key_expiration_time().and_then(unix);
963    let mut earliest = |at: Option<i64>| {
964        if let Some(at) = at {
965            expires = Some(expires.map_or(at, |e| e.min(at)));
966        }
967    };
968    let mut alive = valid.alive().is_ok();
969    for (slot, signing) in [(&card.sign, true), (&card.decrypt, false)] {
970        let Some(slot) = slot else { continue };
971        let key = valid.keys().find(|k| k.key().fingerprint().to_hex().eq_ignore_ascii_case(&slot.fingerprint)).ok_or(Mismatch::Missing)?;
972        if matches!(key.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
973            return Err(Mismatch::Revoked);
974        }
975        let allowed = if signing { key.for_signing() } else { key.for_transport_encryption() || key.for_storage_encryption() };
976        if !allowed {
977            return Err(if signing { Mismatch::NotForSigning } else { Mismatch::NotForEncryption });
978        }
979        if let Some(public) = &slot.public
980            && !same_public(key.key(), public)
981        {
982            return Err(Mismatch::OtherPublicKey);
983        }
984        earliest(key.key_expiration_time().and_then(unix));
985        alive &= key.alive().is_ok();
986    }
987    let mut addresses: Vec<String> = valid.userids().filter_map(|u| u.userid().email().ok().flatten().map(|e| e.to_ascii_lowercase())).collect();
988    addresses.sort();
989    addresses.dedup();
990    let now = unix(SystemTime::now()).unwrap_or(0);
991    Ok(CertCheck {
992        cert: cert.fingerprint().to_hex(),
993        addresses,
994        expires,
995        expired: !alive || expires.is_some_and(|at| at <= now),
996        signs: card.sign.is_some(),
997        decrypts: card.decrypt.is_some(),
998    })
999}
1000
1001/// The certificate of the security key `card` among `bytes` (armored or
1002/// binary, one certificate or several), with what it is; else why none is.
1003pub fn certificate_in(bytes: &[u8], card: &CardInfo) -> Result<(Cert, CertCheck), Mismatch> {
1004    use openpgp::parse::Parse;
1005    let mut why = Mismatch::Missing;
1006    let parser = openpgp::cert::CertParser::from_bytes(bytes).map_err(|e| Mismatch::Invalid(e.to_string()))?;
1007    for cert in parser.filter_map(Result::ok) {
1008        match check(&cert, card) {
1009            Ok(found) => return Ok((cert.strip_secret_key_material(), found)),
1010            // A certificate with the key's fingerprints says more than a stranger's.
1011            Err(Mismatch::Missing) => {}
1012            Err(other) => why = other,
1013        }
1014    }
1015    Err(why)
1016}
1017
1018/// Makes the security key `card` yours: its certificate kept with your keys
1019/// (public parts only), and the key in `cards.toml`, signing for `addresses`;
1020/// `source` says where the certificate came from. Gives what is kept.
1021pub fn adopt(card: &CardInfo, cert: &Cert, source: &str, addresses: Vec<String>, now: i64) -> Result<KnownCard, String> {
1022    crate::pgp::keep_card_certificate(cert)?;
1023    let mut known = KnownCard {
1024        ident: card.ident.clone(),
1025        manufacturer: card.manufacturer,
1026        serial: card.serial,
1027        sign: card.sign.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1028        decrypt: card.decrypt.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1029        cert: cert.fingerprint().to_hex(),
1030        addresses: addresses.into_iter().map(|a| a.trim().to_ascii_lowercase()).collect(),
1031        source: source.to_string(),
1032        checked: now,
1033        ..KnownCard::default()
1034    };
1035    known.update(card);
1036    let kept = known.clone();
1037    change_known(|cards| {
1038        cards.retain(|c| !c.ident.eq_ignore_ascii_case(&kept.ident));
1039        cards.push(kept);
1040    })?;
1041    Ok(known)
1042}
1043
1044/// Stops using the security key `ident`: it leaves `cards.toml`, and its
1045/// certificate leaves your keys for `pgp/removed` (never deleted).
1046pub fn forget(ident: &str) -> Result<(), String> {
1047    let cert = change_known(|cards| {
1048        let cert = cards.iter().find(|c| c.ident.eq_ignore_ascii_case(ident)).map(|c| c.cert.clone());
1049        cards.retain(|c| !c.ident.eq_ignore_ascii_case(ident));
1050        cert
1051    })?;
1052    match cert {
1053        Some(cert) => crate::pgp::remove(&cert),
1054        None => Ok(()),
1055    }
1056}
1057
1058/// Whether a certificate's key is the card's: the same public part.
1059fn same_public(key: &PublicKey, card: &CardPublic) -> bool {
1060    // OpenPGP puts 0x40 before a Curve25519 point (Ed25519, Cv25519); a card does not.
1061    fn native(point: &[u8]) -> &[u8] {
1062        if point.len() == 33 && point[0] == 0x40 { &point[1..] } else { point }
1063    }
1064    fn number(bytes: &[u8]) -> &[u8] {
1065        let zeros = bytes.iter().take_while(|&&b| b == 0).count();
1066        &bytes[zeros..]
1067    }
1068    match (key.mpis(), card) {
1069        (mpi::PublicKey::RSA { e, n }, CardPublic::Rsa { n: card_n, e: card_e }) => number(n.value()) == number(card_n) && number(e.value()) == number(card_e),
1070        (mpi::PublicKey::EdDSA { q, .. } | mpi::PublicKey::ECDSA { q, .. } | mpi::PublicKey::ECDH { q, .. }, CardPublic::Ecc { point }) => native(q.value()) == native(point),
1071        (mpi::PublicKey::Ed25519 { a }, CardPublic::Ecc { point }) => a[..] == *native(point),
1072        (mpi::PublicKey::X25519 { u }, CardPublic::Ecc { point }) => u[..] == *native(point),
1073        _ => false,
1074    }
1075}
1076
1077/// When a certificate stops serving a security key: the earliest expiry of
1078/// its primary key and of its keys named by `fingerprints` (Unix seconds);
1079/// None when none of them expires, or the certificate is not valid at all.
1080pub fn expiry_of(cert: &Cert, fingerprints: &[&str]) -> Option<i64> {
1081    let policy = StandardPolicy::new();
1082    let valid = cert.with_policy(&policy, None).ok()?;
1083    let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
1084    let keys = valid.keys().filter(|k| fingerprints.iter().any(|f| !f.is_empty() && k.key().fingerprint().to_hex().eq_ignore_ascii_case(f))).filter_map(|k| k.key_expiration_time().and_then(unix));
1085    valid.primary_key().key_expiration_time().and_then(unix).into_iter().chain(keys).min()
1086}
1087
1088/// What a key of a certificate is for, as said to you.
1089#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1090pub enum Role {
1091    /// The primary key: it certifies the others (and may sign).
1092    Primary,
1093    /// A subkey that signs.
1094    Signing,
1095    /// A subkey others encrypt to.
1096    Encryption,
1097    /// A subkey that signs you in elsewhere (SSH).
1098    Authentication,
1099    /// A subkey with no use Sioul names.
1100    Other,
1101}
1102
1103/// One key of a certificate: what it is for, its fingerprint, until when
1104/// (Unix seconds; none when it does not expire).
1105#[derive(Debug, Clone, PartialEq, Eq)]
1106pub struct Part {
1107    pub role: Role,
1108    pub fingerprint: String,
1109    pub expires: Option<i64>,
1110}
1111
1112/// The keys of `cert` that are not revoked, the primary key first, each with
1113/// what it is for and until when: what is said of each, never "the key"
1114/// for all of them when one part alone expired.
1115pub fn parts(cert: &Cert) -> Vec<Part> {
1116    let policy = StandardPolicy::new();
1117    let Ok(valid) = cert.with_policy(&policy, None) else { return Vec::new() };
1118    let unix = |t: SystemTime| t.duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|d| i64::try_from(d.as_secs()).ok());
1119    let mut out = vec![Part { role: Role::Primary, fingerprint: cert.fingerprint().to_hex(), expires: valid.primary_key().key_expiration_time().and_then(unix) }];
1120    for key in valid.keys().subkeys() {
1121        if matches!(key.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)) {
1122            continue;
1123        }
1124        let role = if key.for_transport_encryption() || key.for_storage_encryption() {
1125            Role::Encryption
1126        } else if key.for_signing() {
1127            Role::Signing
1128        } else if key.for_authentication() {
1129            Role::Authentication
1130        } else {
1131            Role::Other
1132        };
1133        out.push(Part { role, fingerprint: key.key().fingerprint().to_hex(), expires: key.key_expiration_time().and_then(unix) });
1134    }
1135    // Said in one order whatever the certificate's: signing, encryption, authentication.
1136    out.sort_by_key(|part| part.role as u8);
1137    out
1138}
1139
1140/// The parts of the first certificate among `bytes` (`gpg --export`): none when there is none.
1141pub fn parts_in(bytes: &[u8]) -> Vec<Part> {
1142    use openpgp::parse::Parse;
1143    openpgp::cert::CertParser::from_bytes(bytes).ok().and_then(|mut certs| certs.find_map(Result::ok)).map(|cert| parts(&cert)).unwrap_or_default()
1144}
1145
1146/// "B81A…BF8C": a fingerprint as it is told apart at a glance.
1147pub fn short_fingerprint(fingerprint: &str) -> String {
1148    let hex: String = fingerprint.chars().filter(char::is_ascii_hexdigit).collect::<String>().to_ascii_uppercase();
1149    if hex.len() <= 8 { hex } else { format!("{}…{}", &hex[..4], &hex[hex.len() - 4..]) }
1150}
1151
1152/// Each part that expired by `now`, said exactly: which, since when, and what
1153/// it stops ("Its encryption subkey (B81A…BF8C) expired on 8 April 2025:
1154/// nobody can encrypt to you with it.").
1155pub fn expired_parts(tr: &Translator, parts: &[Part], now: i64) -> Vec<String> {
1156    parts
1157        .iter()
1158        .filter_map(|part| {
1159            let at = part.expires.filter(|at| *at <= now)?;
1160            let id = match part.role {
1161                Role::Primary => "seckey-part-expired-primary",
1162                Role::Signing => "seckey-part-expired-sign",
1163                Role::Encryption => "seckey-part-expired-encrypt",
1164                Role::Authentication => "seckey-part-expired-auth",
1165                Role::Other => "seckey-part-expired-other",
1166            };
1167            Some(say(tr, id, &[("fingerprint", short_fingerprint(&part.fingerprint)), ("date", full_date(tr, at))]))
1168        })
1169        .collect()
1170}
1171
1172/// Every part and until when, in a line: "primary key until 7 October 2028,
1173/// encryption subkey until 7 October 2028"; "without end" for one that does not expire.
1174pub fn parts_until(tr: &Translator, parts: &[Part]) -> String {
1175    parts
1176        .iter()
1177        .map(|part| {
1178            let name = tr.text(
1179                match part.role {
1180                    Role::Primary => "seckey-part-primary",
1181                    Role::Signing => "seckey-part-sign",
1182                    Role::Encryption => "seckey-part-encrypt",
1183                    Role::Authentication => "seckey-part-auth",
1184                    Role::Other => "seckey-part-other",
1185                },
1186                None,
1187            );
1188            match part.expires {
1189                Some(at) => say(tr, "seckey-part-until", &[("part", name), ("date", full_date(tr, at))]),
1190                None => say(tr, "seckey-part-forever", &[("part", name)]),
1191            }
1192        })
1193        .collect::<Vec<_>>()
1194        .join(", ")
1195}
1196
1197/// What a certificate says of its expiry, part by part: each part expired,
1198/// exactly ("Its encryption subkey (B81A…BF8C) expired on 8 April 2025:
1199/// nobody can encrypt to you with it."), and, when its subkeys alone expired,
1200/// that Renew renews them; else until when it is valid, or that it expires
1201/// soon (`expires`, the earliest of the keys that matter). What it says,
1202/// whether warmly, and whether a part expired.
1203pub fn expiry_words(tr: &Translator, cert: &Cert, expires: Option<i64>, now: i64) -> (String, bool, bool) {
1204    let parts = parts(cert);
1205    let expired = expired_parts(tr, &parts, now);
1206    if expired.is_empty() {
1207        return expiry_line(tr, expires, &cert.fingerprint().to_hex(), now).map_or((String::new(), false, false), |(line, late)| (line, late, false));
1208    }
1209    let primary_alive = parts.first().is_some_and(|p| p.expires.is_none_or(|at| at > now));
1210    let mut line = expired.join(" ");
1211    if primary_alive {
1212        line = format!("{line} {}", tr.text("seckey-subkeys-renew", None));
1213    }
1214    (line, true, true)
1215}
1216
1217/// The subkeys of a certificate that are not revoked, by fingerprint: what
1218/// "Renew for two years" names one by one.
1219pub fn subkeys_of(cert: &Cert) -> Vec<String> {
1220    parts(cert).into_iter().filter(|p| p.role != Role::Primary).map(|p| p.fingerprint).collect()
1221}
1222
1223/// "3 March 2026", "3 mars 2026".
1224pub fn full_date(tr: &Translator, at: i64) -> String {
1225    let Ok(stamp) = jiff::Timestamp::from_second(at) else { return String::new() };
1226    let date = stamp.to_zoned(jiff::tz::TimeZone::system()).date();
1227    format!("{} {}", tr.day_month(date), date.year())
1228}
1229
1230/// What the certificate's expiry says now: expired (and how to renew), soon
1231/// (within a month), or valid until a date; nothing when it does not expire.
1232pub fn expiry_line(tr: &Translator, expires: Option<i64>, fingerprint: &str, now: i64) -> Option<(String, bool)> {
1233    let at = expires?;
1234    let date = full_date(tr, at);
1235    let pairs = [("date", date), ("fingerprint", fingerprint.to_string())];
1236    Some(if at <= now {
1237        (say(tr, "seckey-expired", &pairs), true)
1238    } else if at - now <= 31 * 24 * 3600 {
1239        (say(tr, "seckey-expires-soon", &pairs), true)
1240    } else {
1241        (say(tr, "seckey-valid-until", &pairs), false)
1242    })
1243}
1244
1245/// A security key made of software keys, behind the same traits as a real one,
1246/// for tests: Sioul itself never makes one. It counts its PIN tries, forgets a
1247/// verified PIN when closed, asks the PIN again after each signature when told
1248/// to, and can fail as a real key does ([`SoftCard::fault`]).
1249pub struct SoftCard {
1250    /// What it says.
1251    pub info: CardInfo,
1252    signer: Option<crypto::KeyPair>,
1253    decryptor: Option<crypto::KeyPair>,
1254    pin: Vec<u8>,
1255    /// X25519 for its Curve25519 key: Sequoia keeps its own private, so a test lends one.
1256    x25519: Option<fn(&[u8; 32], &[u8; 32]) -> [u8; 32]>,
1257    verified_sign: bool,
1258    verified_decrypt: bool,
1259    /// What its next signature or opening does instead of working: a touch
1260    /// missed, the key pulled out.
1261    pub fault: Option<CardError>,
1262    /// Signatures and openings it made.
1263    pub operations: usize,
1264    /// How long a person takes to touch it, when a key asks for a touch: none in
1265    /// tests; a few seconds in the demo, so that "Touch your security key" shows.
1266    pub touch_delay: Duration,
1267}
1268
1269impl SoftCard {
1270    /// A key named `ident` holding the secret keys of `cert` (its first
1271    /// signing key and its first encryption key), with `pin`.
1272    pub fn new(ident: &str, cert: &Cert, pin: &str) -> SoftCard {
1273        let policy = StandardPolicy::new();
1274        let pair = |signing: bool| {
1275            cert.keys()
1276                .unencrypted_secret()
1277                .with_policy(&policy, None)
1278                .find(|k| if signing { k.for_signing() } else { k.for_transport_encryption() })
1279                .and_then(|k| k.key().clone().into_keypair().ok())
1280        };
1281        let (signer, decryptor) = (pair(true), pair(false));
1282        let slot = |pair: &Option<crypto::KeyPair>| {
1283            pair.as_ref().map(|p| SlotInfo {
1284                fingerprint: p.public().fingerprint().to_hex(),
1285                created: openpgp::types::Timestamp::try_from(p.public().creation_time()).map(u32::from).unwrap_or(0),
1286                algorithm: algorithm_name(p.public()),
1287                touch: Touch::Off,
1288                public: card_public(p.public()),
1289            })
1290        };
1291        let (manufacturer, serial) = ident.split_once(':').map_or((0, 0), |(m, s)| (u16::from_str_radix(m, 16).unwrap_or(0), u32::from_str_radix(s, 16).unwrap_or(0)));
1292        SoftCard {
1293            info: CardInfo {
1294                ident: ident.to_string(),
1295                manufacturer,
1296                serial,
1297                holder: String::new(),
1298                url: String::new(),
1299                sign: slot(&signer),
1300                decrypt: slot(&decryptor),
1301                pin_tries: 3,
1302                reset_tries: 0,
1303                admin_tries: 3,
1304                pin_each_signature: false,
1305                signatures: 0,
1306                reader: format!("Soft reader {ident}"),
1307            },
1308            signer,
1309            decryptor,
1310            pin: pin.as_bytes().to_vec(),
1311            x25519: None,
1312            verified_sign: false,
1313            verified_decrypt: false,
1314            fault: None,
1315            operations: 0,
1316            touch_delay: Duration::ZERO,
1317        }
1318    }
1319
1320    /// A new key for tests (Curve25519: its primary key signs, a subkey
1321    /// decrypts), named by `userids`, on a software key `ident` with `pin`;
1322    /// and its certificate, armored, public parts only. For the crates that
1323    /// do not use Sequoia themselves.
1324    pub fn generate(ident: &str, userids: &[&str], pin: &str) -> Result<(SoftCard, Vec<u8>), String> {
1325        use openpgp::serialize::SerializeInto;
1326        let mut builder = CertBuilder::new().set_cipher_suite(CipherSuite::Cv25519).set_primary_key_flags(openpgp::types::KeyFlags::empty().set_certification().set_signing()).add_transport_encryption_subkey();
1327        for userid in userids {
1328            builder = builder.add_userid(*userid);
1329        }
1330        let (cert, _) = builder.generate().map_err(|e| e.to_string())?;
1331        let public = cert.clone().strip_secret_key_material().armored().to_vec().map_err(|e| e.to_string())?;
1332        Ok((SoftCard::new(ident, &cert, pin), public))
1333    }
1334
1335    /// Lends it X25519, for its Curve25519 decryption key.
1336    pub fn with_x25519(mut self, x25519: fn(&[u8; 32], &[u8; 32]) -> [u8; 32]) -> SoftCard {
1337        self.x25519 = Some(x25519);
1338        self
1339    }
1340
1341    /// Closed: a real key is reset, and its PIN must be given again.
1342    fn close(&mut self) {
1343        self.verified_sign = false;
1344        self.verified_decrypt = false;
1345    }
1346}
1347
1348/// The card's form of a public key (its point without OpenPGP's 0x40).
1349fn card_public(key: &PublicKey) -> Option<CardPublic> {
1350    let native = |q: &mpi::MPI| {
1351        let v = q.value();
1352        if v.len() == 33 && v[0] == 0x40 { v[1..].to_vec() } else { v.to_vec() }
1353    };
1354    match key.mpis() {
1355        mpi::PublicKey::RSA { e, n } => Some(CardPublic::Rsa { n: n.value().to_vec(), e: e.value().to_vec() }),
1356        mpi::PublicKey::EdDSA { q, .. } | mpi::PublicKey::ECDSA { q, .. } | mpi::PublicKey::ECDH { q, .. } => Some(CardPublic::Ecc { point: native(q) }),
1357        mpi::PublicKey::Ed25519 { a } => Some(CardPublic::Ecc { point: a.to_vec() }),
1358        _ => None,
1359    }
1360}
1361
1362impl Card for SoftCard {
1363    fn info(&mut self) -> Result<CardInfo, CardError> {
1364        Ok(self.info.clone())
1365    }
1366
1367    fn verify(&mut self, pin: &Password, mode: PinMode) -> Result<(), CardError> {
1368        if self.info.pin_tries == 0 {
1369            return Err(CardError::Blocked);
1370        }
1371        if pin.map(|p| p[..] == self.pin[..]) {
1372            self.info.pin_tries = 3;
1373            match mode {
1374                PinMode::Sign => self.verified_sign = true,
1375                PinMode::Decrypt => self.verified_decrypt = true,
1376            }
1377            Ok(())
1378        } else {
1379            self.info.pin_tries -= 1;
1380            Err(CardError::WrongPin { left: self.info.pin_tries })
1381        }
1382    }
1383
1384    fn sign(&mut self, input: ToSign<'_>) -> Result<Vec<u8>, CardError> {
1385        if !self.verified_sign {
1386            return Err(CardError::Other("security status not satisfied".into()));
1387        }
1388        if let Some(fault) = self.fault.take() {
1389            return Err(fault);
1390        }
1391        if self.info.sign.as_ref().is_some_and(|s| s.touch.asked()) {
1392            std::thread::sleep(self.touch_delay);
1393        }
1394        let pair = self.signer.as_mut().ok_or(CardError::NotOpenPgp)?;
1395        let (hash, digest) = match input {
1396            ToSign::Digest(digest) => (HashAlgorithm::SHA512, digest),
1397            ToSign::Rsa { hash, digest } => (hash, digest),
1398        };
1399        let signature = crypto::Signer::sign(pair, hash, digest).map_err(|e| CardError::Other(e.to_string()))?;
1400        let pad = |m: &mpi::MPI, size: usize| m.value_padded(size).map(|v| v.to_vec()).map_err(|e| CardError::Other(e.to_string()));
1401        let raw = match (&signature, pair.public().mpis()) {
1402            (mpi::Signature::RSA { s }, mpi::PublicKey::RSA { n, .. }) => pad(s, n.value().len())?,
1403            (mpi::Signature::EdDSA { r, s }, _) => [pad(r, 32)?, pad(s, 32)?].concat(),
1404            (mpi::Signature::Ed25519 { s }, _) => s.to_vec(),
1405            (mpi::Signature::ECDSA { r, s }, mpi::PublicKey::ECDSA { curve, .. }) => {
1406                let size = curve.field_size().map_err(|e| CardError::Other(e.to_string()))?;
1407                [pad(r, size)?, pad(s, size)?].concat()
1408            }
1409            _ => return Err(CardError::Algorithm(algorithm_name(pair.public()))),
1410        };
1411        if self.info.pin_each_signature {
1412            self.verified_sign = false;
1413        }
1414        self.info.signatures += 1;
1415        self.operations += 1;
1416        Ok(raw)
1417    }
1418
1419    fn decipher(&mut self, input: ToDecipher<'_>) -> Result<Protected, CardError> {
1420        if !self.verified_decrypt {
1421            return Err(CardError::Other("security status not satisfied".into()));
1422        }
1423        if let Some(fault) = self.fault.take() {
1424            return Err(fault);
1425        }
1426        if self.info.decrypt.as_ref().is_some_and(|s| s.touch.asked()) {
1427            std::thread::sleep(self.touch_delay);
1428        }
1429        let pair = self.decryptor.as_mut().ok_or(CardError::NotOpenPgp)?;
1430        let opened = match (input, pair.public().mpis()) {
1431            (ToDecipher::Rsa(c), mpi::PublicKey::RSA { .. }) => {
1432                let session = crypto::Decryptor::decrypt(pair, &mpi::Ciphertext::RSA { c: mpi::MPI::new(c) }, None).map_err(|e| CardError::Other(e.to_string()))?;
1433                Protected::from(&session[..])
1434            }
1435            (ToDecipher::Ecdh(point), mpi::PublicKey::ECDH { curve: Curve::Cv25519, .. }) => {
1436                let x25519 = self.x25519.ok_or_else(|| CardError::Algorithm("Curve25519".into()))?;
1437                // Sequoia keeps the scalar reversed, as OpenPGP does; the card keeps X25519's own order.
1438                let scalar = pair.secret().map(|secret| match secret {
1439                    mpi::SecretKeyMaterial::ECDH { scalar } => {
1440                        let mut native = scalar.value_padded(32).to_vec();
1441                        native.reverse();
1442                        <[u8; 32]>::try_from(native.as_slice()).ok()
1443                    }
1444                    _ => None,
1445                });
1446                let scalar = scalar.ok_or_else(|| CardError::Other("no Curve25519 scalar".into()))?;
1447                let point = <[u8; 32]>::try_from(point).map_err(|_| CardError::Other("malformed point".into()))?;
1448                Protected::from(&x25519(&scalar, &point)[..])
1449            }
1450            _ => return Err(CardError::Algorithm(algorithm_name(pair.public()))),
1451        };
1452        self.operations += 1;
1453        Ok(opened)
1454    }
1455}
1456
1457/// Readers holding [`SoftCard`]s, for tests: none, some, held by another
1458/// program, or no smart card service at all.
1459#[derive(Default)]
1460pub struct SoftReaders {
1461    /// The keys plugged in.
1462    pub cards: Mutex<Vec<SoftCard>>,
1463    /// Keys another program holds: present, never opened.
1464    pub busy: usize,
1465    /// The smart card service answers with this instead.
1466    pub failure: Option<CardError>,
1467}
1468
1469impl SoftReaders {
1470    /// Readers holding `cards`.
1471    pub fn with(cards: Vec<SoftCard>) -> SoftReaders {
1472        SoftReaders { cards: Mutex::new(cards), busy: 0, failure: None }
1473    }
1474
1475    /// Runs `f` on the key `ident`, to see or change it between operations.
1476    pub fn card<R>(&self, ident: &str, f: impl FnOnce(&mut SoftCard) -> R) -> Option<R> {
1477        let mut cards = self.cards.lock().ok()?;
1478        cards.iter_mut().find(|c| c.info.ident == ident).map(f)
1479    }
1480}
1481
1482impl Readers for SoftReaders {
1483    fn present(&self) -> Result<Vec<String>, CardError> {
1484        if let Some(failure) = &self.failure {
1485            return Err(failure.clone());
1486        }
1487        Ok(self.cards.lock().map(|c| c.iter().map(|c| c.info.reader.clone()).collect()).unwrap_or_default())
1488    }
1489
1490    fn visit(&self, visit: &mut dyn FnMut(&mut dyn Card) -> Visit) -> Result<Visited, CardError> {
1491        if let Some(failure) = &self.failure {
1492            return Err(failure.clone());
1493        }
1494        let mut cards = self.cards.lock().map_err(|_| CardError::Other("poisoned".into()))?;
1495        let mut visited = Visited { opened: 0, busy: self.busy };
1496        for card in cards.iter_mut() {
1497            visited.opened += 1;
1498            let next = visit(card);
1499            card.close();
1500            if next == Visit::Done {
1501                break;
1502            }
1503        }
1504        Ok(visited)
1505    }
1506}
1507
1508#[cfg(test)]
1509pub(crate) mod x25519 {
1510    //! X25519 (RFC 7748), after TweetNaCl's `crypto_scalarmult` (public
1511    //! domain): for the software stand-in's Curve25519 key in tests only, since
1512    //! Sequoia keeps its own private. Not constant-time; never used by Sioul.
1513
1514    type Field = [i64; 16];
1515
1516    const A24: Field = [0xDB41, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1517
1518    fn carry(o: &mut Field) {
1519        for i in 0..16 {
1520            o[i] += 1 << 16;
1521            let c = o[i] >> 16;
1522            if i < 15 {
1523                o[i + 1] += c - 1;
1524            } else {
1525                o[0] += 38 * (c - 1);
1526            }
1527            o[i] -= c << 16;
1528        }
1529    }
1530
1531    fn swap(p: &mut Field, q: &mut Field, bit: i64) {
1532        let c = !(bit - 1);
1533        for i in 0..16 {
1534            let t = c & (p[i] ^ q[i]);
1535            p[i] ^= t;
1536            q[i] ^= t;
1537        }
1538    }
1539
1540    fn pack(n: &Field) -> [u8; 32] {
1541        let mut t = *n;
1542        carry(&mut t);
1543        carry(&mut t);
1544        carry(&mut t);
1545        for _ in 0..2 {
1546            let mut m = [0i64; 16];
1547            m[0] = t[0] - 0xffed;
1548            for i in 1..15 {
1549                m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
1550                m[i - 1] &= 0xffff;
1551            }
1552            m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
1553            let b = (m[15] >> 16) & 1;
1554            m[14] &= 0xffff;
1555            swap(&mut t, &mut m, 1 - b);
1556        }
1557        let mut o = [0u8; 32];
1558        for i in 0..16 {
1559            o[2 * i] = (t[i] & 0xff) as u8;
1560            o[2 * i + 1] = ((t[i] >> 8) & 0xff) as u8;
1561        }
1562        o
1563    }
1564
1565    fn unpack(n: &[u8; 32]) -> Field {
1566        let mut o = [0i64; 16];
1567        for i in 0..16 {
1568            o[i] = i64::from(n[2 * i]) + (i64::from(n[2 * i + 1]) << 8);
1569        }
1570        o[15] &= 0x7fff;
1571        o
1572    }
1573
1574    fn add(a: &Field, b: &Field) -> Field {
1575        std::array::from_fn(|i| a[i] + b[i])
1576    }
1577
1578    fn sub(a: &Field, b: &Field) -> Field {
1579        std::array::from_fn(|i| a[i] - b[i])
1580    }
1581
1582    fn mul(a: &Field, b: &Field) -> Field {
1583        let mut t = [0i64; 31];
1584        for i in 0..16 {
1585            for j in 0..16 {
1586                t[i + j] += a[i] * b[j];
1587            }
1588        }
1589        for i in 0..15 {
1590            t[i] += 38 * t[i + 16];
1591        }
1592        let mut o: Field = std::array::from_fn(|i| t[i]);
1593        carry(&mut o);
1594        carry(&mut o);
1595        o
1596    }
1597
1598    fn invert(i: &Field) -> Field {
1599        let mut c = *i;
1600        for a in (0..=253).rev() {
1601            c = mul(&c, &c);
1602            if a != 2 && a != 4 {
1603                c = mul(&c, i);
1604            }
1605        }
1606        c
1607    }
1608
1609    /// `scalar` times the point `u`.
1610    pub(crate) fn x25519(scalar: &[u8; 32], u: &[u8; 32]) -> [u8; 32] {
1611        let mut z = *scalar;
1612        z[31] = (z[31] & 127) | 64;
1613        z[0] &= 248;
1614        let x = unpack(u);
1615        let (mut a, mut b, mut c, mut d) = ([0i64; 16], x, [0i64; 16], [0i64; 16]);
1616        a[0] = 1;
1617        d[0] = 1;
1618        for i in (0..=254usize).rev() {
1619            let r = i64::from((z[i >> 3] >> (i & 7)) & 1);
1620            swap(&mut a, &mut b, r);
1621            swap(&mut c, &mut d, r);
1622            let mut e = add(&a, &c);
1623            a = sub(&a, &c);
1624            c = add(&b, &d);
1625            b = sub(&b, &d);
1626            d = mul(&e, &e);
1627            let f = mul(&a, &a);
1628            a = mul(&c, &a);
1629            c = mul(&b, &e);
1630            e = add(&a, &c);
1631            a = sub(&a, &c);
1632            b = mul(&a, &a);
1633            c = sub(&d, &f);
1634            a = mul(&c, &A24);
1635            a = add(&a, &d);
1636            c = mul(&c, &a);
1637            a = mul(&d, &f);
1638            d = mul(&b, &x);
1639            b = mul(&e, &e);
1640            swap(&mut a, &mut b, r);
1641            swap(&mut c, &mut d, r);
1642        }
1643        pack(&mul(&a, &invert(&c)))
1644    }
1645
1646    #[test]
1647    fn rfc_7748_vector() {
1648        let hex = |s: &str| -> [u8; 32] { std::array::from_fn(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap()) };
1649        let out = x25519(&hex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4"), &hex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c"));
1650        assert_eq!(out, hex("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552"));
1651    }
1652}
1653
1654#[cfg(test)]
1655mod tests {
1656    use super::*;
1657    use openpgp::cert::prelude::*;
1658    use openpgp::crypto::Signer as _;
1659    use openpgp::types::KeyFlags;
1660
1661    /// A key as the owner's card holds it: the primary signs, a Curve25519 subkey decrypts.
1662    fn primary_signs(address: &str) -> Cert {
1663        CertBuilder::new()
1664            .add_userid(format!("Card Holder <{address}>"))
1665            .set_cipher_suite(CipherSuite::Cv25519)
1666            .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
1667            .add_transport_encryption_subkey()
1668            .generate()
1669            .unwrap()
1670            .0
1671    }
1672
1673    fn touchless(_: &[u8; 32], _: &[u8; 32]) -> [u8; 32] {
1674        [0; 32]
1675    }
1676
1677    #[test]
1678    fn signs_and_checks_each_signature() {
1679        let cert = primary_signs("holder@example.org");
1680        let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
1681        let pin = Password::from("123456");
1682        let touched = std::sync::atomic::AtomicUsize::new(0);
1683        let touch = || {
1684            touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1685        };
1686        let access = CardAccess::new(&readers, &pin, &touch);
1687        let public = cert.primary_key().key().clone().role_into_unspecified();
1688        let mut signer = CardSigner::new(&access, "0006:12345678", public.clone());
1689        let digest = [7u8; 64];
1690        let signature = signer.sign(HashAlgorithm::SHA512, &digest).unwrap();
1691        public.verify(&signature, HashAlgorithm::SHA512, &digest).unwrap();
1692        assert_eq!(touched.load(std::sync::atomic::Ordering::Relaxed), 0, "no touch asked: none said");
1693        assert_eq!(access.reader().as_deref(), Some("Soft reader 0006:12345678"));
1694        // Touch on: said once, just before the key waits.
1695        readers.card("0006:12345678", |c| c.info.sign.as_mut().unwrap().touch = Touch::On);
1696        signer.sign(HashAlgorithm::SHA512, &digest).unwrap();
1697        assert_eq!(touched.load(std::sync::atomic::Ordering::Relaxed), 1);
1698        // Not touched in time: said as such, and nothing comes back.
1699        readers.card("0006:12345678", |c| c.fault = Some(CardError::TouchMissed));
1700        let fresh = CardAccess::new(&readers, &pin, &touch);
1701        assert!(CardSigner::new(&fresh, "0006:12345678", public.clone()).sign(HashAlgorithm::SHA512, &digest).is_err());
1702        assert_eq!(fresh.failure(), Some(CardError::TouchMissed));
1703        // The PIN was taken before the touch was missed: it may be held.
1704        assert!(fresh.pin_accepted());
1705        // Another key plugged in: the one wanted is absent.
1706        let other = SoftReaders::with(vec![SoftCard::new("0006:87654321", &cert, "123456")]);
1707        let elsewhere = CardAccess::new(&other, &pin, &touch);
1708        assert!(CardSigner::new(&elsewhere, "0006:12345678", public).sign(HashAlgorithm::SHA512, &digest).is_err());
1709        assert_eq!(elsewhere.failure(), Some(CardError::Absent));
1710    }
1711
1712    #[test]
1713    fn a_wrong_pin_costs_a_try_and_three_lock_the_key() {
1714        let cert = primary_signs("holder@example.org");
1715        let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
1716        let public = cert.primary_key().key().clone().role_into_unspecified();
1717        let wrong = Password::from("000000");
1718        let access = CardAccess::new(&readers, &wrong, &|| {});
1719        let mut signer = CardSigner::new(&access, "0006:12345678", public.clone());
1720        for left in [2, 1, 0] {
1721            assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
1722            assert_eq!(access.failure(), Some(CardError::WrongPin { left }));
1723        }
1724        assert!(!access.pin_accepted());
1725        assert!(signer.sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
1726        assert_eq!(access.failure(), Some(CardError::Blocked));
1727        // Locked: the right PIN does not open it either.
1728        let right = Password::from("123456");
1729        let access = CardAccess::new(&readers, &right, &|| {});
1730        assert!(CardSigner::new(&access, "0006:12345678", public).sign(HashAlgorithm::SHA512, &[1; 64]).is_err());
1731        assert_eq!(access.failure(), Some(CardError::Blocked));
1732        let tr = Translator::new("en");
1733        assert!(CardError::WrongPin { left: 2 }.sentence(&tr, Purpose::Sign).contains("2 tries left"));
1734        assert!(CardError::WrongPin { left: 1 }.sentence(&tr, Purpose::Sign).contains("one more mistake locks the key"));
1735        assert!(CardError::WrongPin { left: 0 }.sentence(&tr, Purpose::Sign).contains("Admin PIN"));
1736        assert_eq!(CardError::WrongPin { left: 1 }.remedy(), Remedy::Pin);
1737        assert_eq!(CardError::Blocked.remedy(), Remedy::None);
1738    }
1739
1740    #[test]
1741    fn the_pin_for_each_signature_is_given_each_time() {
1742        let cert = primary_signs("holder@example.org");
1743        let mut card = SoftCard::new("0006:12345678", &cert, "123456");
1744        card.info.pin_each_signature = true;
1745        let pin = Password::from("123456");
1746        card.verify(&pin, PinMode::Sign).unwrap();
1747        card.sign(ToSign::Digest(&[1; 64])).unwrap();
1748        // The card wants it again; Sioul gives it before each signature, one per opening.
1749        assert!(card.sign(ToSign::Digest(&[1; 64])).is_err());
1750        let readers = SoftReaders::with(vec![card]);
1751        let access = CardAccess::new(&readers, &pin, &|| {});
1752        let mut signer = CardSigner::new(&access, "0006:12345678", cert.primary_key().key().clone().role_into_unspecified());
1753        signer.sign(HashAlgorithm::SHA512, &[2; 64]).unwrap();
1754        signer.sign(HashAlgorithm::SHA512, &[3; 64]).unwrap();
1755    }
1756
1757    #[test]
1758    fn rsa_keys_sign_with_a_digest_info() {
1759        let (cert, _) = CertBuilder::new().add_userid("Card Holder <rsa@example.org>").set_cipher_suite(CipherSuite::RSA2k).set_primary_key_flags(KeyFlags::empty().set_certification().set_signing()).add_transport_encryption_subkey().generate().unwrap();
1760        let readers = SoftReaders::with(vec![SoftCard::new("000F:00001A2B", &cert, "123456")]);
1761        let pin = Password::from("123456");
1762        let access = CardAccess::new(&readers, &pin, &|| {});
1763        let public = cert.primary_key().key().clone().role_into_unspecified();
1764        let mut signer = CardSigner::new(&access, "000F:00001A2B", public.clone());
1765        assert!(!signer.acceptable_hashes().contains(&HashAlgorithm::SHA1));
1766        let digest = [9u8; 32];
1767        let signature = signer.sign(HashAlgorithm::SHA256, &digest).unwrap();
1768        public.verify(&signature, HashAlgorithm::SHA256, &digest).unwrap();
1769        // RSA opens a session key on the card, PKCS#1 taken off there.
1770        let subkey = cert.keys().subkeys().next().unwrap().key().clone().role_into_unspecified();
1771        let session = SessionKey::from(&[1u8; 32][..]);
1772        let sealed = subkey.encrypt(&session).unwrap();
1773        let mut decryptor = CardDecryptor::new(&access, "000F:00001A2B", subkey);
1774        let opened = crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).unwrap();
1775        assert_eq!(&opened[..], &session[..]);
1776    }
1777
1778    #[test]
1779    fn curve25519_opens_through_the_cards_shared_secret() {
1780        let cert = primary_signs("holder@example.org");
1781        let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456").with_x25519(x25519::x25519)]);
1782        let pin = Password::from("123456");
1783        let access = CardAccess::new(&readers, &pin, &|| {});
1784        let subkey = cert.keys().subkeys().next().unwrap().key().clone().role_into_unspecified();
1785        let session = SessionKey::from(&[5u8; 32][..]);
1786        let sealed = subkey.encrypt(&session).unwrap();
1787        let mut decryptor = CardDecryptor::new(&access, "0006:12345678", subkey.clone());
1788        let opened = crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).unwrap();
1789        assert_eq!(&opened[..], &session[..]);
1790        // A wrong shared secret does not unwrap: the glue's work is checked, not assumed.
1791        let wrong = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456").with_x25519(touchless)]);
1792        let access = CardAccess::new(&wrong, &pin, &|| {});
1793        let mut decryptor = CardDecryptor::new(&access, "0006:12345678", subkey);
1794        assert!(crypto::Decryptor::decrypt(&mut decryptor, &sealed, None).is_err());
1795    }
1796
1797    #[test]
1798    fn a_key_held_elsewhere_or_no_service_is_said() {
1799        let cert = primary_signs("holder@example.org");
1800        let mut readers = SoftReaders::with(vec![]);
1801        assert_eq!(cards(&readers), Err(CardError::Absent));
1802        readers.busy = 1;
1803        assert_eq!(cards(&readers), Err(CardError::Busy));
1804        assert_eq!(with_card(&readers, "0006:12345678", |_, _| Ok(())), Err(CardError::Busy));
1805        readers.failure = Some(CardError::NoService);
1806        assert_eq!(cards(&readers), Err(CardError::NoService));
1807        let readers = SoftReaders::with(vec![SoftCard::new("0006:12345678", &cert, "123456")]);
1808        let found = cards(&readers).unwrap();
1809        assert_eq!(found[0].ident, "0006:12345678");
1810        assert_eq!(found[0].sign.as_ref().unwrap().algorithm, "Ed25519");
1811        assert_eq!(found[0].decrypt.as_ref().unwrap().algorithm, "Curve25519");
1812        let tr = Translator::new("en");
1813        assert_eq!(found[0].label(&tr), "YubiKey 12\u{202F}345\u{202F}678");
1814        assert_eq!(serial_text(0x0A1B2C3D), "A1B2C3D");
1815        assert_eq!(CardError::Busy.remedy(), Remedy::Release);
1816        assert!(CardError::NoService.sentence(&tr, Purpose::Sign).contains("smart card service"));
1817    }
1818
1819    #[test]
1820    fn the_system_names_its_own_command() {
1821        assert_eq!(hint_for_os_release("NAME=\"Fedora Linux\"\nID=fedora\n"), "seckey-hint-fedora");
1822        assert_eq!(hint_for_os_release("ID=linuxmint\nID_LIKE=\"ubuntu debian\"\n"), "seckey-hint-debian");
1823        assert_eq!(hint_for_os_release("ID=endeavouros\nID_LIKE=arch\n"), "seckey-hint-arch");
1824        assert_eq!(hint_for_os_release("ID=\"opensuse-tumbleweed\"\nID_LIKE=\"opensuse suse\"\n"), "seckey-hint-suse");
1825        assert_eq!(hint_for_os_release("ID=gentoo\n"), "seckey-hint-linux");
1826    }
1827
1828    #[test]
1829    fn a_certificate_is_checked_against_the_key() {
1830        let cert = primary_signs("holder@example.org");
1831        let card = SoftCard::new("0006:12345678", &cert, "123456");
1832        let found = check(&cert.clone().strip_secret_key_material(), &card.info).unwrap();
1833        assert_eq!(found.addresses, vec!["holder@example.org".to_string()]);
1834        assert!(found.signs && found.decrypts && !found.expired);
1835        assert_eq!(found.cert, cert.fingerprint().to_hex());
1836        // Another certificate: its keys are not those of the card.
1837        let stranger = primary_signs("holder@example.org");
1838        assert_eq!(check(&stranger, &card.info), Err(Mismatch::Missing));
1839        // The card's fingerprint written over another key's: the public parts tell.
1840        let mut forged = card.info.clone();
1841        forged.sign.as_mut().unwrap().public = card_public(stranger.primary_key().key().role_as_unspecified());
1842        assert_eq!(check(&cert, &forged), Err(Mismatch::OtherPublicKey));
1843        // A key in the signing slot that its certificate does not let sign.
1844        let mut swapped = card.info.clone();
1845        swapped.sign = card.info.decrypt.clone();
1846        assert_eq!(check(&cert, &swapped), Err(Mismatch::NotForSigning));
1847        // Revoked.
1848        let (revoked, revocation) = CertBuilder::new().add_userid("<gone@example.org>").set_cipher_suite(CipherSuite::Cv25519).set_primary_key_flags(KeyFlags::empty().set_certification().set_signing()).add_transport_encryption_subkey().generate().unwrap();
1849        let card_of_revoked = SoftCard::new("0006:00000001", &revoked, "123456");
1850        let revoked = revoked.insert_packets(revocation).unwrap().0;
1851        assert_eq!(check(&revoked, &card_of_revoked.info), Err(Mismatch::Revoked));
1852        // No user ID: still the key, naming no address.
1853        let bare = Cert::from_packets(cert.clone().strip_secret_key_material().into_packets().filter(|p| !matches!(p, openpgp::Packet::UserID(_)) && !matches!(p, openpgp::Packet::Signature(s) if s.typ() == openpgp::types::SignatureType::PositiveCertification))).unwrap();
1854        match check(&bare, &card.info) {
1855            Ok(found) => assert!(found.addresses.is_empty()),
1856            // Sequoia may want a binding to call the primary key valid: then said as such.
1857            Err(Mismatch::Invalid(_)) => {}
1858            Err(other) => panic!("{other:?}"),
1859        }
1860    }
1861
1862    /// Each part of a certificate and its expiry said exactly: all expired; the
1863    /// primary key renewed alone, its encryption subkey expired (Renew offered
1864    /// for it); none expired.
1865    #[test]
1866    fn each_part_of_the_key_and_its_expiry_said_exactly() {
1867        let tr = Translator::new("en");
1868        let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
1869        let made = SystemTime::now() - Duration::from_secs(3 * 365 * 24 * 3600);
1870        let (old, _) = CertBuilder::new()
1871            .add_userid("Card Holder <late@example.org>")
1872            .set_cipher_suite(CipherSuite::Cv25519)
1873            .set_creation_time(made)
1874            .set_validity_period(Duration::from_secs(365 * 24 * 3600))
1875            .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
1876            .add_transport_encryption_subkey()
1877            .add_authentication_subkey()
1878            .generate()
1879            .unwrap();
1880        let roles: Vec<Role> = parts(&old).iter().map(|p| p.role).collect();
1881        assert_eq!(roles, [Role::Primary, Role::Encryption, Role::Authentication]);
1882        // All expired: each said, the primary key first; no word of renewing the subkeys alone.
1883        let (line, warm, expired) = expiry_words(&tr, &old, None, now);
1884        assert!(warm && expired);
1885        let primary = short_fingerprint(&old.fingerprint().to_hex());
1886        assert!(line.starts_with(&format!("Its primary key ({primary}) expired on ")), "{line}");
1887        assert!(line.contains("Its encryption subkey (") && line.contains("nobody can encrypt to you with it.") && line.contains("Its authentication subkey ("), "{line}");
1888        assert!(!line.contains("Renew for two years"), "{line}");
1889        // The primary key renewed alone, as in a terminal: the subkeys said, and Renew offered for them.
1890        let mut signer = old.primary_key().key().clone().parts_into_secret().unwrap().into_keypair().unwrap();
1891        let policy = StandardPolicy::new();
1892        let renewed = old.primary_key().with_policy(&policy, None).unwrap().set_expiration_time(&mut signer, Some(SystemTime::now() + Duration::from_secs(2 * 365 * 24 * 3600))).unwrap();
1893        let half = old.clone().insert_packets(renewed).unwrap().0;
1894        let (line, warm, expired) = expiry_words(&tr, &half, None, now);
1895        assert!(warm && expired);
1896        let encryption = short_fingerprint(&parts(&half)[1].fingerprint);
1897        assert!(line.starts_with(&format!("Its encryption subkey ({encryption}) expired on ")) && !line.contains("Its primary key"), "{line}");
1898        assert!(line.ends_with("Renew for two years renews them with its primary key. By hand, name each subkey: GnuPG's \"*\" leaves those already expired as they are."), "{line}");
1899        assert_eq!(subkeys_of(&half).len(), 2);
1900        // Nothing expired: until when, as before.
1901        let (fresh, _) = CertBuilder::general_purpose(Some("Card Holder <now@example.org>")).set_validity_period(Duration::from_secs(365 * 24 * 3600)).generate().unwrap();
1902        let (line, warm, expired) = expiry_words(&tr, &fresh, Some(now + 300 * 86_400), now);
1903        assert!(!warm && !expired && line.starts_with("Valid until "), "{line}");
1904        assert!(parts_until(&tr, &parts(&fresh)).starts_with("primary key until "));
1905        assert_eq!(short_fingerprint("b81a 2c3d 4e5f 6a7b 8c9d 0e1f 2a3b 4c5d 6e7f bf8c"), "B81A…BF8C");
1906    }
1907
1908    #[test]
1909    fn an_expired_key_is_still_its_key_and_says_so() {
1910        let made = SystemTime::now() - Duration::from_secs(3 * 365 * 24 * 3600);
1911        let (cert, _) = CertBuilder::new()
1912            .add_userid("Card Holder <late@example.org>")
1913            .set_cipher_suite(CipherSuite::Cv25519)
1914            .set_creation_time(made)
1915            .set_validity_period(Duration::from_secs(365 * 24 * 3600))
1916            .set_primary_key_flags(KeyFlags::empty().set_certification().set_signing())
1917            .add_transport_encryption_subkey()
1918            .generate()
1919            .unwrap();
1920        let card = SoftCard::new("0006:12345678", &cert, "123456");
1921        let found = check(&cert, &card.info).unwrap();
1922        assert!(found.expired);
1923        let tr = Translator::new("en");
1924        let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
1925        let (line, warm) = expiry_line(&tr, found.expires, &found.cert, now).unwrap();
1926        // Where it is renewed, never a command in the sentence: those are under "By hand", to copy.
1927        assert!(warm && line.contains("expired on") && line.contains("Accounts ▸ Encryption") && !line.contains("gpg "), "{line}");
1928        let (soon, warm) = expiry_line(&tr, Some(now + 10 * 24 * 3600), "F", now).unwrap();
1929        assert!(warm && soon.contains("expires on"), "{soon}");
1930        let (later, warm) = expiry_line(&tr, Some(now + 300 * 24 * 3600), "F", now).unwrap();
1931        assert!(!warm && later.contains("until"), "{later}");
1932        assert_eq!(expiry_line(&tr, None, "F", now), None);
1933    }
1934
1935    #[test]
1936    fn the_pin_is_held_fifteen_quiet_minutes() {
1937        let start = Instant::now();
1938        let mut memory = PinMemory::new();
1939        memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
1940        assert!(memory.get("0006:12345678", start + Duration::from_secs(60)).is_some());
1941        assert!(memory.get("0006:87654321", start).is_none());
1942        // Each use starts the quiet time again.
1943        memory.used("0006:12345678", start + Duration::from_secs(14 * 60));
1944        assert!(memory.get("0006:12345678", start + Duration::from_secs(28 * 60)).is_some());
1945        assert!(memory.get("0006:12345678", start + Duration::from_secs(29 * 60 + 1)).is_none());
1946        assert_eq!(memory.holder(), None);
1947        // The key pulled out: forgotten.
1948        memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
1949        assert!(!memory.went_away(&["Yubico YubiKey".to_string()]));
1950        assert!(memory.went_away(&[]));
1951        assert_eq!(memory.holder(), None);
1952        memory.keep("0006:12345678", Password::from("123456"), "Yubico YubiKey", start);
1953        memory.forget();
1954        assert!(memory.get("0006:12345678", start).is_none());
1955    }
1956
1957    #[test]
1958    fn known_keys_are_kept_without_secrets() {
1959        let dir = std::env::temp_dir().join(format!("sioul-cards-{}", std::process::id()));
1960        let path = dir.join("cards.toml");
1961        let _ = std::fs::remove_file(&path);
1962        change_known_at(&path, |cards| {
1963            cards.push(KnownCard { ident: "0006:12345678".into(), manufacturer: 6, serial: 0x12345678, cert: "ABCD".into(), addresses: vec!["Holder@Example.org".into()], touch_sign: Touch::On, ..KnownCard::default() });
1964        })
1965        .unwrap();
1966        let read = read_known(&path);
1967        assert_eq!(read.len(), 1);
1968        assert!(read[0].serves("holder@example.org") && !read[0].serves("other@example.org"));
1969        assert_eq!(read[0].touch_sign, Touch::On);
1970        let text = std::fs::read_to_string(&path).unwrap();
1971        assert!(text.contains("[[card]]") && text.contains("touch_sign = \"on\""), "{text}");
1972        let tr = Translator::new("en");
1973        let mut known = read[0].clone();
1974        assert_eq!(known.tries_line(3, &tr), None);
1975        assert!(known.tries_line(2, &tr).unwrap().contains("2 tries left"));
1976        known.pin_tries_full = 5;
1977        assert!(known.tries_line(4, &tr).is_some());
1978        std::fs::remove_dir_all(&dir).unwrap();
1979    }
1980
1981    #[test]
1982    fn every_failure_has_words_in_each_language() {
1983        for language in ["en", "fr"] {
1984            let tr = Translator::new(language);
1985            let failures = [
1986                CardError::Unsupported,
1987                CardError::NoService,
1988                CardError::Absent,
1989                CardError::Busy,
1990                CardError::WrongPin { left: 2 },
1991                CardError::WrongPin { left: 1 },
1992                CardError::Blocked,
1993                CardError::TouchMissed,
1994                CardError::Removed,
1995                CardError::NotOpenPgp,
1996                CardError::OtherKeys,
1997                CardError::BadSignature,
1998                CardError::Algorithm("X448".into()),
1999                CardError::Other("no reader".into()),
2000            ];
2001            for failure in failures {
2002                for purpose in [Purpose::Sign, Purpose::Open, Purpose::Read] {
2003                    let said = failure.sentence(&tr, purpose);
2004                    assert!(!said.is_empty() && !said.contains("seckey-") && !said.contains('!'), "{language} {failure:?}: {said}");
2005                }
2006            }
2007            for mismatch in [Mismatch::Empty, Mismatch::Missing, Mismatch::Revoked, Mismatch::NotForSigning, Mismatch::OtherPublicKey, Mismatch::Invalid("SHA-1".into())] {
2008                assert!(!mismatch.sentence(&tr).contains("seckey-"), "{language} {mismatch:?}");
2009            }
2010            for hint in ["seckey-hint-fedora", "seckey-hint-debian", "seckey-hint-arch", "seckey-hint-suse", "seckey-hint-linux", "seckey-hint-flatpak", "seckey-hint-windows", "seckey-hint-macos"] {
2011                assert_ne!(tr.text(hint, None), hint, "{language} {hint}");
2012            }
2013        }
2014    }
2015}