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