Skip to main content

sioul_core/
pgp.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! OpenPGP (RFC 9580) for mail, with Sequoia and its pure-Rust cryptography.
5//!
6//! - **Your keys** live in `~/.local/share/sioul/pgp/own/`. A key made here
7//!   keeps its revocation certificate beside it, `<fingerprint>.rev`, yours
8//!   alone as the key is ([`revocation_path`]); revoking is not built. A key's private
9//!   part is in one of two places ([`Place`]): here, encrypted with a
10//!   passphrase the system keyring keeps (`sioul-sync` asks it), so nothing is
11//!   asked at each message; or on a security key, which signs and opens itself
12//!   after its PIN ([`crate::securitykey`]), Sioul keeping only its public
13//!   certificate. Keys come from an import (a file, GnuPG's export), are made
14//!   here, or are a security key's.
15//! - **Others' keys** live in `…/pgp/others/`: imported, found in their
16//!   messages (Autocrypt), or looked up on request.
17//! - **Signing and opening** take Sequoia's own `Signer` and `Decryptor`
18//!   traits: a key here or a security key, the same way. A message a security
19//!   key opened keeps its session key in memory ([`SessionKeys`]), so that it
20//!   opens again without the key until Sioul closes.
21//! - **What this module does not do**: talk to the network, or to the
22//!   keyring. Passphrases and PINs come in as arguments.
23
24use crate::config::data_dir;
25use crate::securitykey::{self, CardAccess, CardDecryptor, KnownCard};
26use openpgp::cert::prelude::*;
27use openpgp::crypto::{self, Password, SessionKey, mpi};
28use openpgp::packet::{PKESK, SKESK, key};
29use openpgp::parse::Parse;
30use openpgp::parse::stream::{DecryptionHelper, DecryptorBuilder, DetachedVerifierBuilder, MessageLayer, MessageStructure, VerificationHelper};
31use openpgp::policy::StandardPolicy;
32use openpgp::serialize::stream::{Armorer, Encryptor, LiteralWriter, Message, Signer};
33use openpgp::serialize::{Serialize, SerializeInto};
34use openpgp::types::{HashAlgorithm, SymmetricAlgorithm};
35use openpgp::{Cert, KeyHandle};
36/// A key's fingerprint, as the crates that do not use Sequoia themselves name it.
37pub use openpgp::Fingerprint;
38use sequoia_openpgp as openpgp;
39use serde::Serialize as Serde;
40use std::cell::RefCell;
41use std::collections::BTreeMap;
42use std::io::Write;
43use std::path::PathBuf;
44use std::sync::Mutex;
45
46fn policy() -> StandardPolicy<'static> {
47    StandardPolicy::new()
48}
49
50fn pgp_dir() -> PathBuf {
51    data_dir().join("pgp")
52}
53
54fn own_dir() -> PathBuf {
55    pgp_dir().join("own")
56}
57
58/// The keys Sioul knows: yours (their secret parts here, or on a security
59/// key), others', and your security keys.
60#[derive(Default, Clone)]
61pub struct Keys {
62    pub own: Vec<Cert>,
63    pub others: Vec<Cert>,
64    /// Your security keys read on this device, as `pgp/cards.toml` lists
65    /// them: they alone sign here (review of 19 October 2026, F18).
66    pub cards: Vec<KnownCard>,
67    /// Your security keys your other devices made known and this one did not
68    /// read (`pgp/cards/`), those stopped here aside (`pgp/stopped.toml`):
69    /// they serve encryption to you, Autocrypt and the reader's words, never
70    /// signing here.
71    pub shared: Vec<KnownCard>,
72    /// Your addresses: your accounts' (`config.toml`). With the addresses
73    /// your security keys serve, they are yours whatever certificate claims
74    /// them; never taken from a certificate's user IDs (review of 19 October
75    /// 2026, F1).
76    pub addresses: Vec<String>,
77}
78
79/// Where the private part of one of your keys is.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum Place {
82    /// Here: its secret part in `pgp/own`, encrypted with a passphrase the system keyring keeps.
83    Here,
84    /// On a security key, by its identifier ("0006:12345678").
85    Card {
86        /// The security key, as [`securitykey::CardInfo::ident`].
87        ident: String,
88    },
89}
90
91/// Your key for an address: its certificate, and where its private part is.
92#[derive(Debug, Clone)]
93pub struct Own<'a> {
94    pub cert: &'a Cert,
95    pub place: Place,
96}
97
98fn read_certs(dir: &std::path::Path) -> Vec<Cert> {
99    let Ok(entries) = std::fs::read_dir(dir) else { return Vec::new() };
100    entries
101        .filter_map(Result::ok)
102        .map(|e| e.path())
103        .filter(|p| p.extension().is_some_and(|x| x == "asc" || x == "pgp"))
104        .filter_map(|p| std::fs::read(&p).ok())
105        .flat_map(|bytes| CertParser::from_bytes(&bytes).map(|parser| parser.filter_map(Result::ok).collect::<Vec<_>>()).unwrap_or_default())
106        .collect()
107}
108
109/// Whether a key holds `address` among its user IDs bound to it, as
110/// Sequoia's policy sees them: a user ID with no binding signature of the
111/// key's own, revoked, or bound under an algorithm the policy refuses, names
112/// nothing (review of 19 October 2026, F1: `userids()` alone counts every
113/// user ID, "even those without a binding signature").
114fn bound(cert: &Cert, address: &str) -> bool {
115    let policy = policy();
116    let Ok(valid) = cert.with_policy(&policy, None) else { return false };
117    valid.userids().revoked(false).any(|u| email_is(u.userid(), address))
118}
119
120/// Whether a certificate may serve at all: valid under Sequoia's policy and
121/// not revoked as a whole. Sequoia's `revoked(false)` on a key asks that
122/// key's own revocation, never the certificate's: a certificate revoked as
123/// compromised still has live, unrevoked subkeys (review of 19 October 2026,
124/// N3). A revoked certificate never takes a message or a seal and never
125/// signs; it still opens what was sent to it before.
126fn usable(cert: &Cert) -> bool {
127    let policy = policy();
128    cert.with_policy(&policy, None).is_ok_and(|valid| !matches!(valid.revocation_status(), openpgp::types::RevocationStatus::Revoked(_)))
129}
130
131/// Whether a key serves `address`: bound to it (`bound`) and not revoked as
132/// a whole (`usable`). Every choice of a key for an address goes through it
133/// (`for_address`, `self_cert_for`, `own_for`).
134fn names(cert: &Cert, address: &str) -> bool {
135    usable(cert) && bound(cert, address)
136}
137
138/// For the window's words: whether `cert` holds `address` among the user
139/// IDs bound to it, revoked as a whole or not.
140pub fn names_address(cert: &Cert, address: &str) -> bool {
141    bound(cert, address)
142}
143
144/// Whether `cert` is valid but revoked as a whole, for the window's words.
145pub fn revoked_whole(cert: &Cert) -> bool {
146    !usable(cert) && cert.with_policy(&policy(), None).is_ok()
147}
148
149fn email_is(userid: &openpgp::packet::UserID, address: &str) -> bool {
150    userid.email().ok().flatten().is_some_and(|e| e.eq_ignore_ascii_case(address.trim()))
151}
152
153/// `cert` with only its user IDs for `address`, when one of them is bound
154/// to it (`names`); none otherwise. What a certificate brings from
155/// elsewhere (an Autocrypt header, a lookup) is kept for the address it
156/// came for alone: its other user IDs, bound or not, never make it
157/// another address's key (review of 19 October 2026, F2).
158fn for_its_address(cert: Cert, address: &str) -> Option<Cert> {
159    let kept = cert.retain_userids(|u| email_is(u.userid(), address));
160    // Bound, revoked as a whole or not: a revocation that comes is kept, and stops the key kept.
161    bound(&kept, address).then_some(kept)
162}
163
164/// The addresses bound to `cert` (`bound`), as written there, each once.
165fn bound_addresses(cert: &Cert) -> Vec<String> {
166    let policy = policy();
167    let Ok(valid) = cert.with_policy(&policy, None) else { return Vec::new() };
168    let mut out: Vec<String> = Vec::new();
169    for address in valid.userids().revoked(false).filter_map(|u| u.userid().email().ok().flatten().map(|e| e.trim().to_string())) {
170        if !out.iter().any(|kept| kept.eq_ignore_ascii_case(&address)) {
171            out.push(address);
172        }
173    }
174    out
175}
176
177/// What travels beside each of others' keys (`others/<fingerprint>.toml`):
178/// the addresses it was learned for (an Autocrypt header's, a lookup's) or,
179/// imported from a file you chose, every address bound to it. The only ones
180/// it serves (review of 19 October 2026, N2).
181#[derive(serde::Serialize, serde::Deserialize)]
182struct Served {
183    format: u32,
184    #[serde(default)]
185    addresses: Vec<String>,
186    /// Of those, the ones judged from a key kept without this file (the
187    /// migration, `judged`) rather than learned: a later rule may look at
188    /// them again. An address learned since leaves this list.
189    #[serde(default, skip_serializing_if = "Vec::is_empty")]
190    judged: Vec<String>,
191}
192
193/// The format of `others/`: 2 since its keys say what they serve; 1 (none
194/// said), what Sioul kept before, or what an older Sioul writes through the
195/// sharing.
196const OTHERS_FORMAT: u32 = 2;
197
198fn served_path(dir: &std::path::Path, fingerprint: &str) -> PathBuf {
199    dir.join(format!("{fingerprint}.toml"))
200}
201
202/// What the file beside someone's key `cert`, kept in `dir`, says, when it
203/// reads.
204fn record_of(dir: &std::path::Path, cert: &Cert) -> Option<Served> {
205    std::fs::read_to_string(served_path(dir, &cert.fingerprint().to_hex())).ok().and_then(|text| toml::from_str::<Served>(&text).ok())
206}
207
208/// What a key kept without that file serves (before 19 October 2026, or
209/// written through the sharing by an older Sioul), judged as it is: the
210/// address of every user ID it carries, bound or not, revoked or not, since
211/// a Sioul before 1f9acc0 took a key on any of them (a header's key had to
212/// name its sender in some user ID, bound by nothing if it liked). Its one
213/// address, only when every user ID names that same address and it is
214/// bound; else none (review of 19 October 2026, N2 and R1: Mallory's key
215/// binding Jane's address, his own carried by nothing, came from his header
216/// and named one bound address, hers). A key it serves none waits, kept,
217/// until it comes again for an address.
218fn judged(cert: &Cert) -> Vec<String> {
219    let mut named: Vec<String> = Vec::new();
220    for address in cert.userids().filter_map(|u| u.userid().email().ok().flatten().map(|e| e.trim().to_string())) {
221        if !named.iter().any(|kept| kept.eq_ignore_ascii_case(&address)) {
222            named.push(address);
223        }
224    }
225    match named.as_slice() {
226        [one] if bound(cert, one) => named,
227        _ => Vec::new(),
228    }
229}
230
231/// The addresses someone's key `cert`, kept in `dir`, serves: those its file
232/// beside says, as they are bound to it; without that file, as it is
233/// judged (`judged`), at each reading.
234fn served_by(dir: &std::path::Path, cert: &Cert) -> Vec<String> {
235    record_of(dir, cert).map_or_else(|| judged(cert), |served| served.addresses)
236}
237
238/// `cert` cut to the user IDs of `addresses`.
239fn only_for(cert: Cert, addresses: &[String]) -> Cert {
240    cert.retain_userids(|u| u.userid().email().ok().flatten().is_some_and(|e| addresses.iter().any(|a| a.trim().eq_ignore_ascii_case(e.trim()))))
241}
242
243/// Others' keys in `dir`, each cut to the addresses it serves (`served_by`);
244/// one that serves none is left out of every use, its signatures said as a
245/// key's Sioul does not know (kept on the disk, it comes back once learned
246/// again for an address).
247fn read_others(dir: &std::path::Path) -> Vec<Cert> {
248    read_certs(dir)
249        .into_iter()
250        .filter_map(|cert| {
251            let served = served_by(dir, &cert);
252            let cut = only_for(cert, &served);
253            cut.userids().next().is_some().then_some(cut)
254        })
255        .collect()
256}
257
258/// Writes what someone's key serves beside it.
259fn write_served(dir: &std::path::Path, fingerprint: &str, served: &Served) -> Result<(), String> {
260    let text = format!(
261        "# Sioul: the addresses this key was learned for, the only ones it serves (docs/client.md, \"What others' keys serve\").\n{}",
262        toml::to_string(served).map_err(|e| e.to_string())?
263    );
264    write_key(&served_path(dir, fingerprint), text.as_bytes())
265}
266
267/// Once on each device (`state/pgp-others.toml`): each key of `others/`
268/// kept without what it serves (format 1) gets it, as it is judged now
269/// (`judged`): its one address when every user ID it carries names it and
270/// it is bound, else none; written as judged, not learned. Kept so, an older
271/// Sioul's later write to it (every user ID of a header, merged) adds no
272/// address it serves. The key's own file is never touched: one that serves
273/// none stays, to serve again once it comes for an address (its owner's
274/// next message, a lookup). How many keys it wrote it for.
275pub fn keep_others_for_their_addresses() -> Result<usize, String> {
276    keep_others_for_their_addresses_at(&pgp_dir(), &crate::config::state_dir().join("pgp-others.toml"))
277}
278
279/// `keep_others_for_their_addresses` in the folder `root` (`pgp/`), its mark at `mark`.
280fn keep_others_for_their_addresses_at(root: &std::path::Path, mark: &std::path::Path) -> Result<usize, String> {
281    #[derive(Default, serde::Serialize, serde::Deserialize)]
282    struct Mark {
283        #[serde(default)]
284        format: u32,
285    }
286    let done = std::fs::read_to_string(mark).ok().and_then(|text| toml::from_str::<Mark>(&text).ok()).unwrap_or_default();
287    if done.format >= OTHERS_FORMAT {
288        return Ok(0);
289    }
290    let dir = root.join("others");
291    let mut written = 0;
292    for cert in read_certs(&dir) {
293        let fingerprint = cert.fingerprint().to_hex();
294        if served_path(&dir, &fingerprint).exists() {
295            continue;
296        }
297        let addresses = judged(&cert);
298        write_served(&dir, &fingerprint, &Served { format: OTHERS_FORMAT, judged: addresses.clone(), addresses })?;
299        written += 1;
300    }
301    if let Some(parent) = mark.parent() {
302        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
303    }
304    let text = format!(
305        "# Sioul: others' keys say beside them what they serve (pgp/others/<fingerprint>.toml) since this format; never shared.\n{}",
306        toml::to_string(&Mark { format: OTHERS_FORMAT }).map_err(|e| e.to_string())?
307    );
308    crate::filelock::replace(mark, text.as_bytes()).map_err(|e| format!("{}: {e}", mark.display()))?;
309    Ok(written)
310}
311
312impl Keys {
313    pub fn load() -> Keys {
314        let addresses = crate::config::Config::load(&crate::config::default_path()).map(|c| c.accounts.iter().filter_map(|a| a.address.clone()).collect()).unwrap_or_default();
315        Keys { addresses, ..Keys::load_at(&pgp_dir()) }
316    }
317
318    /// The keys of the folder `root` (`pgp/`): yours, others', your security
319    /// keys (`cards.toml`), and those your other devices made known
320    /// (`cards/`, [`securitykey::shared_cards`]): a security key read on your
321    /// computer, its certificate with it, so that a phone encrypts to you,
322    /// hands your key by Autocrypt and says what waits for the key. A key
323    /// this device read itself says what it is; its certificate is merged
324    /// with the shared one (a renewal made on another computer).
325    pub fn load_at(root: &std::path::Path) -> Keys {
326        let mut keys = Keys { own: read_certs(&root.join("own")), others: read_others(&root.join("others")), cards: securitykey::known_cards_in(&root.join("cards.toml")), ..Keys::default() };
327        let stopped = securitykey::stopped_in(root);
328        for (card, cert) in securitykey::shared_cards_in(&root.join(securitykey::SHARED_CARDS)) {
329            let read_here = keys.cards.iter().any(|known| known.ident.eq_ignore_ascii_case(&card.ident));
330            // Stopped on this device: never taken back from what the others share (F4).
331            if !read_here && stopped.iter().any(|ident| ident.eq_ignore_ascii_case(&card.ident)) {
332                continue;
333            }
334            if !read_here {
335                keys.shared.push(card);
336            }
337            match keys.own.iter_mut().find(|own| own.fingerprint() == cert.fingerprint()) {
338                Some(own) => {
339                    if let Ok(merged) = own.clone().merge_public(cert) {
340                        *own = merged;
341                    }
342                }
343                None => keys.own.push(cert),
344            }
345        }
346        keys
347    }
348
349    /// Your key for an address, able to sign now. A security key read on
350    /// this device that serves the address comes first, and alone: a key kept
351    /// here for the same address only opens older mail. When that security
352    /// key's certificate expired, nothing signs for the address until it is
353    /// renewed. A security key another computer read signs nowhere but there
354    /// (review of 19 October 2026, F18): to sign here, it is read here.
355    pub fn own_for(&self, address: &str) -> Option<Own<'_>> {
356        if let Some((card, cert)) = self.card_for(address) {
357            return (can_sign_with(cert, &card.sign) && self.usable(cert)).then(|| Own { cert, place: Place::Card { ident: card.ident.clone() } });
358        }
359        let policy = policy();
360        self.own
361            .iter()
362            .find(|c| self.serves(c, address) && c.keys().secret().with_policy(&policy, None).alive().revoked(false).for_signing().next().is_some())
363            .map(|cert| Own { cert, place: Place::Here })
364    }
365
366    /// The security key read here that signs for `address`, and its
367    /// certificate, even when it expired: to say so.
368    pub fn card_for(&self, address: &str) -> Option<(&KnownCard, &Cert)> {
369        self.cards.iter().filter(|card| card.serves(address)).find_map(|card| Some((card, self.cert(&card.cert)?)))
370    }
371
372    /// The security key of yours that serves `address`, read here or on
373    /// another device of yours, and its certificate: encryption to you,
374    /// Autocrypt, and what the reader says wait for it.
375    pub fn serving_card(&self, address: &str) -> Option<(&KnownCard, &Cert)> {
376        self.all_cards().filter(|card| card.serves(address)).find_map(|card| Some((card, self.cert(&card.cert)?)))
377    }
378
379    /// Every security key of yours known here: read here first, then those
380    /// your other devices made known.
381    pub fn all_cards(&self) -> impl Iterator<Item = &KnownCard> {
382        self.cards.iter().chain(&self.shared)
383    }
384
385    /// Your certificate of this fingerprint.
386    pub fn cert(&self, fingerprint: &str) -> Option<&Cert> {
387        self.own.iter().find(|c| c.fingerprint().to_hex().eq_ignore_ascii_case(fingerprint))
388    }
389
390    /// A key to encrypt to `address`: someone's, or yours. For one of your
391    /// own addresses, your own key alone: never a certificate someone sent
392    /// claiming it (an Autocrypt header, a lookup), which would read what you
393    /// send yourself; none when yours cannot take encryption now.
394    ///
395    /// Someone's keys are kept for the address each came for alone
396    /// (`for_its_address`). Two of them for one address (a key made anew,
397    /// the old one kept): the one whose newest live encryption key is the
398    /// most recent, then the one whose primary key is, then by fingerprint;
399    /// never the order the folder lists them in (review of 19 October 2026,
400    /// F2). Others' before a key of yours that names the address.
401    pub fn for_address(&self, address: &str) -> Option<&Cert> {
402        if self.is_own_address(address) {
403            return self.self_cert_for(address);
404        }
405        let policy = policy();
406        let newest = |c: &Cert| {
407            let encryption = c.keys().with_policy(&policy, None).alive().revoked(false).for_transport_encryption().map(|k| k.key().creation_time()).max();
408            (encryption, c.primary_key().key().creation_time(), c.fingerprint().to_hex())
409        };
410        let best = |certs: &'_ [Cert]| -> Option<usize> {
411            certs.iter().enumerate().filter(|(_, c)| self.serves(c, address)).filter_map(|(i, c)| { let n = newest(c); n.0.map(|_| (n, i)) }).max_by(|a, b| a.0.cmp(&b.0)).map(|(_, i)| i)
412        };
413        best(&self.others).map(|i| &self.others[i]).or_else(|| best(&self.own).map(|i| &self.own[i]))
414    }
415
416    /// Your own certificate to encrypt to, for `address`: your security key's
417    /// when one serves the address (its certificate as your computer read
418    /// it), else a key of yours naming it; none when it cannot take
419    /// encryption now (expired, revoked).
420    pub fn self_cert_for(&self, address: &str) -> Option<&Cert> {
421        let policy = policy();
422        let encrypts = |c: &&Cert| self.usable(c) && c.keys().with_policy(&policy, None).alive().revoked(false).for_transport_encryption().next().is_some();
423        if let Some((_, cert)) = self.serving_card(address) {
424            return Some(cert).filter(encrypts);
425        }
426        self.own.iter().filter(|c| self.serves(c, address)).find(encrypts)
427    }
428
429    /// Your own certificates that can take what is sealed for you alone
430    /// (your security keys', as your computer read them, and your keys
431    /// here): an encryption key not revoked, expired or not, since you open
432    /// such a seal with it (F19); never a certificate revoked as a whole
433    /// (N3). Encrypted to each, any of them opens it.
434    pub fn own_certs(&self) -> Vec<&Cert> {
435        let policy = policy();
436        let mut out: Vec<&Cert> = Vec::new();
437        for cert in &self.own {
438            if self.usable(cert) && cert.keys().with_policy(&policy, None).supported().revoked(false).for_transport_encryption().next().is_some() && !out.iter().any(|c| c.fingerprint() == cert.fingerprint()) {
439                out.push(cert);
440            }
441        }
442        out
443    }
444
445    /// Whether `address` is yours: one of your accounts' addresses, or one a
446    /// security key of yours serves. Never from a certificate's user IDs,
447    /// which anyone can write (review of 19 October 2026, F1).
448    pub fn is_own_address(&self, address: &str) -> bool {
449        let address = address.trim();
450        self.addresses.iter().any(|a| a.trim().eq_ignore_ascii_case(address)) || self.all_cards().any(|card| card.serves(address))
451    }
452
453    /// Whether `cert` may serve: valid, not revoked as a whole (`usable`),
454    /// and not revoked by its designated revoker (a Revocation Key, GnuPG's
455    /// `addrevoker`) whose own certificate is kept here, yours or someone's,
456    /// the revocation checked against it (review of 19 October 2026, R2).
457    /// Sequoia says such a revocation "could be", leaving that check to
458    /// whoever holds the revoker's certificate; one that does not check is
459    /// passed over, since anyone can attach one.
460    pub fn usable(&self, cert: &Cert) -> bool {
461        let policy = policy();
462        let Ok(valid) = cert.with_policy(&policy, None) else { return false };
463        match valid.revocation_status() {
464            openpgp::types::RevocationStatus::Revoked(_) => false,
465            openpgp::types::RevocationStatus::NotAsFarAsWeKnow => true,
466            openpgp::types::RevocationStatus::CouldBe(revocations) => {
467                let revokers: Vec<&Fingerprint> = cert.revocation_keys(&policy).map(|key| key.revoker().1).collect();
468                let kept = self.own.iter().chain(&self.others).filter(|kept| revokers.contains(&&kept.fingerprint()));
469                !kept.into_iter().any(|revoker| revocations.iter().any(|revocation| revocation.verify_primary_key_revocation(revoker.primary_key().key(), cert.primary_key().key()).is_ok()))
470            }
471        }
472    }
473
474    /// Whether `cert` serves `address`: bound to it and usable here (`names`,
475    /// [`Keys::usable`]).
476    fn serves(&self, cert: &Cert, address: &str) -> bool {
477        names(cert, address) && self.usable(cert)
478    }
479
480    /// Whether `fingerprint` is one of your certificates: kept with yours,
481    /// or your security keys' (`cards.toml`).
482    pub fn is_yours(&self, fingerprint: &Fingerprint) -> bool {
483        let hex = fingerprint.to_hex();
484        self.own.iter().any(|c| c.fingerprint() == *fingerprint) || self.all_cards().any(|card| card.cert.eq_ignore_ascii_case(&hex))
485    }
486
487    fn all(&self) -> impl Iterator<Item = &Cert> {
488        self.own.iter().chain(&self.others)
489    }
490
491    fn by_handle(&self, handle: &KeyHandle) -> Option<&Cert> {
492        self.all().find(|c| c.keys().any(|k| k.key().key_handle().aliases(handle)))
493    }
494}
495
496/// Whether the key `fingerprint` of `cert` can sign now: valid, alive, not
497/// revoked, allowed to sign.
498fn can_sign_with(cert: &Cert, fingerprint: &str) -> bool {
499    let policy = policy();
500    usable(cert) && cert.keys().with_policy(&policy, None).alive().revoked(false).for_signing().any(|k| k.key().fingerprint().to_hex().eq_ignore_ascii_case(fingerprint))
501}
502
503/// The key `fingerprint` of `cert`, its public part, primary or subkey.
504pub fn key_of(cert: &Cert, fingerprint: &str) -> Option<securitykey::PublicKey> {
505    cert.keys().find(|k| k.key().fingerprint().to_hex().eq_ignore_ascii_case(fingerprint)).map(|k| k.key().clone())
506}
507
508/// A key as the window lists it.
509#[derive(Debug, Clone, Serde, PartialEq, Eq)]
510pub struct KeySummary {
511    pub fingerprint: String,
512    /// "Jane Exemple <jane@example.org>", one per user ID.
513    pub names: Vec<String>,
514    pub own: bool,
515    /// "2026-10-03".
516    pub created: String,
517    /// "2029-10-03", or empty when it does not expire.
518    pub expires: String,
519    pub can_encrypt: bool,
520    /// The security key holding its private keys ("0006:12345678"); empty when they are here.
521    pub card: String,
522    /// Yours, made here: its revocation certificate is kept beside it (`revocation_path`).
523    pub revocation: bool,
524}
525
526fn summary(cert: &Cert, own: bool) -> KeySummary {
527    let policy = policy();
528    let valid = cert.with_policy(&policy, None).ok();
529    let date = |t: std::time::SystemTime| jiff::Timestamp::try_from(t).map(|t| t.strftime("%Y-%m-%d").to_string()).unwrap_or_default();
530    KeySummary {
531        fingerprint: cert.fingerprint().to_hex(),
532        // The user IDs bound to it, as matching reads them (`names`).
533        names: valid.as_ref().map(|v| v.userids().revoked(false).map(|u| String::from_utf8_lossy(u.userid().value()).to_string()).collect()).unwrap_or_default(),
534        own,
535        created: date(cert.primary_key().key().creation_time()),
536        expires: valid.as_ref().and_then(|v| v.primary_key().key_expiration_time()).map(date).unwrap_or_default(),
537        can_encrypt: usable(cert) && cert.keys().with_policy(&policy, None).alive().revoked(false).for_transport_encryption().next().is_some(),
538        card: String::new(),
539        revocation: false,
540    }
541}
542
543/// Every key, yours first; a security key's certificate names its key.
544pub fn summaries() -> Vec<KeySummary> {
545    let keys = Keys::load();
546    // Revoked by a designated revoker kept here (R2): said as unable to encrypt too.
547    let summarised = |c: &Cert, own: bool| KeySummary { can_encrypt: summary(c, own).can_encrypt && keys.usable(c), ..summary(c, own) };
548    let mut all: Vec<KeySummary> = keys.own.iter().map(|c| summarised(c, true)).chain(keys.others.iter().map(|c| summarised(c, false))).collect();
549    for key in all.iter_mut().filter(|k| k.own) {
550        if let Some(card) = keys.all_cards().find(|c| c.cert.eq_ignore_ascii_case(&key.fingerprint)) {
551            key.card = card.ident.clone();
552        }
553        key.revocation = revocation_path(&key.fingerprint).is_file();
554    }
555    all
556}
557
558/// Keeps a key that came from elsewhere for `address` (a lookup: its
559/// domain's Web Key Directory, keys.openpgp.org): its user IDs for that
560/// address alone. One of yours brought again so is merged without anything
561/// it names: only what your own primary key binds (a renewal's signatures,
562/// a subkey), never another source's user IDs into your certificate
563/// (review of 19 October 2026, F1).
564fn store_from_elsewhere_at(root: &std::path::Path, cert: Cert, address: &str) -> Option<KeySummary> {
565    let cert = for_its_address(cert.strip_secret_key_material(), address)?;
566    let fingerprint = cert.fingerprint().to_hex();
567    let yours = root.join("own").join(format!("{fingerprint}.asc"));
568    if let Some(known) = std::fs::read(&yours).ok().and_then(|bytes| Cert::from_bytes(&bytes).ok()) {
569        let names: Vec<Vec<u8>> = known.userids().map(|u| u.userid().value().to_vec()).collect();
570        let merged = known.merge_public(cert).ok()?.retain_userids(|u| names.iter().any(|n| n.as_slice() == u.userid().value()));
571        return store_in(root, only_bound_subkeys(merged), true).ok();
572    }
573    if yours_at(root, &fingerprint) {
574        return None;
575    }
576    store_in(root, cert, false).ok()
577}
578
579/// `cert` without the subkeys its own primary key does not bind under the
580/// policy (bound by nothing, or by another key): what a lookup brings of
581/// yours adds only what your key signed (review of 19 October 2026, Info:
582/// such subkeys were inert, but kept, and a security key's travelled in its
583/// shared file).
584fn only_bound_subkeys(cert: Cert) -> Cert {
585    let policy = policy();
586    let bound: Vec<Fingerprint> = cert.keys().subkeys().with_policy(&policy, None).map(|k| k.key().fingerprint()).collect();
587    cert.retain_subkeys(|k| bound.contains(&k.key().fingerprint()))
588}
589
590/// Whether the certificate `fingerprint` (hexadecimal) is one of yours in
591/// the folder `root` (`pgp/`): kept with your keys, a security key's read
592/// here, or one your other devices made known (`cards/`), as a phone knows
593/// your security key's certificate: never kept among others' keys, from
594/// wherever it comes again (your own mail's Autocrypt header, a lookup).
595fn yours_at(root: &std::path::Path, fingerprint: &str) -> bool {
596    root.join("own").join(format!("{fingerprint}.asc")).exists()
597        || securitykey::known_cards_in(&root.join("cards.toml")).iter().any(|card| card.cert.eq_ignore_ascii_case(fingerprint))
598        || securitykey::shared_cards_in(&root.join(securitykey::SHARED_CARDS)).iter().any(|(card, _)| card.cert.eq_ignore_ascii_case(fingerprint))
599}
600
601/// Keeps a key you chose (imported, made here) in the folder `root`
602/// (`pgp/`), merged with what was known of it. Secret parts go with yours,
603/// and so does a certificate of yours.
604fn store_at(root: &std::path::Path, cert: Cert) -> Result<KeySummary, String> {
605    let known_as_own = root.join("own").join(format!("{}.asc", cert.fingerprint().to_hex())).exists();
606    let own = cert.is_tsk() || known_as_own;
607    store_in(root, cert, own)
608}
609
610fn store_in(root: &std::path::Path, cert: Cert, own: bool) -> Result<KeySummary, String> {
611    if !own {
612        let learned = bound_addresses(&cert);
613        return store_others(root, cert, &learned);
614    }
615    let dir = root.join(if own { "own" } else { "others" });
616    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
617    let path = dir.join(format!("{}.asc", cert.fingerprint().to_hex()));
618    let merged = match std::fs::read(&path).ok().and_then(|bytes| Cert::from_bytes(&bytes).ok()) {
619        Some(known) => known.merge_public_and_secret(cert).map_err(|e| e.to_string())?,
620        None => cert,
621    };
622    let armored = if merged.is_tsk() { merged.as_tsk().armored().to_vec() } else { merged.armored().to_vec() }.map_err(|e| e.to_string())?;
623    write_key(&path, &armored)?;
624    // A security key's certificate kept again (renewed, a subkey added): your other devices follow.
625    let fingerprint = merged.fingerprint().to_hex();
626    if own && securitykey::known_cards_in(&root.join("cards.toml")).iter().any(|card| card.cert.eq_ignore_ascii_case(&fingerprint)) {
627        securitykey::share_cards_in(&root.join("cards.toml"), &dir, &root.join(securitykey::SHARED_CARDS), false)?;
628    }
629    Ok(summary(&merged, own))
630}
631
632/// Keeps someone's key for `learned`: the address it came for (a key from
633/// elsewhere comes cut to it, `for_its_address`) or, imported from a file
634/// you chose, every address bound to it. Merged with the one kept, it serves
635/// what it served and these (its file beside, written first), cut to their
636/// user IDs: an address a key kept before, or an older Sioul's write, named
637/// besides is never kept nor served (review of 19 October 2026, N2), and the
638/// user IDs of an address it was learned for before stay.
639fn store_others(root: &std::path::Path, cert: Cert, learned: &[String]) -> Result<KeySummary, String> {
640    let dir = root.join("others");
641    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
642    let fingerprint = cert.fingerprint().to_hex();
643    let path = dir.join(format!("{fingerprint}.asc"));
644    let kept = std::fs::read(&path).ok().and_then(|bytes| Cert::from_bytes(&bytes).ok());
645    let mut served = match &kept {
646        Some(kept) => record_of(&dir, kept).unwrap_or_else(|| {
647            let addresses = judged(kept);
648            Served { format: OTHERS_FORMAT, judged: addresses.clone(), addresses }
649        }),
650        None => Served { format: OTHERS_FORMAT, addresses: Vec::new(), judged: Vec::new() },
651    };
652    served.format = OTHERS_FORMAT;
653    for address in learned {
654        let address = address.trim();
655        served.judged.retain(|judged| !judged.eq_ignore_ascii_case(address));
656        if !served.addresses.iter().any(|a| a.trim().eq_ignore_ascii_case(address)) {
657            served.addresses.push(address.to_string());
658        }
659    }
660    let merged = match kept {
661        Some(kept) => kept.merge_public(cert.strip_secret_key_material()).map_err(|e| e.to_string())?,
662        None => cert.strip_secret_key_material(),
663    };
664    let merged = only_for(merged, &served.addresses);
665    write_served(&dir, &fingerprint, &served)?;
666    write_key(&path, &merged.armored().to_vec().map_err(|e| e.to_string())?)?;
667    Ok(summary(&merged, false))
668}
669
670/// Keeps the certificate of your security key with your keys: public parts only.
671pub fn keep_card_certificate(cert: &Cert) -> Result<KeySummary, String> {
672    store_in(&pgp_dir(), cert.clone().strip_secret_key_material(), true)
673}
674
675/// Writes a key file beside, then moves it in place. On Unix it is readable by
676/// you alone, whatever the umask: your secret keys, and whose keys you keep.
677fn write_key(path: &std::path::Path, armored: &[u8]) -> Result<(), String> {
678    let fail = |e: std::io::Error| format!("{}: {e}", path.display());
679    // Named after the whole file: a key and what it serves (`<fingerprint>.asc`,
680    // `.toml`) never share one; hidden, so that the sharing passes it over.
681    let temporary = path.with_file_name(format!(".{}.tmp", path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default()));
682    // A file left by an interrupted write keeps its permissions when truncated: it goes first.
683    let _ = std::fs::remove_file(&temporary);
684    let mut options = std::fs::OpenOptions::new();
685    options.write(true).create_new(true);
686    #[cfg(unix)]
687    {
688        use std::os::unix::fs::OpenOptionsExt;
689        options.mode(0o600);
690    }
691    let mut file = options.open(&temporary).map_err(fail)?;
692    file.write_all(armored).and_then(|()| file.sync_all()).map_err(fail)?;
693    drop(file);
694    std::fs::rename(&temporary, path).map_err(fail)
695}
696
697/// The certificates of `bytes` (armored or binary), read, not kept.
698pub fn certificates_in(bytes: &[u8]) -> Vec<Cert> {
699    certificates_of(bytes).unwrap_or_default()
700}
701
702/// The certificates among `bytes` (armored or binary, one or several), as a
703/// certificate from outside is read everywhere here (a file, GnuPG, a
704/// server, a message): read as they came, and again with their signatures'
705/// numbers written shortest (`minimal_mpis`), merged by fingerprint, in the
706/// order they came; so the repair only ever adds, never takes a certificate
707/// or a part away. Sequoia's words when neither reading reads.
708pub fn certificates_of(bytes: &[u8]) -> Result<Vec<Cert>, String> {
709    let read = |bytes: &[u8]| CertParser::from_bytes(bytes).map(|parser| parser.filter_map(Result::ok).collect::<Vec<Cert>>()).map_err(|e| e.to_string());
710    let as_they_came = read(bytes);
711    let shortest = match minimal_mpis(bytes) {
712        std::borrow::Cow::Borrowed(_) => return as_they_came,
713        std::borrow::Cow::Owned(repaired) => read(&repaired),
714    };
715    let (mut certs, shortest) = match (as_they_came, shortest) {
716        (Ok(certs), Ok(shortest)) => (certs, shortest),
717        (Ok(certs), Err(_)) => return Ok(certs),
718        (Err(_), shortest) => return shortest,
719    };
720    for cert in shortest {
721        match certs.iter().position(|kept| kept.fingerprint() == cert.fingerprint()) {
722            Some(index) => {
723                // The same key read both ways: every component either reading found.
724                if let Ok(merged) = certs[index].clone().merge_public_and_secret(cert) {
725                    certs[index] = merged;
726                }
727            }
728            None => certs.push(cert),
729        }
730    }
731    Ok(certs)
732}
733
734/// `bytes` (armored or binary) with the numbers of each signature written in
735/// their shortest form, as RFC 9580 (3.2) asks and Sequoia insists
736/// (`certificates_of` reads with it). GnuPG 2.4.4, Ubuntu 24.04's, writes
737/// about one key signature in 128 with a number one octet too long (a
738/// leading zero octet counted in its length); Sequoia then read that
739/// signature as none, and the subkey it binds, or the whole key, as missing.
740/// A signature's numbers are not part of what it signs: written shorter they
741/// are the same numbers, and Sequoia still checks the signature. Only ever
742/// shorter: a length said too short is left as it came. Armor is read block
743/// by block, every key block kept, in order. The bytes as they came when none
744/// needed it, or when they do not read as packets of known length; armored
745/// bytes come back binary once changed, which every reader here takes.
746pub fn minimal_mpis(bytes: &[u8]) -> std::borrow::Cow<'_, [u8]> {
747    use std::borrow::Cow;
748    // A binary packet starts with its tag's high bit set: the packets as they come.
749    if bytes.first().is_some_and(|b| b & 0x80 != 0) {
750        return shortened(bytes).map_or(Cow::Borrowed(bytes), Cow::Owned);
751    }
752    // Armor: each key block on its own, dearmored, then shortened where it
753    // needs it; every one kept, as Sequoia reads them all.
754    let mut out = Vec::new();
755    let mut changed = false;
756    for block in armored_keys(bytes) {
757        let mut reader = openpgp::armor::Reader::from_bytes(block, openpgp::armor::ReaderMode::Tolerant(None));
758        let mut binary = Vec::new();
759        if std::io::Read::read_to_end(&mut reader, &mut binary).is_err() || binary.is_empty() {
760            return Cow::Borrowed(bytes);
761        }
762        match shortened(&binary) {
763            Some(shorter) => {
764                out.extend_from_slice(&shorter);
765                changed = true;
766            }
767            None => out.extend_from_slice(&binary),
768        }
769    }
770    if changed { Cow::Owned(out) } else { Cow::Borrowed(bytes) }
771}
772
773/// The armored key blocks of `text`, in order: each from its "-----BEGIN PGP
774/// PUBLIC KEY BLOCK-----" (or PRIVATE) to the end of its "-----END PGP" line.
775/// What lies around or between them is left out, a message's or a
776/// signature's block among it.
777fn armored_keys(text: &[u8]) -> Vec<&[u8]> {
778    const BEGIN: &[u8] = b"-----BEGIN PGP ";
779    const END: &[u8] = b"-----END PGP ";
780    let find = |from: usize, needle: &[u8]| text.get(from..).and_then(|rest| rest.windows(needle.len()).position(|window| window == needle)).map(|i| from + i);
781    let mut blocks = Vec::new();
782    let mut at = 0;
783    while let Some(begin) = find(at, BEGIN) {
784        let Some(end) = find(begin + BEGIN.len(), END) else { break };
785        let line_end = text[end..].iter().position(|b| *b == b'\n').map_or(text.len(), |i| end + i + 1);
786        let kind = &text[begin + BEGIN.len()..];
787        if kind.starts_with(b"PUBLIC KEY BLOCK-----") || kind.starts_with(b"PRIVATE KEY BLOCK-----") {
788            blocks.push(&text[begin..line_end]);
789        }
790        at = line_end;
791    }
792    blocks
793}
794
795/// The packets of `binary` with each version 4 signature's numbers in their
796/// shortest form; none when none needed it, or when `binary` does not read
797/// as packets of known length (a partial length is data's, never a key's).
798fn shortened(binary: &[u8]) -> Option<Vec<u8>> {
799    let mut out = Vec::with_capacity(binary.len());
800    let mut changed = false;
801    let mut at = 0;
802    while at < binary.len() {
803        let (tag, body) = packet_at(binary, at)?;
804        let shorter = if tag == 2 && binary[body.clone()].first() == Some(&4) { signature_shortened(&binary[body.clone()]) } else { None };
805        match shorter {
806            // A signature, version 4 (RFC 9580, 5.2.3), whose numbers were written longer.
807            Some(shorter) => {
808                put_packet(&mut out, tag, &shorter);
809                changed = true;
810            }
811            None => out.extend_from_slice(&binary[at..body.end]),
812        }
813        at = body.end;
814    }
815    changed.then_some(out)
816}
817
818/// The packet at `at` (RFC 9580, 4.2): its tag and where its body lies; none
819/// for one cut short, or of partial length.
820fn packet_at(bytes: &[u8], at: usize) -> Option<(u8, std::ops::Range<usize>)> {
821    let ctb = *bytes.get(at)?;
822    let number = |from: usize, octets: usize| bytes.get(from..from + octets).map(|b| b.iter().fold(0usize, |n, b| (n << 8) | usize::from(*b)));
823    let (tag, start, length) = match ctb {
824        // OpenPGP's format: the tag in six bits, then one, two or five octets of length.
825        0xC0..=0xFF => match *bytes.get(at + 1)? {
826            first @ 0..=191 => (ctb & 0x3F, at + 2, usize::from(first)),
827            first @ 192..=223 => (ctb & 0x3F, at + 3, ((usize::from(first) - 192) << 8) + usize::from(*bytes.get(at + 2)?) + 192),
828            255 => (ctb & 0x3F, at + 6, number(at + 2, 4)?),
829            _ => return None,
830        },
831        // The legacy format: the tag in four bits, the length's size in two.
832        0x80..=0xBF => match ctb & 3 {
833            0 => ((ctb >> 2) & 0x0F, at + 2, number(at + 1, 1)?),
834            1 => ((ctb >> 2) & 0x0F, at + 3, number(at + 1, 2)?),
835            2 => ((ctb >> 2) & 0x0F, at + 5, number(at + 1, 4)?),
836            // Up to the end.
837            _ => ((ctb >> 2) & 0x0F, at + 1, bytes.len().checked_sub(at + 1)?),
838        },
839        _ => return None,
840    };
841    let end = start.checked_add(length)?;
842    (end <= bytes.len()).then_some((tag, start..end))
843}
844
845/// A version 4 signature's body with its numbers in their shortest form (no
846/// leading zero bits, RFC 9580, 3.2), and those of a signature embedded in
847/// its unhashed area (a signing subkey's back-signature, where GnuPG writes
848/// it); never anything in its hashed area, which the signature covers. None
849/// when nothing needed it, when its algorithm's are not numbers (Ed25519's own
850/// octets) or do not read whole, or when a length is said too short: only
851/// ever shorter, as GnuPG's fault is.
852fn signature_shortened(body: &[u8]) -> Option<Vec<u8>> {
853    shortened_at(body, true)
854}
855
856/// `signature_shortened`, the signatures embedded in the unhashed area
857/// looked into only when `descend`: one level, never more. An embedded
858/// signature's own embedded ones stay as they came: Sequoia refuses a
859/// signature embedded in an embedded one, and each level looked into would
860/// take the stack of the thread reading a file from outside (a phone's
861/// import runs on Qt's own, about 1 MiB), a crafted one nesting thousands.
862fn shortened_at(body: &[u8], descend: bool) -> Option<Vec<u8>> {
863    let numbers = match *body.get(2)? {
864        // RSA.
865        1 | 3 => 1,
866        // ElGamal, DSA, ECDSA, EdDSA (the legacy one GnuPG makes).
867        16 | 17 | 19 | 20 | 22 => 2,
868        _ => return None,
869    };
870    let length_at = |at: usize| body.get(at..at + 2).map(|two| usize::from(u16::from_be_bytes([two[0], two[1]])));
871    // Version, type, the key's and the hash's algorithms, then the hashed
872    // area after its length: as it came.
873    let unhashed = 4usize.checked_add(2 + length_at(4)?)?;
874    let mut out = body.get(..unhashed)?.to_vec();
875    // The unhashed area, its embedded signatures shortened (one level).
876    let area = body.get(unhashed + 2..unhashed + 2 + length_at(unhashed)?)?;
877    let (area_out, mut changed) = match descend.then(|| embedded_shortened(area)).flatten() {
878        Some(shorter) => (shorter, true),
879        None => (area.to_vec(), false),
880    };
881    out.extend_from_slice(&u16::try_from(area_out.len()).ok()?.to_be_bytes());
882    out.extend_from_slice(&area_out);
883    // The hash's first two octets, then the numbers.
884    let mut at = unhashed + 2 + area.len();
885    out.extend_from_slice(body.get(at..at + 2)?);
886    at += 2;
887    for _ in 0..numbers {
888        let bits = length_at(at)?;
889        let value = body.get(at + 2..at + 2 + bits.div_ceil(8))?;
890        at += 2 + value.len();
891        let value = &value[value.iter().position(|b| *b != 0).unwrap_or(value.len())..];
892        let shortest = value.first().map_or(0, |first| value.len() * 8 - first.leading_zeros() as usize);
893        if shortest > bits {
894            return None;
895        }
896        changed |= shortest < bits;
897        out.extend_from_slice(&u16::try_from(shortest).ok()?.to_be_bytes());
898        out.extend_from_slice(value);
899    }
900    (changed && at == body.len()).then_some(out)
901}
902
903/// A signature's unhashed area (RFC 9580, 5.2.3.7) with each embedded
904/// signature (subpacket 32) shortened, its own unhashed area left as it came
905/// (`shortened_at`), its length said again; none when none needed it, or
906/// when the area does not read as subpackets.
907fn embedded_shortened(area: &[u8]) -> Option<Vec<u8>> {
908    let mut out = Vec::with_capacity(area.len());
909    let mut changed = false;
910    let mut at = 0;
911    while at < area.len() {
912        let (start, length) = match *area.get(at)? {
913            first @ 0..=191 => (at + 1, usize::from(first)),
914            first @ 192..=254 => (at + 2, ((usize::from(first) - 192) << 8) + usize::from(*area.get(at + 1)?) + 192),
915            255 => (at + 5, usize::try_from(u32::from_be_bytes(area.get(at + 1..at + 5)?.try_into().ok()?)).ok()?),
916        };
917        // Its type (the critical bit aside), then its body.
918        let subpacket = area.get(start..start.checked_add(length)?)?;
919        let (&kind, inner) = subpacket.split_first()?;
920        match (kind & 0x7F == 32 && inner.first() == Some(&4)).then(|| shortened_at(inner, false)).flatten() {
921            Some(shorter) => {
922                put_subpacket_length(&mut out, 1 + shorter.len());
923                out.push(kind);
924                out.extend_from_slice(&shorter);
925                changed = true;
926            }
927            None => out.extend_from_slice(&area[at..start + length]),
928        }
929        at = start + length;
930    }
931    changed.then_some(out)
932}
933
934/// A subpacket's length (RFC 9580, 5.2.3.7), in one, two or five octets.
935fn put_subpacket_length(out: &mut Vec<u8>, length: usize) {
936    match length {
937        0..=191 => out.push(length as u8),
938        192..=16_319 => out.extend_from_slice(&[((length - 192) >> 8) as u8 + 192, (length - 192) as u8]),
939        _ => {
940            out.push(255);
941            out.extend_from_slice(&(length as u32).to_be_bytes());
942        }
943    }
944}
945
946/// A packet in OpenPGP's format: its tag, its length, its body.
947fn put_packet(out: &mut Vec<u8>, tag: u8, body: &[u8]) {
948    out.push(0xC0 | tag);
949    match body.len() {
950        length @ 0..=191 => out.push(length as u8),
951        length @ 192..=8383 => out.extend_from_slice(&[((length - 192) >> 8) as u8 + 192, (length - 192) as u8]),
952        length => {
953            out.push(255);
954            out.extend_from_slice(&(length as u32).to_be_bytes());
955        }
956    }
957    out.extend_from_slice(body);
958}
959
960/// Imports the keys of a file: armored or binary, public or secret.
961/// Someone's key imported so serves every address bound to it: you chose
962/// the file.
963pub fn import(bytes: &[u8]) -> Result<Vec<KeySummary>, String> {
964    import_at(&pgp_dir(), bytes)
965}
966
967/// [`import`] into the folder `root` (`pgp/`).
968fn import_at(root: &std::path::Path, bytes: &[u8]) -> Result<Vec<KeySummary>, String> {
969    let certs = certificates_of(bytes)?;
970    if certs.is_empty() {
971        return Err("no OpenPGP key".into());
972    }
973    certs.into_iter().map(|cert| store_at(root, cert)).collect()
974}
975
976/// Whether `passphrase` opens the secret parts of your key `fingerprint`.
977pub fn opens(fingerprint: &str, passphrase: &str) -> bool {
978    let keys = Keys::load();
979    let Some(cert) = keys.own.iter().find(|c| c.fingerprint().to_hex() == fingerprint) else { return false };
980    let policy = policy();
981    let password = Password::from(passphrase);
982    cert.keys().secret().with_policy(&policy, None).for_signing().next().is_some_and(|k| k.key().clone().decrypt_secret(&password).is_ok())
983}
984
985/// A long random passphrase for a key made here: nobody types it, the keyring keeps it.
986pub fn new_passphrase() -> Result<String, String> {
987    let mut bytes = [0u8; 24];
988    openpgp::crypto::random(&mut bytes).map_err(|e| e.to_string())?;
989    Ok(crate::lines::base64_encode(&bytes))
990}
991
992/// Makes a key for you: Curve25519, for signing and encrypting, valid three
993/// years; its secret parts encrypted with `passphrase`. Its revocation
994/// certificate, which Sequoia makes with it, is kept beside it
995/// (`revocation_path`): the only way to revoke the key once it is lost.
996pub fn generate(name: &str, address: &str, passphrase: &str) -> Result<KeySummary, String> {
997    generate_at(&pgp_dir(), name, address, passphrase)
998}
999
1000/// [`generate`] into the folder `root` (`pgp/`).
1001fn generate_at(root: &std::path::Path, name: &str, address: &str, passphrase: &str) -> Result<KeySummary, String> {
1002    let userid = if name.trim().is_empty() { format!("<{}>", address.trim()) } else { format!("{} <{}>", name.trim(), address.trim()) };
1003    let (cert, revocation) = CertBuilder::general_purpose(Some(userid))
1004        .set_cipher_suite(CipherSuite::Cv25519)
1005        .set_validity_period(std::time::Duration::from_secs(3 * 365 * 24 * 3600))
1006        .set_password(Some(Password::from(passphrase)))
1007        .generate()
1008        .map_err(|e| e.to_string())?;
1009    // The certificate first: a key is never kept without it.
1010    let own = root.join("own");
1011    std::fs::create_dir_all(&own).map_err(|e| e.to_string())?;
1012    let kept = own.join(format!("{}.rev", cert.fingerprint().to_hex()));
1013    write_key(&kept, revocation_text(&cert, revocation)?.as_bytes())?;
1014    match store_at(root, cert) {
1015        Ok(summary) => Ok(KeySummary { revocation: true, ..summary }),
1016        Err(e) => {
1017            let _ = std::fs::remove_file(&kept);
1018            Err(e)
1019        }
1020    }
1021}
1022
1023/// Where the revocation certificate of your key made here is kept: beside
1024/// the key, `pgp/own/<fingerprint>.rev`, readable by you alone (`write_key`),
1025/// as GnuPG keeps its own in `openpgp-revocs.d`.
1026pub fn revocation_path(fingerprint: &str) -> PathBuf {
1027    own_dir().join(format!("{fingerprint}.rev"))
1028}
1029
1030/// A revocation certificate as a file to keep: what it is, in a few lines,
1031/// then the signature armored, a colon before its first line so that it
1032/// cannot be imported by mistake (as GnuPG writes its own).
1033fn revocation_text(cert: &Cert, revocation: openpgp::packet::Signature) -> Result<String, String> {
1034    let mut armored = Vec::new();
1035    {
1036        let mut writer = openpgp::armor::Writer::with_headers(&mut armored, openpgp::armor::Kind::PublicKey, [("Comment", "This is a revocation certificate")]).map_err(|e| e.to_string())?;
1037        openpgp::Packet::from(revocation).serialize(&mut writer).map_err(|e| e.to_string())?;
1038        writer.finalize().map_err(|e| e.to_string())?;
1039    }
1040    let names: Vec<String> = cert.userids().map(|u| String::from_utf8_lossy(u.userid().value()).to_string()).collect();
1041    Ok(format!(
1042        "This is the revocation certificate of the OpenPGP key\n{}\n{}\n\n\
1043         Published, it tells everyone that this key must no longer be used, for\n\
1044         good: it cannot be taken back. Keep a copy where only you can reach it,\n\
1045         apart from the key, for the day the key is lost or stolen.\n\n\
1046         A colon stands before the five dashes below, so that the certificate\n\
1047         cannot be imported by mistake: take it away with a text editor before\n\
1048         you import the certificate and publish it.\n\n:{}",
1049        cert.fingerprint().to_hex(),
1050        names.join("\n"),
1051        String::from_utf8_lossy(&armored)
1052    ))
1053}
1054
1055/// Copies the revocation certificate of your key `fingerprint` into
1056/// `folder`, as `<fingerprint>.rev` (a number added when that name is
1057/// taken), yours alone; none kept for that key, an error.
1058pub fn save_revocation(fingerprint: &str, folder: &std::path::Path) -> Result<PathBuf, String> {
1059    save_revocation_at(&pgp_dir(), fingerprint, folder)
1060}
1061
1062/// [`save_revocation`] from the folder `root` (`pgp/`).
1063fn save_revocation_at(root: &std::path::Path, fingerprint: &str, folder: &std::path::Path) -> Result<PathBuf, String> {
1064    let text = std::fs::read(root.join("own").join(format!("{fingerprint}.rev"))).map_err(|e| e.to_string())?;
1065    std::fs::create_dir_all(folder).map_err(|e| e.to_string())?;
1066    let path = (1..)
1067        .map(|n| folder.join(if n == 1 { format!("{fingerprint}.rev") } else { format!("{fingerprint}-{n}.rev") }))
1068        .find(|p| !p.exists())
1069        .unwrap_or_else(|| folder.join(format!("{fingerprint}.rev")));
1070    write_key(&path, &text)?;
1071    Ok(path)
1072}
1073
1074/// A key's public part, armored, to give to others.
1075pub fn export_public(fingerprint: &str) -> Option<String> {
1076    let keys = Keys::load();
1077    let cert = keys.all().find(|c| c.fingerprint().to_hex() == fingerprint)?;
1078    cert.armored().to_vec().ok().map(|b| String::from_utf8_lossy(&b).to_string())
1079}
1080
1081/// Removes a key; yours are kept aside in `pgp/removed`, never deleted, with
1082/// their revocation certificate.
1083pub fn remove(fingerprint: &str) -> Result<(), String> {
1084    remove_in(&pgp_dir(), fingerprint)
1085}
1086
1087/// [`remove`] in the folder `root` (`pgp/`).
1088pub(crate) fn remove_in(root: &std::path::Path, fingerprint: &str) -> Result<(), String> {
1089    for (dir, own) in [(root.join("own"), true), (root.join("others"), false)] {
1090        let path = dir.join(format!("{fingerprint}.asc"));
1091        if !path.exists() {
1092            continue;
1093        }
1094        if own {
1095            let aside = root.join("removed");
1096            std::fs::create_dir_all(&aside).map_err(|e| e.to_string())?;
1097            std::fs::rename(&path, aside.join(format!("{fingerprint}.asc"))).map_err(|e| e.to_string())?;
1098            let revocation = dir.join(format!("{fingerprint}.rev"));
1099            if revocation.exists() {
1100                std::fs::rename(&revocation, aside.join(format!("{fingerprint}.rev"))).map_err(|e| e.to_string())?;
1101            }
1102        } else {
1103            std::fs::remove_file(&path).map_err(|e| e.to_string())?;
1104            let _ = std::fs::remove_file(served_path(&dir, fingerprint));
1105        }
1106    }
1107    Ok(())
1108}
1109
1110/// A signer of yours, as Sequoia takes it: a key here ([`here_signer`]) or
1111/// on a security key ([`securitykey::CardSigner`]).
1112pub type Signing<'a> = dyn crypto::Signer + Send + Sync + 'a;
1113
1114/// Your key here that signs, its secret part opened with `passphrase`: the
1115/// signer of a key kept on this device.
1116pub fn here_signer(cert: &Cert, passphrase: &str) -> Result<crypto::KeyPair, String> {
1117    let policy = policy();
1118    let key = cert.keys().secret().with_policy(&policy, None).alive().revoked(false).for_signing().next().ok_or("no signing key")?.key().clone();
1119    let key = if key.has_unencrypted_secret() { key } else { key.decrypt_secret(&Password::from(passphrase)).map_err(|_| "wrong passphrase".to_string())? };
1120    key.into_keypair().map_err(|e| e.to_string())
1121}
1122
1123/// A signer lent for one message: Sequoia's stream takes its signer by value,
1124/// and a security key's signer borrows what it needs to reach the key.
1125struct Lent<'r, 'o>(&'r mut Signing<'o>);
1126
1127impl crypto::Signer for Lent<'_, '_> {
1128    fn public(&self) -> &openpgp::packet::Key<key::PublicParts, key::UnspecifiedRole> {
1129        self.0.public()
1130    }
1131
1132    fn acceptable_hashes(&self) -> &[HashAlgorithm] {
1133        self.0.acceptable_hashes()
1134    }
1135
1136    fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result<mpi::Signature> {
1137        self.0.sign(hash_algo, digest)
1138    }
1139}
1140
1141/// A detached signature of `data`, armored, and the hash it used ("pgp-sha512"),
1142/// for multipart/signed (RFC 3156 §5). `signer` is your key here
1143/// ([`here_signer`]) or on a security key ([`securitykey::CardSigner`]).
1144pub fn sign_detached(data: &[u8], signer: &mut Signing<'_>) -> Result<(String, String), String> {
1145    let mut out = Vec::new();
1146    {
1147        let message = Message::new(&mut out);
1148        let message = Armorer::new(message).kind(openpgp::armor::Kind::Signature).build().map_err(|e| e.to_string())?;
1149        let mut signing = Signer::new(message, Lent(signer)).map_err(|e| e.to_string())?.detached().build().map_err(|e| e.to_string())?;
1150        signing.write_all(data).map_err(|e| e.to_string())?;
1151        signing.finalize().map_err(|e| e.to_string())?;
1152    }
1153    let micalg = openpgp::Packet::from_bytes(&out)
1154        .ok()
1155        .and_then(|p| match p {
1156            openpgp::Packet::Signature(s) => Some(format!("pgp-{}", s.hash_algo().to_string().to_ascii_lowercase())),
1157            _ => None,
1158        })
1159        .unwrap_or_else(|| "pgp-sha512".into());
1160    Ok((String::from_utf8_lossy(&out).to_string(), micalg))
1161}
1162
1163/// `data` encrypted to the recipients' keys, signed by yours when given; armored.
1164pub fn encrypt(data: &[u8], recipients: &[&Cert], signer: Option<&mut Signing<'_>>) -> Result<String, String> {
1165    let policy = policy();
1166    // Never to a certificate revoked as a whole (N3), whoever chose it.
1167    let keys: Vec<_> = recipients.iter().filter(|c| usable(c)).flat_map(|c| c.keys().with_policy(&policy, None).supported().alive().revoked(false).for_transport_encryption()).collect();
1168    encrypt_to(data, keys, signer)
1169}
1170
1171/// `data` encrypted for you alone, to `yours`: an encryption key of yours
1172/// that expired but was not revoked still takes it, since you open it with
1173/// that key, which Sequoia's policy allows (review of 19 October 2026, F19).
1174fn encrypt_for_yourself(data: &[u8], yours: &[&Cert]) -> Result<String, String> {
1175    let policy = policy();
1176    let keys: Vec<_> = yours.iter().filter(|c| usable(c)).flat_map(|c| c.keys().with_policy(&policy, None).supported().revoked(false).for_transport_encryption()).collect();
1177    encrypt_to(data, keys, None)
1178}
1179
1180fn encrypt_to<'a>(data: &[u8], keys: Vec<ValidErasedKeyAmalgamation<'a, openpgp::packet::key::PublicParts>>, signer: Option<&mut Signing<'_>>) -> Result<String, String> {
1181    if keys.is_empty() {
1182        return Err("no encryption key".into());
1183    }
1184    let mut out = Vec::new();
1185    {
1186        let message = Message::new(&mut out);
1187        let message = Armorer::new(message).build().map_err(|e| e.to_string())?;
1188        let message = Encryptor::for_recipients(message, keys).build().map_err(|e| e.to_string())?;
1189        let message = match signer {
1190            Some(signer) => Signer::new(message, Lent(signer)).map_err(|e| e.to_string())?.build().map_err(|e| e.to_string())?,
1191            None => message,
1192        };
1193        let mut literal = LiteralWriter::new(message).build().map_err(|e| e.to_string())?;
1194        literal.write_all(data).map_err(|e| e.to_string())?;
1195        literal.finalize().map_err(|e| e.to_string())?;
1196    }
1197    Ok(String::from_utf8_lossy(&out).to_string())
1198}
1199
1200/// A signature, checked.
1201#[derive(Debug, Clone, Serde, PartialEq, Eq)]
1202pub struct Checked {
1203    pub good: bool,
1204    /// The signer's first user ID, when their key is known.
1205    pub signer: String,
1206    pub fingerprint: String,
1207    /// Why it is not good: "unknown key", "bad signature", "revoked key"…
1208    pub problem: String,
1209}
1210
1211/// What opening a message found.
1212#[derive(Debug, Clone, Default)]
1213pub struct Opened {
1214    pub data: Vec<u8>,
1215    pub signatures: Vec<Checked>,
1216}
1217
1218/// The session keys of messages a security key opened, by message: such a
1219/// message opens again, with its attachments and its answer, without the key,
1220/// until Sioul closes. In memory only, never on disk.
1221#[derive(Default)]
1222pub struct SessionKeys(Mutex<BTreeMap<[u8; 32], (Option<SymmetricAlgorithm>, SessionKey)>>);
1223
1224impl SessionKeys {
1225    /// None yet.
1226    pub const fn new() -> SessionKeys {
1227        SessionKeys(Mutex::new(BTreeMap::new()))
1228    }
1229
1230    /// The session's own: one for each running Sioul.
1231    pub fn global() -> &'static SessionKeys {
1232        static GLOBAL: SessionKeys = SessionKeys::new();
1233        &GLOBAL
1234    }
1235
1236    fn get(&self, id: &[u8; 32]) -> Option<(Option<SymmetricAlgorithm>, SessionKey)> {
1237        self.0.lock().ok()?.get(id).cloned()
1238    }
1239
1240    fn keep(&self, id: [u8; 32], algo: Option<SymmetricAlgorithm>, key: SessionKey) {
1241        if let Ok(mut kept) = self.0.lock() {
1242            kept.insert(id, (algo, key));
1243        }
1244    }
1245
1246    /// Forgets them all: every message a security key opened needs it again.
1247    pub fn forget(&self) {
1248        if let Ok(mut kept) = self.0.lock() {
1249            kept.clear();
1250        }
1251    }
1252}
1253
1254/// A message's name among the session keys: SHA-256 of its encrypted bytes.
1255fn message_id(message: &[u8]) -> Option<[u8; 32]> {
1256    let mut context = HashAlgorithm::SHA256.context().ok()?.for_digest();
1257    context.update(message);
1258    context.into_digest().ok()?.try_into().ok()
1259}
1260
1261/// How your keys open what is encrypted to you.
1262pub struct Unlock<'a> {
1263    /// The passphrase of a key kept here, from the system keyring.
1264    pub passphrase: &'a dyn Fn(&Fingerprint) -> Option<String>,
1265    /// The session keys of messages a security key opened before.
1266    pub sessions: &'a SessionKeys,
1267    /// Your security key, when you asked to open with it now; without it, no
1268    /// security key is ever asked anything.
1269    pub card: Option<&'a CardAccess<'a>>,
1270    /// The security key a message waits for.
1271    wanted: RefCell<Option<String>>,
1272}
1273
1274impl<'a> Unlock<'a> {
1275    /// Your keys here, with their passphrases, and the session keys kept.
1276    pub fn new(passphrase: &'a dyn Fn(&Fingerprint) -> Option<String>, sessions: &'a SessionKeys) -> Unlock<'a> {
1277        Unlock { passphrase, sessions, card: None, wanted: RefCell::new(None) }
1278    }
1279
1280    /// And your security key, asked now.
1281    pub fn with_card(mut self, card: &'a CardAccess<'a>) -> Unlock<'a> {
1282        self.card = Some(card);
1283        self
1284    }
1285
1286    /// The security key the last message waits for: it is encrypted to it, and
1287    /// it was not asked, or it did not open it.
1288    pub fn wanted(&self) -> Option<String> {
1289        self.wanted.borrow().clone()
1290    }
1291}
1292
1293struct Helper<'a> {
1294    keys: &'a Keys,
1295    unlock: &'a Unlock<'a>,
1296    /// The message's name for its session key.
1297    id: Option<[u8; 32]>,
1298    checked: Vec<Checked>,
1299}
1300
1301impl VerificationHelper for Helper<'_> {
1302    fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
1303        Ok(ids.iter().filter_map(|id| self.keys.by_handle(id).cloned()).collect())
1304    }
1305
1306    fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> {
1307        for layer in structure {
1308            let MessageLayer::SignatureGroup { results } = layer else { continue };
1309            for result in results {
1310                self.checked.push(match result {
1311                    // Revoked by its designated revoker kept here (R2): Sequoia's
1312                    // verifier counts only a revocation it can check itself, and
1313                    // says this one "could be". Any other revocation it judged
1314                    // already, at the signature's time (a key superseded since
1315                    // still made good signatures before).
1316                    Ok(good) if matches!(good.ka.cert().revocation_status(&policy(), None), openpgp::types::RevocationStatus::CouldBe(_)) && !self.keys.usable(good.ka.cert()) => Checked { good: false, signer: String::new(), fingerprint: good.ka.cert().fingerprint().to_hex(), problem: "revoked key".into() },
1317                    Ok(good) => Checked {
1318                        good: true,
1319                        signer: good.ka.cert().userids().next().map(|u| String::from_utf8_lossy(u.userid().value()).to_string()).unwrap_or_default(),
1320                        fingerprint: good.ka.cert().fingerprint().to_hex(),
1321                        problem: String::new(),
1322                    },
1323                    Err(error) => {
1324                        use openpgp::parse::stream::VerificationError as E;
1325                        let (fingerprint, problem) = match &error {
1326                            E::MissingKey { sig } => (sig.issuer_fingerprints().next().map(|f| f.to_hex()).unwrap_or_default(), "unknown key"),
1327                            E::BadSignature { .. } => (String::new(), "bad signature"),
1328                            E::BadKey { .. } => (String::new(), "bad key"),
1329                            E::MalformedSignature { .. } => (String::new(), "malformed signature"),
1330                            E::UnboundKey { .. } => (String::new(), "unbound key"),
1331                            _ => (String::new(), "unknown"),
1332                        };
1333                        Checked { good: false, signer: String::new(), fingerprint, problem: problem.to_string() }
1334                    }
1335                });
1336            }
1337        }
1338        // Whatever the signatures say, the text is shown with what they say.
1339        Ok(())
1340    }
1341}
1342
1343impl DecryptionHelper for Helper<'_> {
1344    fn decrypt(&mut self, pkesks: &[PKESK], _skesks: &[SKESK], sym_algo: Option<SymmetricAlgorithm>, decrypt: &mut dyn FnMut(Option<SymmetricAlgorithm>, &SessionKey) -> bool) -> openpgp::Result<Option<Cert>> {
1345        // Opened before with a security key: its session key, kept in memory.
1346        if let Some(id) = &self.id
1347            && let Some((algo, session)) = self.unlock.sessions.get(id)
1348            && decrypt(algo, &session)
1349        {
1350            return Ok(None);
1351        }
1352        let policy = policy();
1353        for pkesk in pkesks {
1354            for cert in &self.keys.own {
1355                let passphrase = (self.unlock.passphrase)(&cert.fingerprint()).unwrap_or_default();
1356                for key in cert.keys().secret().with_policy(&policy, None).for_transport_encryption().chain(cert.keys().secret().with_policy(&policy, None).for_storage_encryption()) {
1357                    let recipient_matches = pkesk.recipient().is_none_or(|r| key.key().key_handle().aliases(&r));
1358                    if !recipient_matches {
1359                        continue;
1360                    }
1361                    let secret = key.key().clone();
1362                    let secret = if secret.has_unencrypted_secret() { secret } else if let Ok(s) = secret.decrypt_secret(&Password::from(passphrase.as_str())) { s } else { continue };
1363                    let Ok(mut pair) = secret.into_keypair() else { continue };
1364                    if let Some((algo, session)) = pkesk.decrypt(&mut pair, sym_algo)
1365                        && decrypt(algo, &session)
1366                    {
1367                        return Ok(Some(cert.clone()));
1368                    }
1369                }
1370            }
1371        }
1372        // Keys on a security key: asked only when you asked to open with it;
1373        // otherwise the message says which key it waits for.
1374        for pkesk in pkesks {
1375            let Some(recipient) = pkesk.recipient() else { continue };
1376            for card in self.keys.all_cards() {
1377                let Ok(fingerprint) = card.decrypt.parse::<Fingerprint>() else { continue };
1378                if !KeyHandle::from(fingerprint).aliases(&recipient) {
1379                    continue;
1380                }
1381                let Some((cert, public)) = self.keys.cert(&card.cert).and_then(|cert| Some((cert, key_of(cert, &card.decrypt)?))) else { continue };
1382                *self.unlock.wanted.borrow_mut() = Some(card.ident.clone());
1383                let Some(access) = self.unlock.card else { continue };
1384                let mut decryptor = CardDecryptor::new(access, &card.ident, public);
1385                if let Some((algo, session)) = pkesk.decrypt(&mut decryptor, sym_algo)
1386                    && decrypt(algo, &session)
1387                {
1388                    *self.unlock.wanted.borrow_mut() = None;
1389                    if let Some(id) = self.id {
1390                        self.unlock.sessions.keep(id, algo, session);
1391                    }
1392                    return Ok(Some(cert.clone()));
1393                }
1394            }
1395        }
1396        Err(openpgp::Error::InvalidOperation("no key of yours opens this message".into()).into())
1397    }
1398}
1399
1400/// The most a decrypted message may hold: anyone can encrypt to your key, and
1401/// a few kilobytes of compressed data can inflate to gigabytes.
1402const LARGEST_DECRYPTED: u64 = 256 * 1024 * 1024;
1403
1404/// Decrypts an OpenPGP message (armored or not) with your keys, checking the
1405/// signatures inside. `unlock` gives the passphrase of a key of yours here,
1406/// the session keys kept, and your security key when you asked to use it.
1407pub fn decrypt(message: &[u8], keys: &Keys, unlock: &Unlock) -> Result<Opened, String> {
1408    use std::io::Read as _;
1409    let policy = policy();
1410    let helper = Helper { keys, unlock, id: message_id(message), checked: Vec::new() };
1411    let mut decryptor = DecryptorBuilder::from_bytes(message).map_err(|e| e.to_string())?.with_policy(&policy, None, helper).map_err(|e| e.to_string())?;
1412    let mut data = Vec::new();
1413    (&mut decryptor).take(LARGEST_DECRYPTED + 1).read_to_end(&mut data).map_err(|e| e.to_string())?;
1414    if data.len() as u64 > LARGEST_DECRYPTED {
1415        return Err(format!("larger than {LARGEST_DECRYPTED} bytes once decrypted"));
1416    }
1417    let helper = decryptor.into_helper();
1418    Ok(Opened { data, signatures: helper.checked })
1419}
1420
1421/// Checks a detached signature of `data` (multipart/signed).
1422pub fn verify_detached(data: &[u8], signature: &[u8], keys: &Keys) -> Vec<Checked> {
1423    let policy = policy();
1424    let none = |_: &Fingerprint| None;
1425    let sessions = SessionKeys::new();
1426    let unlock = Unlock::new(&none, &sessions);
1427    let helper = Helper { keys, unlock: &unlock, id: None, checked: Vec::new() };
1428    let Ok(builder) = DetachedVerifierBuilder::from_bytes(signature) else { return Vec::new() };
1429    let Ok(mut verifier) = builder.with_policy(&policy, None, helper) else { return Vec::new() };
1430    let _ = verifier.verify_bytes(data);
1431    verifier.into_helper().checked
1432}
1433
1434/// Where a domain's Web Key Directory keeps the key of `address`: the
1435/// advanced address, then the direct one (draft-koch-openpgp-webkey-service §3.1).
1436pub fn wkd_urls(address: &str) -> Option<[String; 2]> {
1437    let (local, domain) = address.trim().rsplit_once('@')?;
1438    let domain = domain.to_ascii_lowercase();
1439    // A name in the DNS, nothing else: an address answered may come from anyone,
1440    // and "x@192.168.1.1", "x@[::1]" or "x@host:8080/path?" would send the
1441    // request elsewhere than to a domain's Web Key Directory.
1442    let labels: Vec<&str> = domain.split('.').collect();
1443    let named = labels.len() >= 2
1444        && labels.iter().all(|l| !l.is_empty() && !l.starts_with('-') && !l.ends_with('-') && l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'))
1445        && labels.last().is_some_and(|tld| tld.chars().any(|c| c.is_ascii_alphabetic()) && *tld != "localhost");
1446    if !named {
1447        return None;
1448    }
1449    let mut context = openpgp::types::HashAlgorithm::SHA1.context().ok()?.for_digest();
1450    context.update(local.to_lowercase().as_bytes());
1451    let digest = context.into_digest().ok()?;
1452    let hash = zbase32(&digest);
1453    let query = local.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") }).collect::<String>();
1454    Some([
1455        format!("https://openpgpkey.{domain}/.well-known/openpgpkey/{domain}/hu/{hash}?l={query}"),
1456        format!("https://{domain}/.well-known/openpgpkey/hu/{hash}?l={query}"),
1457    ])
1458}
1459
1460/// z-base-32 (RFC 6189 §5.1.6), as WKD names keys.
1461fn zbase32(bytes: &[u8]) -> String {
1462    const ALPHABET: &[u8; 32] = b"ybndrfg8ejkmcpqxot1uwisza345h769";
1463    let mut out = String::new();
1464    let (mut buffer, mut bits) = (0u32, 0u32);
1465    for &byte in bytes {
1466        buffer = (buffer << 8) | u32::from(byte);
1467        bits += 8;
1468        while bits >= 5 {
1469            bits -= 5;
1470            out.push(ALPHABET[((buffer >> bits) & 31) as usize] as char);
1471        }
1472    }
1473    if bits > 0 {
1474        out.push(ALPHABET[((buffer << (5 - bits)) & 31) as usize] as char);
1475    }
1476    out
1477}
1478
1479/// Keeps keys found for `address`: public parts only, and only those naming it.
1480pub fn learn(bytes: &[u8], address: &str) -> Vec<KeySummary> {
1481    learn_at(&pgp_dir(), bytes, address)
1482}
1483
1484/// [`learn`] into the folder `root` (`pgp/`).
1485fn learn_at(root: &std::path::Path, bytes: &[u8], address: &str) -> Vec<KeySummary> {
1486    certificates_of(bytes).unwrap_or_default().into_iter().filter_map(|c| store_from_elsewhere_at(root, c, address)).collect()
1487}
1488
1489/// What a protected message showed when opened, for the reader's line.
1490#[derive(Debug, Clone, Default, Serde, PartialEq, Eq)]
1491pub struct PgpView {
1492    pub encrypted: bool,
1493    /// Decrypted; false when no key of yours opens it.
1494    pub opened: bool,
1495    pub signatures: Vec<Checked>,
1496    /// Not opened, and encrypted to your security key of this identifier: the
1497    /// reader offers "Open with your security key".
1498    pub security_key: Option<String>,
1499    /// This device cannot use a security key yet (a phone): a message waiting
1500    /// for yours says to open it on a computer, and offers no button.
1501    pub key_elsewhere: bool,
1502}
1503
1504/// Lone LF become CRLF: what was signed is the canonical form (RFC 3156 §5).
1505fn canonical(bytes: &[u8]) -> Vec<u8> {
1506    let mut out = Vec::with_capacity(bytes.len() + bytes.len() / 32);
1507    for (i, &b) in bytes.iter().enumerate() {
1508        if b == b'\n' && (i == 0 || bytes[i - 1] != b'\r') {
1509            out.push(b'\r');
1510        }
1511        out.push(b);
1512    }
1513    out
1514}
1515
1516fn kind_of(part: &mail_parser::MessagePart) -> (String, String) {
1517    use mail_parser::MimeHeaders;
1518    part.content_type().map_or_else(Default::default, |t| {
1519        (format!("{}/{}", t.ctype(), t.subtype().unwrap_or("")).to_ascii_lowercase(), t.attribute("protocol").unwrap_or("").to_ascii_lowercase())
1520    })
1521}
1522
1523/// The message's own header lines but its MIME ones, which the decrypted body brings.
1524fn outer_headers(raw: &[u8], body_offset: usize) -> Vec<u8> {
1525    let head = String::from_utf8_lossy(&raw[..body_offset.min(raw.len())]).to_string();
1526    let mut out = String::new();
1527    let mut keep = false;
1528    for line in head.split_inclusive('\n') {
1529        if !line.starts_with([' ', '\t']) {
1530            let name = line.split(':').next().unwrap_or("").trim().to_ascii_lowercase();
1531            keep = !name.is_empty() && !name.starts_with("content-") && name != "mime-version";
1532        }
1533        if keep {
1534            out.push_str(line);
1535        }
1536    }
1537    canonical(out.trim_end().as_bytes()).into_iter().chain(b"\r\nMIME-Version: 1.0\r\n".iter().copied()).collect()
1538}
1539
1540/// Opens a protected message: PGP/MIME encrypted or signed (RFC 3156), or
1541/// inline PGP. Gives back the message as it reads once decrypted (its own
1542/// headers, then what was inside), when it was encrypted, and what was found;
1543/// None when nothing is protected. A message encrypted to your security key
1544/// opens only when `unlock` carries it, or from a session key kept; otherwise
1545/// its view names the key it waits for.
1546pub fn open(raw: &[u8], keys: &Keys, unlock: &Unlock) -> Option<(Option<Vec<u8>>, PgpView)> {
1547    let message = mail_parser::MessageParser::default().parse(raw)?;
1548    let root = message.parts.first()?;
1549    let (kind, protocol) = kind_of(root);
1550    let children: Vec<&mail_parser::MessagePart> = root.sub_parts().map(|ids| ids.iter().filter_map(|&id| message.parts.get(id as usize)).collect()).unwrap_or_default();
1551    let closed = || PgpView { encrypted: true, opened: false, signatures: Vec::new(), security_key: unlock.wanted(), key_elsewhere: false };
1552    if kind == "multipart/encrypted" && protocol == "application/pgp-encrypted" {
1553        let payload = children.iter().find(|p| kind_of(p).0 == "application/octet-stream").map(|p| p.contents().to_vec())?;
1554        return Some(match decrypt(&payload, keys, unlock) {
1555            Ok(opened) => {
1556                let mut virtual_message = outer_headers(raw, root.raw_body_offset() as usize);
1557                virtual_message.extend(canonical(&opened.data));
1558                let mut signatures = opened.signatures;
1559                // Signed, then encrypted as a multipart/signed (RFC 3156 §6.1): checked too.
1560                if let Some((_, inner)) = open(&virtual_message, keys, unlock) {
1561                    signatures.extend(inner.signatures);
1562                }
1563                (Some(virtual_message), PgpView { encrypted: true, opened: true, signatures, security_key: None, key_elsewhere: false })
1564            }
1565            Err(_) => (None, closed()),
1566        });
1567    }
1568    if kind == "multipart/signed" && protocol == "application/pgp-signature" && children.len() >= 2 {
1569        let signed = children[0];
1570        let (start, end) = (signed.raw_header_offset() as usize, signed.raw_end_offset() as usize);
1571        let data = canonical(raw.get(start..end)?);
1572        let signature = children.iter().find(|p| kind_of(p).0 == "application/pgp-signature")?.contents().to_vec();
1573        // The line break before the boundary belongs to the boundary; parsers
1574        // differ on where a part ends, so the part is checked with and without it.
1575        let mut checked = verify_detached(&data, &signature, keys);
1576        if !checked.iter().any(|c| c.good) && data.ends_with(b"\r\n") {
1577            let trimmed = verify_detached(&data[..data.len() - 2], &signature, keys);
1578            if trimmed.iter().any(|c| c.good) {
1579                checked = trimmed;
1580            }
1581        }
1582        return Some((None, PgpView { encrypted: false, opened: true, signatures: checked, security_key: None, key_elsewhere: false }));
1583    }
1584    // Inline: an armored message in the text.
1585    let text = message.body_text(0)?.to_string();
1586    let start = text.find("-----BEGIN PGP MESSAGE-----")?;
1587    let end = text[start..].find("-----END PGP MESSAGE-----").map(|e| start + e + "-----END PGP MESSAGE-----".len())?;
1588    Some(match decrypt(text[start..end].as_bytes(), keys, unlock) {
1589        Ok(opened) => {
1590            let mut virtual_message = outer_headers(raw, root.raw_body_offset() as usize);
1591            virtual_message.extend_from_slice(b"Content-Type: text/plain; charset=utf-8\r\n\r\n");
1592            virtual_message.extend(canonical(&opened.data));
1593            (Some(virtual_message), PgpView { encrypted: true, opened: true, signatures: opened.signatures, security_key: None, key_elsewhere: false })
1594        }
1595        Err(_) => (None, closed()),
1596    })
1597}
1598
1599/// Whether a message is encrypted with OpenPGP, as a whole: PGP/MIME (RFC
1600/// 3156, the shape [`seal_message`] writes), or an inline message whose whole
1601/// first text part is one armored message. Never a text that quotes one
1602/// among its words (review of 19 October 2026, F7: a decrypted copy quoting
1603/// an older armored block was taken for sealed). Nothing is opened.
1604pub fn is_encrypted(raw: &[u8]) -> bool {
1605    let Some(message) = mail_parser::MessageParser::default().parse(raw) else { return false };
1606    let Some(root) = message.parts.first() else { return false };
1607    let (kind, protocol) = kind_of(root);
1608    if kind == "multipart/encrypted" && protocol == "application/pgp-encrypted" {
1609        return true;
1610    }
1611    message.body_text(0).is_some_and(|text| {
1612        let text = text.trim();
1613        text.starts_with("-----BEGIN PGP MESSAGE-----") && text.ends_with("-----END PGP MESSAGE-----") && text.matches("-----BEGIN PGP MESSAGE-----").count() == 1
1614    })
1615}
1616
1617/// The head of a message as [`open`] rebuilds an encrypted one: its outer
1618/// header lines (none of them `Content-` nor `MIME-Version`), the line
1619/// `MIME-Version: 1.0` it adds after them, then the opened entity's own
1620/// header lines, a `Content-` one among them. Where that line is, when the
1621/// head has that shape: what a copy decrypted by Sioul before 19 October
1622/// 2026 looks like (a message that came in clear may too).
1623fn opened_split(raw: &[u8]) -> Option<usize> {
1624    mime_split(raw, true)
1625}
1626
1627/// Where the `MIME-Version: 1.0` line [`open`] adds after a message's outer
1628/// headers starts, when nothing of MIME comes before it; with `content`,
1629/// only when a content's header follows it (what `open` rebuilt from an
1630/// entity with headers of its own). Without, whatever follows: an entity
1631/// with an empty head, or none, rebuilt so (review of 19 October 2026, N7).
1632fn mime_split(raw: &[u8], content: bool) -> Option<usize> {
1633    let head = &raw[..header_end(raw)];
1634    let mut at = 0;
1635    let mut seen_mime = None;
1636    let mut content_after = false;
1637    for line in head.split_inclusive(|b| *b == b'\n') {
1638        let start = at;
1639        at += line.len();
1640        if line.starts_with(b" ") || line.starts_with(b"\t") || line.trim_ascii().is_empty() {
1641            continue;
1642        }
1643        let name = String::from_utf8_lossy(line.split(|b| *b == b':').next().unwrap_or_default()).trim().to_ascii_lowercase();
1644        match seen_mime {
1645            None if line == b"MIME-Version: 1.0\r\n" => seen_mime = Some(start),
1646            None if name.starts_with("content-") || name == "mime-version" => return None,
1647            None => {}
1648            Some(_) => content_after |= name.starts_with("content-"),
1649        }
1650    }
1651    seen_mime.filter(|_| content_after || !content)
1652}
1653
1654/// Whether `raw` has the head of a message [`open`] rebuilt (`opened_split`):
1655/// what may be a copy decrypted by Sioul before 19 October 2026.
1656pub fn looks_opened(raw: &[u8]) -> bool {
1657    opened_split(raw).is_some()
1658}
1659
1660/// Where a message's header ends: after its first empty line.
1661fn header_end(raw: &[u8]) -> usize {
1662    let crlf = raw.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4);
1663    let lf = raw.windows(2).position(|w| w == b"\n\n").map(|p| p + 2);
1664    match (crlf, lf) {
1665        (Some(a), Some(b)) => a.min(b),
1666        (a, b) => a.or(b).unwrap_or(raw.len()),
1667    }
1668}
1669
1670/// A message as it reads (opened, in clear), sealed again for `to`, as an
1671/// encrypted message comes (PGP/MIME, RFC 3156): its own headers in clear but
1672/// those of its content (From, To, Date, Subject, Message-ID: what a list of
1673/// mail shows), its content and their headers encrypted inside. [`open`]
1674/// gives the same message back. For the copy kept beside a draft that
1675/// answers an encrypted message, never left in clear.
1676pub fn seal_message(clear: &[u8], to: &[&Cert]) -> Result<Vec<u8>, String> {
1677    let end = header_end(clear);
1678    let (head, body) = clear.split_at(end);
1679    let (mut outer, mut inner): (Vec<u8>, Vec<u8>) = (Vec::new(), Vec::new());
1680    // A message `open` rebuilt: outside, only the outer headers it came with
1681    // (those before the `MIME-Version: 1.0` it adds); inside, everything
1682    // after, as it is: the entity's own headers whatever they are (protected
1683    // headers, the subject its sender encrypted, never come out in clear:
1684    // review of 19 October 2026, F8), or no head at all (N7: its first line
1685    // would read as a header, and stay outside). Any other message: its own
1686    // headers outside, its content's (`Content-`) inside.
1687    let payload = if let Some(split) = mime_split(clear, false) {
1688        let mime = b"MIME-Version: 1.0\r\n".len();
1689        outer.extend_from_slice(&head[..split]);
1690        clear[split + mime..].to_vec()
1691    } else {
1692        let mut into_outer = true;
1693        let mut skip = false;
1694        for line in head.split_inclusive(|b| *b == b'\n') {
1695            if line == b"\r\n" || line == b"\n" {
1696                continue;
1697            }
1698            if !line.starts_with(b" ") && !line.starts_with(b"\t") {
1699                let name = String::from_utf8_lossy(line.split(|b| *b == b':').next().unwrap_or_default()).trim().to_ascii_lowercase();
1700                skip = name == "mime-version";
1701                into_outer = !name.starts_with("content-");
1702            }
1703            if skip {
1704                continue;
1705            }
1706            if into_outer { outer.extend_from_slice(line) } else { inner.extend_from_slice(line) }
1707        }
1708        if inner.is_empty() {
1709            inner.extend_from_slice(b"Content-Type: text/plain; charset=utf-8\r\n");
1710        }
1711        let mut payload = canonical(&inner);
1712        payload.extend_from_slice(b"\r\n");
1713        payload.extend_from_slice(body);
1714        payload
1715    };
1716    let armored = encrypt_for_yourself(&payload, to)?;
1717    let boundary = "sioul-sealed-copy";
1718    let mut out = canonical(&outer);
1719    out.extend_from_slice(
1720        format!(
1721            "MIME-Version: 1.0\r\nContent-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"{boundary}\"\r\n\r\n--{boundary}\r\nContent-Type: application/pgp-encrypted\r\nContent-Description: PGP/MIME version identification\r\n\r\nVersion: 1\r\n\r\n--{boundary}\r\nContent-Type: application/octet-stream; name=\"encrypted.asc\"\r\nContent-Disposition: inline; filename=\"encrypted.asc\"\r\n\r\n{}\r\n--{boundary}--\r\n",
1722            canonical(armored.trim_end().as_bytes()).iter().map(|&b| b as char).collect::<String>()
1723        )
1724        .as_bytes(),
1725    );
1726    Ok(out)
1727}
1728
1729/// A key for tests, made now (Curve25519: its primary key certifies and
1730/// signs, a subkey decrypts), named `userid`, its secret part unencrypted,
1731/// armored: for the crates that do not use Sequoia themselves. Never one of
1732/// yours.
1733pub fn stand_in_key(userid: &str) -> Result<Vec<u8>, String> {
1734    let (cert, _) = CertBuilder::new().set_cipher_suite(CipherSuite::Cv25519).add_userid(userid).add_transport_encryption_subkey().generate().map_err(|e| e.to_string())?;
1735    cert.as_tsk().armored().to_vec().map_err(|e| e.to_string())
1736}
1737
1738/// The keys a message is encrypted to, as its packets name them (key IDs or
1739/// fingerprints, hexadecimal), in its order: whom it is for, nothing opened.
1740pub fn recipients_of(raw: &[u8]) -> Vec<String> {
1741    let Some(message) = mail_parser::MessageParser::default().parse(raw) else { return Vec::new() };
1742    let payload = message.parts.iter().find(|p| kind_of(p).0 == "application/octet-stream").map(|p| p.contents().to_vec()).unwrap_or_else(|| raw.to_vec());
1743    let mut out = Vec::new();
1744    let Ok(mut parsing) = openpgp::parse::PacketParser::from_bytes(&payload) else { return out };
1745    while let openpgp::parse::PacketParserResult::Some(packet) = parsing {
1746        if let openpgp::Packet::PKESK(pkesk) = &packet.packet {
1747            out.push(pkesk.recipient().map(|r| r.to_hex()).unwrap_or_default());
1748        }
1749        match packet.next() {
1750            Ok((_, next)) => parsing = next,
1751            Err(_) => break,
1752        }
1753    }
1754    out
1755}
1756
1757/// Autocrypt (Level 1, §2.1): the key a message's header carries for its sender.
1758pub fn autocrypt_key(header: &str, from: &str) -> Option<Cert> {
1759    let mut addr = None;
1760    let mut keydata = None;
1761    for part in header.split(';') {
1762        let (name, value) = part.split_once('=')?;
1763        match name.trim() {
1764            "addr" => addr = Some(value.trim().to_string()),
1765            "keydata" => keydata = Some(value.split_whitespace().collect::<String>()),
1766            _ => {}
1767        }
1768    }
1769    if !addr?.eq_ignore_ascii_case(from.trim()) {
1770        return None;
1771    }
1772    let bytes = crate::lines::base64_decode(&keydata?)?;
1773    // One certificate, as `Cert::from_bytes` reads it, kept for `addr`
1774    // alone (Autocrypt Level 1 binds the key to it; its other user IDs, bound
1775    // or not, are decoration: review of 19 October 2026, F1 and F2).
1776    let [cert] = <[Cert; 1]>::try_from(certificates_of(&bytes).ok()?).ok()?;
1777    for_its_address(cert, from)
1778}
1779
1780/// Keeps a key from an Autocrypt header: only public parts, only for its
1781/// sender's address, never one of yours (whoever sends your own key back
1782/// cannot add anything to it).
1783pub fn learn_autocrypt(header: &str, from: &str) -> Option<KeySummary> {
1784    learn_autocrypt_at(&pgp_dir(), header, from)
1785}
1786
1787/// [`learn_autocrypt`] into the folder `root` (`pgp/`).
1788fn learn_autocrypt_at(root: &std::path::Path, header: &str, from: &str) -> Option<KeySummary> {
1789    let cert = autocrypt_key(header, from)?.strip_secret_key_material();
1790    if yours_at(root, &cert.fingerprint().to_hex()) {
1791        return None;
1792    }
1793    store_in(root, cert, false).ok()
1794}
1795
1796/// The Autocrypt header for your outgoing mail: your key, smallest form
1797/// (primary, encryption subkey, the user ID with your address).
1798pub fn autocrypt_header(cert: &Cert, address: &str) -> Option<String> {
1799    let policy = policy();
1800    let valid = cert.with_policy(&policy, None).ok()?;
1801    let userid = valid.userids().find(|u| u.userid().email().ok().flatten().is_some_and(|e| e.eq_ignore_ascii_case(address)))?;
1802    let encryption = valid.keys().subkeys().alive().revoked(false).for_transport_encryption().next()?;
1803    let mut packets: Vec<openpgp::Packet> = vec![cert.primary_key().key().clone().into()];
1804    packets.push(userid.userid().clone().into());
1805    packets.extend(userid.self_signatures().take(1).cloned().map(Into::into));
1806    packets.push(encryption.key().clone().into());
1807    packets.extend(encryption.self_signatures().take(1).cloned().map(Into::into));
1808    let minimal = Cert::from_packets(packets.into_iter()).ok()?;
1809    let mut bytes = Vec::new();
1810    minimal.serialize(&mut bytes).ok()?;
1811    // A space every 76 characters, where the header may be folded (Autocrypt §2.1):
1812    // a line holds 998 characters at most (RFC 5322 §2.1.1), and an RSA key
1813    // imported from GnuPG takes several thousand.
1814    let encoded: Vec<char> = crate::lines::base64_encode(&bytes).chars().collect();
1815    let keydata = encoded.chunks(76).map(|chunk| chunk.iter().collect::<String>()).collect::<Vec<_>>().join(" ");
1816    Some(format!("addr={address}; keydata={keydata}"))
1817}
1818
1819#[cfg(test)]
1820mod tests {
1821    use super::*;
1822
1823    fn temporary() -> PathBuf {
1824        let dir = std::env::temp_dir().join(format!("sioul-pgp-{}-{}", std::process::id(), std::thread::current().name().unwrap_or("t").replace("::", "-")));
1825        std::fs::create_dir_all(&dir).unwrap();
1826        dir
1827    }
1828
1829    fn key(address: &str) -> Cert {
1830        CertBuilder::general_purpose(Some(format!("Someone <{address}>")))
1831            .set_cipher_suite(CipherSuite::Cv25519)
1832            .set_password(Some(Password::from("secret words")))
1833            .generate()
1834            .unwrap()
1835            .0
1836    }
1837
1838    #[test]
1839    fn signs_encrypts_decrypts_and_verifies() {
1840        let _ = temporary();
1841        let alice = key("alice@example.org");
1842        let bob = key("bob@example.org");
1843        let keys_of_bob = Keys { own: vec![bob.clone()], others: vec![alice.clone().strip_secret_key_material()], ..Keys::default() };
1844        let mut signer = here_signer(&alice, "secret words").unwrap();
1845        let sealed = encrypt(b"Content-Type: text/plain\r\n\r\nHello Bob.\r\n", &[&bob, &alice], Some(&mut signer)).unwrap();
1846        assert!(sealed.starts_with("-----BEGIN PGP MESSAGE-----"));
1847        let passphrase = |_: &Fingerprint| Some("secret words".to_string());
1848        let sessions = SessionKeys::new();
1849        let opened = decrypt(sealed.as_bytes(), &keys_of_bob, &Unlock::new(&passphrase, &sessions)).unwrap();
1850        assert_eq!(opened.data, b"Content-Type: text/plain\r\n\r\nHello Bob.\r\n");
1851        assert_eq!(opened.signatures.len(), 1);
1852        assert!(opened.signatures[0].good && opened.signatures[0].signer.contains("alice@example.org"));
1853        let wrong = |_: &Fingerprint| Some("other words".to_string());
1854        assert!(decrypt(sealed.as_bytes(), &keys_of_bob, &Unlock::new(&wrong, &sessions)).is_err());
1855        assert!(here_signer(&alice, "other words").is_err());
1856        let (signature, micalg) = sign_detached(b"signed part", &mut signer).unwrap();
1857        assert!(micalg.starts_with("pgp-sha"), "{micalg}");
1858        let checked = verify_detached(b"signed part", signature.as_bytes(), &keys_of_bob);
1859        assert!(checked[0].good);
1860        let tampered = verify_detached(b"signed part!", signature.as_bytes(), &keys_of_bob);
1861        assert!(!tampered[0].good);
1862        let stranger = verify_detached(b"signed part", signature.as_bytes(), &Keys::default());
1863        assert_eq!(stranger[0].problem, "unknown key");
1864    }
1865
1866    #[test]
1867    fn key_files_are_yours_alone() {
1868        let path = temporary().join("key.asc");
1869        write_key(&path, b"-----BEGIN PGP PRIVATE KEY BLOCK-----").unwrap();
1870        write_key(&path, b"-----BEGIN PGP PRIVATE KEY BLOCK----- again").unwrap();
1871        assert_eq!(std::fs::read(&path).unwrap(), b"-----BEGIN PGP PRIVATE KEY BLOCK----- again");
1872        #[cfg(unix)]
1873        {
1874            use std::os::unix::fs::PermissionsExt;
1875            assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
1876        }
1877        let _ = std::fs::remove_file(&path);
1878    }
1879
1880    /// A key made here keeps its revocation certificate beside it, yours
1881    /// alone; a copy can be saved; removed, the key takes it aside with it.
1882    #[test]
1883    fn a_key_made_here_keeps_its_revocation_certificate() {
1884        let root = temporary().join("pgp-made");
1885        let _ = std::fs::remove_dir_all(&root);
1886        let made = generate_at(&root, "Jane Exemple", "jane@example.org", "secret words").unwrap();
1887        assert!(made.own && made.revocation);
1888        let kept = root.join("own").join(format!("{}.rev", made.fingerprint));
1889        let text = std::fs::read_to_string(&kept).unwrap();
1890        #[cfg(unix)]
1891        {
1892            use std::os::unix::fs::PermissionsExt;
1893            assert_eq!(std::fs::metadata(&kept).unwrap().permissions().mode() & 0o777, 0o600, "protected as the key is");
1894        }
1895        assert!(text.contains(&made.fingerprint) && text.contains("Jane Exemple <jane@example.org>"), "{text}");
1896        // Never imported by mistake: a colon before the armor; read as no key.
1897        assert!(text.contains("\n:-----BEGIN PGP PUBLIC KEY BLOCK-----") && certificates_in(text.as_bytes()).is_empty());
1898        // The colon taken away, it revokes this very key.
1899        let armored = &text[text.find(":-----BEGIN").unwrap() + 1..];
1900        let pile = openpgp::PacketPile::from_bytes(armored.as_bytes()).unwrap();
1901        let revocation = pile.descendants().find_map(|p| if let openpgp::Packet::Signature(s) = p { Some(s.clone()) } else { None }).unwrap();
1902        let key = Cert::from_bytes(&std::fs::read(root.join("own").join(format!("{}.asc", made.fingerprint))).unwrap()).unwrap();
1903        assert!(matches!(key.revocation_status(&policy(), None), openpgp::types::RevocationStatus::NotAsFarAsWeKnow));
1904        let (revoked, _) = key.insert_packets(revocation).unwrap();
1905        assert!(matches!(revoked.revocation_status(&policy(), None), openpgp::types::RevocationStatus::Revoked(_)));
1906        // A copy saved elsewhere, as kept, yours alone; a second one beside it.
1907        let elsewhere = root.join("saved");
1908        let copy = save_revocation_at(&root, &made.fingerprint, &elsewhere).unwrap();
1909        assert_eq!((copy.file_name().unwrap().to_string_lossy().to_string(), std::fs::read_to_string(&copy).unwrap()), (format!("{}.rev", made.fingerprint), text));
1910        assert!(save_revocation_at(&root, &made.fingerprint, &elsewhere).unwrap().ends_with(format!("{}-2.rev", made.fingerprint)));
1911        assert!(save_revocation_at(&root, "0000", &elsewhere).is_err(), "none kept for a key not made here");
1912        let _ = std::fs::remove_dir_all(&root);
1913    }
1914
1915    #[test]
1916    fn web_key_directory_names() {
1917        // The example of draft-koch-openpgp-webkey-service §3.1.
1918        let [advanced, direct] = wkd_urls("Joe.Doe@Example.ORG").unwrap();
1919        assert_eq!(direct, "https://example.org/.well-known/openpgpkey/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe");
1920        assert!(advanced.starts_with("https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q"));
1921        // Only a domain's own directory: no address, no local name, no port or path.
1922        for elsewhere in ["joe@192.168.1.1", "joe@[::1]", "joe@localhost", "joe@host.example:8080/x?", "joe@1.2.3"] {
1923            assert!(wkd_urls(elsewhere).is_none(), "{elsewhere}");
1924        }
1925    }
1926
1927    /// A key as a security key holds it: the primary signs, a Curve25519 subkey decrypts.
1928    fn card_key(address: &str, made: Option<std::time::SystemTime>) -> Cert {
1929        let mut builder = CertBuilder::new()
1930            .add_userid(format!("Card Holder <{address}>"))
1931            .set_cipher_suite(CipherSuite::Cv25519)
1932            .set_primary_key_flags(openpgp::types::KeyFlags::empty().set_certification().set_signing())
1933            .add_transport_encryption_subkey();
1934        if let Some(made) = made {
1935            builder = builder.set_creation_time(made).set_validity_period(std::time::Duration::from_secs(365 * 24 * 3600));
1936        }
1937        builder.generate().unwrap().0
1938    }
1939
1940    fn record(card: &securitykey::SoftCard, cert: &Cert, address: &str) -> KnownCard {
1941        KnownCard {
1942            ident: card.info.ident.clone(),
1943            sign: card.info.sign.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1944            decrypt: card.info.decrypt.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1945            cert: cert.fingerprint().to_hex(),
1946            addresses: vec![address.to_string()],
1947            ..KnownCard::default()
1948        }
1949    }
1950
1951    #[test]
1952    fn a_security_key_signs_alone_for_its_addresses() {
1953        let on_card = card_key("me@example.org", None);
1954        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456");
1955        let here = key("me@example.org");
1956        let mut keys = Keys { own: vec![on_card.clone().strip_secret_key_material(), here.clone()], ..Keys::default() };
1957        assert_eq!(keys.own_for("me@example.org").unwrap().place, Place::Here);
1958        keys.cards.push(record(&card, &on_card, "me@example.org"));
1959        let own = keys.own_for("ME@example.org").unwrap();
1960        assert_eq!(own.place, Place::Card { ident: "0006:12345678".into() });
1961        assert_eq!(own.cert.fingerprint(), on_card.fingerprint());
1962        // Autocrypt announces the security key's certificate.
1963        assert!(autocrypt_header(own.cert, "me@example.org").is_some());
1964        assert!(keys.own_for("other@example.org").is_none());
1965        // Expired on the card: nothing signs for the address, and that is known.
1966        let late = card_key("late@example.org", Some(std::time::SystemTime::now() - std::time::Duration::from_secs(3 * 365 * 24 * 3600)));
1967        let late_card = securitykey::SoftCard::new("0006:00000002", &late, "123456");
1968        let keys = Keys { own: vec![late.clone().strip_secret_key_material(), key("late@example.org")], cards: vec![record(&late_card, &late, "late@example.org")], ..Keys::default() };
1969        assert!(keys.own_for("late@example.org").is_none());
1970        assert!(keys.card_for("late@example.org").is_some());
1971    }
1972
1973    /// The folders of a device's keys (`pgp/`), and its security keys made known to the others.
1974    fn read_here(root: &std::path::Path) {
1975        securitykey::share_cards_in(&root.join("cards.toml"), &root.join("own"), &root.join(securitykey::SHARED_CARDS), false).unwrap();
1976    }
1977
1978    /// What the sharing carries from one device's `pgp/cards/` to another's.
1979    fn carry(from: &std::path::Path, to: &std::path::Path) {
1980        let (from, to) = (from.join(securitykey::SHARED_CARDS), to.join(securitykey::SHARED_CARDS));
1981        let _ = std::fs::remove_dir_all(&to);
1982        std::fs::create_dir_all(&to).unwrap();
1983        for entry in std::fs::read_dir(&from).unwrap().filter_map(Result::ok) {
1984            std::fs::copy(entry.path(), to.join(entry.file_name())).unwrap();
1985        }
1986    }
1987
1988    /// Batch A (18 October 2026): your security key's public half travels
1989    /// (`pgp/cards/`), written by the computer that read the key, read by a
1990    /// phone that never did: it then encrypts to you, hands your key by
1991    /// Autocrypt and knows what waits for the key. Never a secret; a
1992    /// certificate replaced, or another key's, is refused; a certificate
1993    /// claiming your address from outside is never yours.
1994    #[test]
1995    fn your_security_keys_certificate_travels_and_only_it_is_yours() {
1996        let root = temporary().join("cards-travel");
1997        let _ = std::fs::remove_dir_all(&root);
1998        let (computer, phone) = (root.join("computer"), root.join("phone"));
1999        let on_card = card_key("me@example.org", None);
2000        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456");
2001        store_in(&computer, on_card.clone().strip_secret_key_material(), true).unwrap();
2002        securitykey::change_known_at(&computer.join("cards.toml"), |cards| cards.push(record(&card, &on_card, "me@example.org"))).unwrap();
2003        read_here(&computer);
2004        let shared = std::fs::read_to_string(computer.join("cards").join("0006-12345678.toml")).unwrap();
2005        assert!(shared.contains("BEGIN PGP PUBLIC KEY BLOCK") && !shared.contains("PRIVATE KEY"), "public parts only");
2006        // The phone: nothing of its own, the card's file carried by the sharing.
2007        carry(&computer, &phone);
2008        let keys = Keys::load_at(&phone);
2009        assert_eq!((keys.cards.len(), keys.shared.len()), (0, 1), "known from the computer, not read here");
2010        assert!(keys.own_for("me@example.org").is_none(), "signing only where it was read (F18)");
2011        assert_eq!(keys.serving_card("me@example.org").map(|(card, _)| card.ident.as_str()), Some("0006:12345678"));
2012        let yours = keys.self_cert_for("me@example.org").expect("yours to encrypt to");
2013        assert_eq!(yours.fingerprint(), on_card.fingerprint());
2014        assert!(autocrypt_header(yours, "me@example.org").is_some(), "Autocrypt from the phone too");
2015        // What waits for the key is known there.
2016        let none = |_: &Fingerprint| None;
2017        let sessions = SessionKeys::new();
2018        let unlock = Unlock::new(&none, &sessions);
2019        let sealed = encrypt(b"Hello me.", &[yours], None).unwrap();
2020        assert!(decrypt(sealed.as_bytes(), &keys, &unlock).is_err());
2021        assert_eq!(unlock.wanted().as_deref(), Some("0006:12345678"));
2022        // And your key opens it, wherever it can be used.
2023        let readers = securitykey::SoftReaders::with(vec![securitykey::SoftCard::new("0006:12345678", &on_card, "123456").with_x25519(securitykey::x25519::x25519)]);
2024        let pin = Password::from("123456");
2025        let access = CardAccess::new(&readers, &pin, &|| {});
2026        assert_eq!(decrypt(sealed.as_bytes(), &keys, &Unlock::new(&none, &sessions).with_card(&access)).unwrap().data, b"Hello me.");
2027        // Your own mail read on the phone carries your security key's
2028        // certificate in its Autocrypt header, which the phone knows from the
2029        // sharing alone: never kept among others' keys there, nor from a lookup.
2030        assert!(learn_autocrypt_at(&phone, &autocrypt_header(yours, "me@example.org").unwrap(), "me@example.org").is_none());
2031        assert!(learn_at(&phone, &on_card.clone().strip_secret_key_material().armored().to_vec().unwrap(), "me@example.org").is_empty());
2032        assert!(!phone.join("others").join(format!("{}.asc", on_card.fingerprint().to_hex())).exists());
2033        // A certificate claiming your address from outside (an Autocrypt header) is never yours.
2034        let stranger = key("me@example.org");
2035        store_in(&phone, stranger.clone().strip_secret_key_material(), false).unwrap();
2036        let keys = Keys::load_at(&phone);
2037        assert_eq!(keys.for_address("me@example.org").map(Cert::fingerprint), Some(on_card.fingerprint()));
2038        assert!(keys.own.iter().all(|c| c.fingerprint() != stranger.fingerprint()));
2039        // A shared file whose certificate was replaced, or is another key's: refused.
2040        let mut replaced: securitykey::SharedCard = toml::from_str(&shared).unwrap();
2041        replaced.certificate = String::from_utf8(stranger.clone().strip_secret_key_material().armored().to_vec().unwrap()).unwrap();
2042        std::fs::write(phone.join("cards").join("0006-12345678.toml"), toml::to_string(&replaced).unwrap()).unwrap();
2043        let keys = Keys::load_at(&phone);
2044        assert!(keys.shared.is_empty() && keys.serving_card("me@example.org").is_none() && keys.own.iter().all(|c| c.fingerprint() != stranger.fingerprint()), "replaced: refused");
2045        let _ = std::fs::remove_dir_all(&root);
2046    }
2047
2048    /// A computer whose key changed (renewed, as on 8 October 2026) updates
2049    /// what the others know; a computer still holding the older certificate
2050    /// never undoes it; an expired certificate of yours takes no encryption.
2051    #[test]
2052    fn a_renewal_reaches_every_device_and_is_never_undone() {
2053        let root = temporary().join("cards-renewal");
2054        let _ = std::fs::remove_dir_all(&root);
2055        let (one, two, phone) = (root.join("one"), root.join("two"), root.join("phone"));
2056        let made = std::time::SystemTime::now() - std::time::Duration::from_secs(30 * 24 * 3600);
2057        let on_card = card_key("me@example.org", Some(made));
2058        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456");
2059        for computer in [&one, &two] {
2060            store_in(computer, on_card.clone().strip_secret_key_material(), true).unwrap();
2061            securitykey::change_known_at(&computer.join("cards.toml"), |cards| cards.push(record(&card, &on_card, "me@example.org"))).unwrap();
2062            read_here(computer);
2063        }
2064        let expires = |keys: &Keys| keys.serving_card("me@example.org").and_then(|(_, cert)| cert.with_policy(&policy(), None).ok()?.primary_key().key_expiration_time());
2065        carry(&one, &phone);
2066        let before = expires(&Keys::load_at(&phone)).unwrap();
2067        // Renewed on the first computer: kept with its keys, the shared file follows.
2068        let mut signer = on_card.primary_key().key().clone().parts_into_secret().unwrap().into_keypair().unwrap();
2069        let later = std::time::SystemTime::now() + std::time::Duration::from_secs(2 * 365 * 24 * 3600);
2070        let signatures = on_card.set_expiration_time(&policy(), None, &mut signer, Some(later)).unwrap();
2071        let renewed = on_card.clone().insert_packets(signatures).unwrap().0;
2072        store_in(&one, renewed.strip_secret_key_material(), true).unwrap();
2073        carry(&one, &phone);
2074        let after = expires(&Keys::load_at(&phone)).unwrap();
2075        assert!(after > before, "renewed on the phone too");
2076        // The second computer, still on the older certificate, writes after: the renewal stays.
2077        carry(&one, &two);
2078        securitykey::change_known_at(&two.join("cards.toml"), |cards| cards[0].holder = "Card Holder".into()).unwrap();
2079        read_here(&two);
2080        carry(&two, &phone);
2081        assert_eq!(expires(&Keys::load_at(&phone)), Some(after), "never undone");
2082        // An expired certificate of yours: no encryption to it, said by the window (`self_cert_for` none).
2083        let old = card_key("old@example.org", Some(std::time::SystemTime::now() - std::time::Duration::from_secs(400 * 24 * 3600)));
2084        let old_card = securitykey::SoftCard::new("0006:00000001", &old, "123456");
2085        let keys = Keys { own: vec![old.clone().strip_secret_key_material()], cards: vec![record(&old_card, &old, "old@example.org")], ..Keys::default() };
2086        assert!(keys.is_own_address("old@example.org") && keys.self_cert_for("old@example.org").is_none() && keys.for_address("old@example.org").is_none());
2087        let _ = std::fs::remove_dir_all(&root);
2088    }
2089
2090    /// This device's marks, as `state/security-keys.toml` keeps them.
2091    fn marks(root: &std::path::Path) -> std::path::PathBuf {
2092        root.join("marks.toml")
2093    }
2094
2095    /// An exchange's pass over the security keys read on `root`.
2096    fn exchange_pass(root: &std::path::Path) {
2097        securitykey::share_cards_marked_in(&root.join("cards.toml"), &root.join("own"), &root.join(securitykey::SHARED_CARDS), &marks(root)).unwrap();
2098    }
2099
2100    /// Review of 19 October 2026, P4 (F4): two computers read the key; the
2101    /// first stops using it. The second computer's next exchange no longer
2102    /// makes its file again ("once if missing" ran at every exchange), and
2103    /// when the second computer shares it again later, the first never takes
2104    /// it back: it signed with the key again, and said it was read on another
2105    /// device. The second computer still signs with it; the phone encrypts to it.
2106    #[test]
2107    fn a_card_stopped_on_one_computer_never_comes_back_to_it() {
2108        let root = temporary().join("cards-stopped");
2109        let _ = std::fs::remove_dir_all(&root);
2110        let (one, two, phone) = (root.join("one"), root.join("two"), root.join("phone"));
2111        let on_card = card_key("me@example.org", None);
2112        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456");
2113        for computer in [&one, &two] {
2114            store_in(computer, on_card.clone().strip_secret_key_material(), true).unwrap();
2115            securitykey::change_known_at(&computer.join("cards.toml"), |cards| cards.push(record(&card, &on_card, "me@example.org"))).unwrap();
2116            exchange_pass(computer);
2117        }
2118        carry(&one, &phone);
2119        let file = |root: &std::path::Path| root.join(securitykey::SHARED_CARDS).join("0006-12345678.toml");
2120        assert!(file(&two).exists() && file(&phone).exists());
2121        // The first computer stops using it: listed as stopped there, its file taken out, everywhere.
2122        assert!(securitykey::forget_in(&one, "0006:12345678").unwrap());
2123        assert!(Keys::load_at(&one).own_for("me@example.org").is_none());
2124        carry(&one, &two);
2125        carry(&one, &phone);
2126        // The second computer's next exchange makes nothing again.
2127        exchange_pass(&two);
2128        assert!(!file(&two).exists(), "never made again by the exchange's pass");
2129        // Later, what the second computer knows of the key changes: written again, it travels;
2130        // the first computer never takes it back.
2131        securitykey::change_known_at(&two.join("cards.toml"), |cards| cards[0].holder = "Ferrand<<Noa".into()).unwrap();
2132        read_here(&two);
2133        carry(&two, &one);
2134        let first = Keys::load_at(&one);
2135        assert!(first.own_for("me@example.org").is_none() && first.shared.is_empty() && first.serving_card("me@example.org").is_none(), "stopped there");
2136        // The second computer signs with it; the phone encrypts to it and signs nothing.
2137        assert_eq!(Keys::load_at(&two).own_for("me@example.org").map(|o| o.place), Some(Place::Card { ident: "0006:12345678".into() }));
2138        carry(&two, &phone);
2139        let on_phone = Keys::load_at(&phone);
2140        assert!(on_phone.self_cert_for("me@example.org").is_some() && on_phone.own_for("me@example.org").is_none());
2141        let _ = std::fs::remove_dir_all(&root);
2142    }
2143
2144    /// Review of 19 October 2026, F18: a computer that keeps a key of its own
2145    /// for an address goes on signing with it when another computer shares a
2146    /// security key serving that address; the shared key serves encryption to
2147    /// you and Autocrypt there.
2148    #[test]
2149    fn a_card_read_on_another_computer_signs_only_there() {
2150        let root = temporary().join("cards-f18");
2151        let _ = std::fs::remove_dir_all(&root);
2152        let (one, three) = (root.join("one"), root.join("three"));
2153        let on_card = card_key("me@example.org", None);
2154        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456");
2155        store_in(&one, on_card.clone().strip_secret_key_material(), true).unwrap();
2156        securitykey::change_known_at(&one.join("cards.toml"), |cards| cards.push(record(&card, &on_card, "me@example.org"))).unwrap();
2157        exchange_pass(&one);
2158        let here = key("me@example.org");
2159        store_at(&three, here.clone()).unwrap();
2160        carry(&one, &three);
2161        let keys = Keys::load_at(&three);
2162        let signer = keys.own_for("me@example.org").expect("the key kept here signs");
2163        assert_eq!((signer.place, signer.cert.fingerprint()), (Place::Here, here.fingerprint()));
2164        assert_eq!(keys.self_cert_for("me@example.org").map(Cert::fingerprint), Some(on_card.fingerprint()));
2165        assert!(autocrypt_header(keys.self_cert_for("me@example.org").unwrap(), "me@example.org").is_some());
2166        let _ = std::fs::remove_dir_all(&root);
2167    }
2168
2169    /// Review of 19 October 2026, P3 (F3): a shared certificate whose
2170    /// decryption key has no binding, or is revoked, is never the security
2171    /// key's, as Sequoia's policy reads it; one in order is.
2172    #[test]
2173    fn a_shared_certificate_counts_only_with_the_cards_keys_bound() {
2174        let on_card = card_key("me@example.org", None);
2175        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456");
2176        let known = record(&card, &on_card, "me@example.org");
2177        let armored = |c: &Cert| String::from_utf8(c.clone().strip_secret_key_material().armored().to_vec().unwrap()).unwrap();
2178        assert!(securitykey::vouched(&known, &armored(&on_card)).is_some());
2179        let unbound = Cert::from_packets(on_card.clone().into_packets().filter(|p| !matches!(p, openpgp::Packet::Signature(sig) if sig.typ() == openpgp::types::SignatureType::SubkeyBinding))).unwrap();
2180        assert!(on_card.keys().subkeys().count() == 1 && unbound.keys().subkeys().count() == 1);
2181        assert!(securitykey::vouched(&known, &armored(&unbound)).is_none(), "no binding");
2182        let mut signer = on_card.primary_key().key().clone().parts_into_secret().unwrap().into_keypair().unwrap();
2183        let subkey = on_card.keys().subkeys().next().unwrap().key().clone();
2184        let revocation = SubkeyRevocationBuilder::new().set_reason_for_revocation(openpgp::types::ReasonForRevocation::KeyCompromised, b"lost").unwrap().build(&mut signer, &on_card, &subkey, None).unwrap();
2185        let revoked = on_card.clone().insert_packets(vec![openpgp::Packet::from(revocation)]).unwrap().0;
2186        assert!(securitykey::vouched(&known, &armored(&revoked)).is_none(), "revoked");
2187        assert_eq!(securitykey::revoked_encryption(&revoked), Some(subkey.fingerprint().to_hex()));
2188        assert_eq!(securitykey::revoked_encryption(&on_card), None);
2189    }
2190
2191    /// Review of 19 October 2026, P5 (F5): renewed on the first computer, the
2192    /// second (not reached yet) writes its older certificate after: the phone
2193    /// had the expired one until the first computer's record changed. Now
2194    /// the first computer's next exchange writes the renewal again.
2195    #[test]
2196    fn a_renewal_lost_to_an_older_write_comes_back_at_the_next_exchange() {
2197        let root = temporary().join("cards-renewal-lost");
2198        let _ = std::fs::remove_dir_all(&root);
2199        let (one, two, phone) = (root.join("one"), root.join("two"), root.join("phone"));
2200        let made = std::time::SystemTime::now() - std::time::Duration::from_secs(400 * 24 * 3600);
2201        let on_card = card_key("me@example.org", Some(made));
2202        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456");
2203        for computer in [&one, &two] {
2204            store_in(computer, on_card.clone().strip_secret_key_material(), true).unwrap();
2205            securitykey::change_known_at(&computer.join("cards.toml"), |cards| cards.push(record(&card, &on_card, "me@example.org"))).unwrap();
2206            exchange_pass(computer);
2207        }
2208        let mut signer = on_card.primary_key().key().clone().parts_into_secret().unwrap().into_keypair().unwrap();
2209        let later = std::time::SystemTime::now() + std::time::Duration::from_secs(2 * 365 * 24 * 3600);
2210        let mut packets = on_card.set_expiration_time(&policy(), None, &mut signer, Some(later)).unwrap();
2211        let subkey = on_card.keys().subkeys().next().unwrap().key().clone().parts_into_secret().unwrap();
2212        let primary = on_card.primary_key().key().clone();
2213        let binding = openpgp::packet::signature::SignatureBuilder::new(openpgp::types::SignatureType::SubkeyBinding)
2214            .set_key_flags(openpgp::types::KeyFlags::empty().set_transport_encryption().set_storage_encryption())
2215            .unwrap()
2216            .set_key_validity_period(later.duration_since(subkey.creation_time()).unwrap())
2217            .unwrap()
2218            .sign_subkey_binding(&mut signer, &primary, &subkey)
2219            .unwrap();
2220        packets.push(binding.into());
2221        let renewed = on_card.clone().insert_packets(packets).unwrap().0;
2222        store_in(&one, renewed.strip_secret_key_material(), true).unwrap();
2223        assert!(Keys::load_at(&one).self_cert_for("me@example.org").is_some(), "renewed on the first");
2224        // The second, not reached yet, writes its older certificate: it travels after the renewal.
2225        securitykey::change_known_at(&two.join("cards.toml"), |cards| cards[0].holder = "Ferrand<<Noa".into()).unwrap();
2226        read_here(&two);
2227        carry(&two, &one);
2228        carry(&two, &phone);
2229        assert!(Keys::load_at(&phone).self_cert_for("me@example.org").is_none(), "the race: expired on the phone");
2230        // The first computer's next exchange writes it again: the phone has the renewal.
2231        exchange_pass(&one);
2232        carry(&one, &phone);
2233        assert!(Keys::load_at(&phone).self_cert_for("me@example.org").is_some(), "renewed on the phone");
2234        // And it settles: nothing more written.
2235        let written = std::fs::read(one.join(securitykey::SHARED_CARDS).join("0006-12345678.toml")).unwrap();
2236        exchange_pass(&one);
2237        assert_eq!(std::fs::read(one.join(securitykey::SHARED_CARDS).join("0006-12345678.toml")).unwrap(), written);
2238        let _ = std::fs::remove_dir_all(&root);
2239    }
2240
2241    #[test]
2242    fn a_message_to_the_security_key_waits_for_it_then_opens_from_memory() {
2243        let on_card = card_key("me@example.org", None);
2244        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456").with_x25519(securitykey::x25519::x25519);
2245        let keys = Keys { own: vec![on_card.clone().strip_secret_key_material()], cards: vec![record(&card, &on_card, "me@example.org")], ..Keys::default() };
2246        let readers = securitykey::SoftReaders::with(vec![card]);
2247        let sealed = encrypt(b"Hello me.", &[&on_card], None).unwrap();
2248        let none = |_: &Fingerprint| None;
2249        let sessions = SessionKeys::new();
2250        // Shown: nothing is asked of the key, the message says which key it waits for.
2251        let unlock = Unlock::new(&none, &sessions);
2252        assert!(decrypt(sealed.as_bytes(), &keys, &unlock).is_err());
2253        assert_eq!(unlock.wanted().as_deref(), Some("0006:12345678"));
2254        assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(0));
2255        // A wrong PIN: said, nothing opened.
2256        let wrong = Password::from("654321");
2257        let access = CardAccess::new(&readers, &wrong, &|| {});
2258        let unlock = Unlock::new(&none, &sessions).with_card(&access);
2259        assert!(decrypt(sealed.as_bytes(), &keys, &unlock).is_err());
2260        assert_eq!(access.failure(), Some(securitykey::CardError::WrongPin { left: 2 }));
2261        // "Open with your security key": opened, its session key kept.
2262        let pin = Password::from("123456");
2263        let access = CardAccess::new(&readers, &pin, &|| {});
2264        let opened = decrypt(sealed.as_bytes(), &keys, &Unlock::new(&none, &sessions).with_card(&access)).unwrap();
2265        assert_eq!(opened.data, b"Hello me.");
2266        assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(1));
2267        // Again, its attachments, its answer: from memory, the key left alone.
2268        let again = decrypt(sealed.as_bytes(), &keys, &Unlock::new(&none, &sessions)).unwrap();
2269        assert_eq!(again.data, b"Hello me.");
2270        assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(1));
2271        // Another message still waits for the key; forgotten, this one too.
2272        let other = encrypt(b"Another.", &[&on_card], None).unwrap();
2273        assert!(decrypt(other.as_bytes(), &keys, &Unlock::new(&none, &sessions)).is_err());
2274        sessions.forget();
2275        assert!(decrypt(sealed.as_bytes(), &keys, &Unlock::new(&none, &sessions)).is_err());
2276    }
2277
2278    #[test]
2279    fn an_expired_security_key_still_opens_older_mail() {
2280        let then = std::time::SystemTime::now() - std::time::Duration::from_secs(3 * 365 * 24 * 3600);
2281        let late = card_key("late@example.org", Some(then));
2282        // Sent while the key was valid.
2283        let policy = policy();
2284        let sent = then + std::time::Duration::from_secs(24 * 3600);
2285        let recipients: Vec<_> = late.keys().with_policy(&policy, sent).alive().for_transport_encryption().collect();
2286        assert!(!recipients.is_empty());
2287        let mut sealed = Vec::new();
2288        {
2289            let message = Message::new(&mut sealed);
2290            let message = Encryptor::for_recipients(message, recipients).build().unwrap();
2291            let mut literal = LiteralWriter::new(message).build().unwrap();
2292            literal.write_all(b"Older mail.").unwrap();
2293            literal.finalize().unwrap();
2294        }
2295        let card = securitykey::SoftCard::new("0006:00000002", &late, "123456").with_x25519(securitykey::x25519::x25519);
2296        let keys = Keys { own: vec![late.clone().strip_secret_key_material()], cards: vec![record(&card, &late, "late@example.org")], ..Keys::default() };
2297        let readers = securitykey::SoftReaders::with(vec![card]);
2298        let pin = Password::from("123456");
2299        let access = CardAccess::new(&readers, &pin, &|| {});
2300        let none = |_: &Fingerprint| None;
2301        let sessions = SessionKeys::new();
2302        let opened = decrypt(&sealed, &keys, &Unlock::new(&none, &sessions).with_card(&access)).unwrap();
2303        assert_eq!(opened.data, b"Older mail.");
2304        // But nobody can encrypt to it now.
2305        assert!(encrypt(b"New mail.", &[&late], None).is_err());
2306    }
2307
2308    #[test]
2309    fn a_security_key_certificate_comes_back_to_your_keys() {
2310        let root = temporary().join("pgp-card");
2311        let _ = std::fs::remove_dir_all(&root);
2312        let on_card = card_key("me@example.org", None);
2313        let name = format!("{}.asc", on_card.fingerprint().to_hex());
2314        let kept = store_in(&root, on_card.clone().strip_secret_key_material(), true).unwrap();
2315        assert!(kept.own);
2316        let file = std::fs::read_to_string(root.join("own").join(&name)).unwrap();
2317        assert!(file.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----"), "public only: {file}");
2318        // Brought again by an Autocrypt header: refused, never with others'
2319        // (whoever sends your own key back cannot add to it: F1); by a
2320        // lookup: merged with yours, nothing it names added.
2321        let header = autocrypt_header(&on_card, "me@example.org").unwrap();
2322        assert!(learn_autocrypt_at(&root, &header, "me@example.org").is_none());
2323        assert!(learn_at(&root, &on_card.armored().to_vec().unwrap(), "me@example.org").iter().all(|k| k.own));
2324        assert!(!root.join("others").join(&name).exists());
2325        // Someone else's goes with others'.
2326        let jane = key("jane@example.org").strip_secret_key_material();
2327        assert!(!store_at(&root, jane).unwrap().own);
2328        let _ = std::fs::remove_dir_all(&root);
2329    }
2330
2331    /// Review of 19 October 2026, P8 (F8): a message whose sender used
2332    /// protected headers ("..." outside, the real subject inside), opened as
2333    /// Sioul opened such a message before, then sealed again: its real subject
2334    /// stays inside, and opens again with the rest.
2335    #[test]
2336    fn a_sealed_copy_keeps_the_protected_headers_inside() {
2337        let me = key("me@example.org");
2338        let inner = "Content-Type: multipart/mixed; boundary=\"b\"; protected-headers=\"v1\"\r\nSubject: Lease renewal for flat 4B\r\nFrom: Jane <jane@example.org>\r\nTo: Me <me@example.org>\r\nMessage-ID: <ph@example.org>\r\n\r\n--b\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThe renewal.\r\n--b--\r\n";
2339        let outer = "From: Jane <jane@example.org>\r\nTo: Me <me@example.org>\r\nSubject: ...\r\nMessage-ID: <ph@example.org>\r\n";
2340        let received = format!(
2341            "{outer}MIME-Version: 1.0\r\nContent-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"o\"\r\n\r\n--o\r\nContent-Type: application/pgp-encrypted\r\n\r\nVersion: 1\r\n\r\n--o\r\nContent-Type: application/octet-stream\r\n\r\n{}\r\n--o--\r\n",
2342            encrypt(inner.as_bytes(), &[&me], None).unwrap()
2343        );
2344        let keys = Keys { own: vec![me.clone()], ..Keys::default() };
2345        let sessions = SessionKeys::new();
2346        let passphrase = |_: &Fingerprint| Some("secret words".to_string());
2347        let copy = open(received.as_bytes(), &keys, &Unlock::new(&passphrase, &sessions)).unwrap().0.unwrap();
2348        assert!(looks_opened(&copy), "as Sioul wrote it before");
2349        let sealed = seal_message(&copy, &[&me]).unwrap();
2350        let head = String::from_utf8_lossy(&sealed[..header_end(&sealed)]).to_string();
2351        assert!(!head.contains("Lease renewal") && head.contains("Subject: ..."), "{head}");
2352        let again = open(&sealed, &keys, &Unlock::new(&passphrase, &sessions)).unwrap().0.unwrap();
2353        assert!(String::from_utf8_lossy(&again).contains("Subject: Lease renewal for flat 4B"));
2354        // An expired encryption key of yours, not revoked, still takes a seal for you alone (F19).
2355        let late = CertBuilder::general_purpose(Some("Me <late@example.org>")).set_cipher_suite(CipherSuite::Cv25519).set_creation_time(std::time::SystemTime::now() - std::time::Duration::from_secs(400 * 24 * 3600)).set_validity_period(std::time::Duration::from_secs(365 * 24 * 3600)).generate().unwrap().0;
2356        assert!(encrypt(b"x", &[&late], None).is_err(), "nobody encrypts to it");
2357        assert!(seal_message(&copy, &[&late]).is_ok());
2358        assert_eq!(Keys { own: vec![late], ..Keys::default() }.own_certs().len(), 1);
2359    }
2360
2361    /// A header carrying every packet of `cert`, as anyone may write one.
2362    fn header_of(cert: &Cert, addr: &str) -> String {
2363        let mut bytes = Vec::new();
2364        cert.serialize(&mut bytes).unwrap();
2365        format!("addr={addr}; keydata={}", crate::lines::base64_encode(&bytes))
2366    }
2367
2368    /// Review of 19 October 2026, P1 (F1): a sender whose domain vouches for
2369    /// him sends, in his Autocrypt header, your own primary key with two user
2370    /// IDs bound by nothing, his and Jane's. It was kept as yours: Jane's
2371    /// address became yours, and a message to her went to you alone, on every
2372    /// device through your security key's shared file. Now refused: your
2373    /// certificate gains nothing, your addresses are your accounts', Jane's
2374    /// key stays hers.
2375    #[test]
2376    fn an_autocrypt_header_never_makes_another_address_yours() {
2377        let root = temporary().join("pgp-p1");
2378        let _ = std::fs::remove_dir_all(&root);
2379        let me = card_key("me@example.org", None);
2380        store_in(&root, me.clone().strip_secret_key_material(), true).unwrap();
2381        let jane = key("jane@example.org");
2382        assert!(learn_autocrypt_at(&root, &autocrypt_header(&jane, "jane@example.org").unwrap(), "jane@example.org").is_some());
2383        let load = || Keys { addresses: vec!["me@example.org".into()], ..Keys::load_at(&root) };
2384        assert_eq!(load().for_address("jane@example.org").map(Cert::fingerprint), Some(jane.fingerprint()));
2385        let forged = Cert::from_packets(vec![openpgp::Packet::from(me.primary_key().key().clone()), openpgp::packet::UserID::from("Mallory <mallory@example.net>").into(), openpgp::packet::UserID::from("Jane <jane@example.org>").into()].into_iter()).unwrap();
2386        assert!(!names(&forged, "jane@example.org") && !names(&forged, "mallory@example.net"), "unbound user IDs name nothing");
2387        assert!(learn_autocrypt_at(&root, &header_of(&forged, "mallory@example.net"), "mallory@example.net").is_none());
2388        let keys = load();
2389        assert!(!keys.is_own_address("jane@example.org"));
2390        assert!(keys.is_own_address("me@example.org"));
2391        assert_eq!(keys.for_address("jane@example.org").map(Cert::fingerprint), Some(jane.fingerprint()));
2392        assert_eq!(keys.own.iter().find(|c| c.fingerprint() == me.fingerprint()).unwrap().userids().count(), 1, "nothing added to yours");
2393        // A message to Jane: her key opens it.
2394        let sealed = encrypt(b"Hello Jane.", &[keys.for_address("jane@example.org").unwrap()], None).unwrap();
2395        let sessions = SessionKeys::new();
2396        let passphrase = |_: &Fingerprint| Some("secret words".to_string());
2397        assert!(decrypt(sealed.as_bytes(), &Keys { own: vec![jane.clone()], ..Keys::default() }, &Unlock::new(&passphrase, &sessions)).is_ok());
2398        let _ = std::fs::remove_dir_all(&root);
2399    }
2400
2401    /// Review of 19 October 2026, P2 (F2): a verified sender's own key with a
2402    /// user ID of Jane's he signed himself, in his own header: kept for his
2403    /// address alone, never Jane's key, whatever order the folder lists keys
2404    /// in. Two keys of Jane's: the newer. A lookup keeps the user ID asked for
2405    /// alone; one of yours that it brings gains no user ID.
2406    #[test]
2407    fn a_senders_key_naming_janes_address_never_becomes_janes() {
2408        let root = temporary().join("pgp-p2");
2409        let _ = std::fs::remove_dir_all(&root);
2410        let mallory = CertBuilder::new().add_userid("Mallory <mallory@example.net>").add_userid("Jane <jane@example.org>").set_cipher_suite(CipherSuite::Cv25519).add_transport_encryption_subkey().generate().unwrap().0;
2411        assert!(names(&mallory, "jane@example.org"), "bound by Mallory himself");
2412        let kept = learn_autocrypt_at(&root, &header_of(&mallory, "mallory@example.net"), "mallory@example.net").unwrap();
2413        assert_eq!(kept.names, vec!["Mallory <mallory@example.net>".to_string()]);
2414        let keys = Keys::load_at(&root);
2415        assert!(keys.for_address("jane@example.org").is_none(), "Jane's key unknown: none, never Mallory's");
2416        assert_eq!(keys.for_address("mallory@example.net").map(Cert::fingerprint), Some(mallory.fingerprint()));
2417        // Jane's key, an older one and her new one: the new one, whichever file comes first.
2418        let year = std::time::Duration::from_secs(365 * 24 * 3600);
2419        let old = CertBuilder::general_purpose(Some("Jane <jane@example.org>")).set_cipher_suite(CipherSuite::Cv25519).set_creation_time(std::time::SystemTime::now() - year).generate().unwrap().0;
2420        let new = key("jane@example.org");
2421        for jane in [&new, &old] {
2422            assert!(learn_autocrypt_at(&root, &autocrypt_header(jane, "jane@example.org").unwrap(), "jane@example.org").is_some());
2423        }
2424        assert_eq!(Keys::load_at(&root).for_address("jane@example.org").map(Cert::fingerprint), Some(new.fingerprint()));
2425        // A lookup for Bob keeps Bob's user ID alone.
2426        let bob = CertBuilder::new().add_userid("Bob <bob@example.org>").add_userid("Robert <robert@example.net>").set_cipher_suite(CipherSuite::Cv25519).add_transport_encryption_subkey().generate().unwrap().0;
2427        let found = learn_at(&root, &bob.armored().to_vec().unwrap(), "bob@example.org");
2428        assert_eq!(found.iter().map(|k| k.names.clone()).collect::<Vec<_>>(), vec![vec!["Bob <bob@example.org>".to_string()]]);
2429        assert!(Keys::load_at(&root).for_address("robert@example.net").is_none());
2430        // One of yours, a user ID of yours added elsewhere: a lookup for that
2431        // address merges what your primary binds, adds no user ID.
2432        let me = key("me@example.org");
2433        store_at(&root, me.clone()).unwrap();
2434        let mut signer = me.primary_key().key().clone().parts_into_secret().unwrap().decrypt_secret(&Password::from("secret words")).unwrap().into_keypair().unwrap();
2435        let other = openpgp::packet::UserID::from("Me <other@example.net>");
2436        let binding = other.bind(&mut signer, &me, openpgp::packet::signature::SignatureBuilder::new(openpgp::types::SignatureType::PositiveCertification)).unwrap();
2437        let more = me.clone().insert_packets(vec![openpgp::Packet::from(other), binding.into()]).unwrap().0.strip_secret_key_material();
2438        assert!(learn_at(&root, &more.armored().to_vec().unwrap(), "other@example.net").iter().all(|k| k.own));
2439        let mine = Keys::load_at(&root).own.into_iter().find(|c| c.fingerprint() == me.fingerprint()).unwrap();
2440        assert_eq!(mine.userids().map(|u| String::from_utf8_lossy(u.userid().value()).to_string()).collect::<Vec<_>>(), vec!["Someone <me@example.org>".to_string()]);
2441        let _ = std::fs::remove_dir_all(&root);
2442    }
2443
2444    /// A key whose secret part is unencrypted, made `ago` before now, for
2445    /// the tests that sign with it (a binding, a revocation).
2446    fn open_key(userids: &[&str], ago: std::time::Duration) -> Cert {
2447        let mut builder = CertBuilder::new().set_cipher_suite(CipherSuite::Cv25519).add_transport_encryption_subkey().set_creation_time(std::time::SystemTime::now() - ago);
2448        for userid in userids {
2449            builder = builder.add_userid(*userid);
2450        }
2451        builder.generate().unwrap().0
2452    }
2453
2454    fn signer_of(cert: &Cert) -> crypto::KeyPair {
2455        cert.primary_key().key().clone().parts_into_secret().unwrap().into_keypair().unwrap()
2456    }
2457
2458    fn public(cert: &Cert) -> Vec<u8> {
2459        cert.clone().strip_secret_key_material().armored().to_vec().unwrap()
2460    }
2461
2462    fn revoked(cert: &Cert) -> Cert {
2463        let revocation = CertRevocationBuilder::new().set_reason_for_revocation(openpgp::types::ReasonForRevocation::KeyCompromised, b"stolen").unwrap().build(&mut signer_of(cert), cert, None).unwrap();
2464        cert.clone().insert_packets(vec![openpgp::Packet::from(revocation)]).unwrap().0
2465    }
2466
2467    /// Review of 19 October 2026, N2 (probe `probe2_f2_replay_and_residue`):
2468    /// Mallory's key with his and Jane's self-bound user IDs, as Sioul kept
2469    /// it before or as an older Sioul's sharing brings it, beside Jane's
2470    /// real key, a year old. Not known for which address it came, it serves
2471    /// none: never Jane's, though its key is newer. His next header heals
2472    /// his file to his own address, Accounts names him alone, and Jane's
2473    /// address never comes back to it.
2474    #[test]
2475    fn a_key_kept_before_serves_no_address_it_was_not_learned_for() {
2476        let root = temporary().join("pgp-n2");
2477        let _ = std::fs::remove_dir_all(&root);
2478        let jane = open_key(&["Jane <jane@example.org>"], std::time::Duration::from_secs(365 * 24 * 3600));
2479        learn_autocrypt_at(&root, &autocrypt_header(&jane, "jane@example.org").unwrap(), "jane@example.org").unwrap();
2480        let mallory = open_key(&["Mallory <mallory@example.net>", "Jane <jane@example.org>"], std::time::Duration::ZERO);
2481        let legacy = root.join("others").join(format!("{}.asc", mallory.fingerprint().to_hex()));
2482        std::fs::write(&legacy, public(&mallory)).unwrap();
2483        let keys = Keys::load_at(&root);
2484        assert_eq!(keys.for_address("jane@example.org").map(Cert::fingerprint), Some(jane.fingerprint()), "Jane's own key, never the newer one naming her");
2485        assert!(keys.for_address("mallory@example.net").is_none(), "which address it came for is not known: none until it comes again");
2486        assert!(keys.others.iter().all(|c| c.fingerprint() == jane.fingerprint() || !bound(c, "jane@example.org")), "no other key names her");
2487        let kept = learn_autocrypt_at(&root, &header_of(&mallory.clone().strip_secret_key_material(), "mallory@example.net"), "mallory@example.net").unwrap();
2488        assert_eq!(kept.names, vec!["Mallory <mallory@example.net>".to_string()]);
2489        assert_eq!(Cert::from_bytes(&std::fs::read(&legacy).unwrap()).unwrap().userids().count(), 1, "Jane's user ID gone from his file");
2490        let keys = Keys::load_at(&root);
2491        assert_eq!(keys.for_address("mallory@example.net").map(Cert::fingerprint), Some(mallory.fingerprint()));
2492        assert_eq!(keys.for_address("jane@example.org").map(Cert::fingerprint), Some(jane.fingerprint()));
2493        // Jane's key gone: her address has none, never his.
2494        remove_in(&root, &jane.fingerprint().to_hex()).unwrap();
2495        assert!(!root.join("others").join(format!("{}.toml", jane.fingerprint().to_hex())).exists(), "what it served goes with it");
2496        assert!(Keys::load_at(&root).for_address("jane@example.org").is_none());
2497        let _ = std::fs::remove_dir_all(&root);
2498    }
2499
2500    /// Review of 19 October 2026, R1 (probe
2501    /// `review_probe_n2_legacy_key_bound_to_one_address_it_did_not_come_for`):
2502    /// Mallory's key binding Jane's address and carrying his own in a user
2503    /// ID bound by nothing, as a Sioul before 1f9acc0 kept it from his own
2504    /// header (any user ID passed then). Its one bound address is not the
2505    /// one it came for: judged by every user ID it carries, it names two and
2506    /// serves none, with Jane's own key or without it, before and after the
2507    /// migration, which writes that down as judged; his next header, his own
2508    /// address bound, makes it serve his alone, learned.
2509    #[test]
2510    fn a_key_kept_before_bound_to_one_address_it_did_not_come_for_serves_none() {
2511        let root = temporary().join("pgp-r1");
2512        let _ = std::fs::remove_dir_all(&root);
2513        let mark = root.join("state").join("pgp-others.toml");
2514        let jane = open_key(&["Jane <jane@example.org>"], std::time::Duration::from_secs(365 * 24 * 3600));
2515        learn_autocrypt_at(&root, &autocrypt_header(&jane, "jane@example.org").unwrap(), "jane@example.org").unwrap();
2516        let bare = open_key(&["Jane <jane@example.org>"], std::time::Duration::ZERO);
2517        let mallory = bare.clone().insert_packets(vec![openpgp::Packet::from(openpgp::packet::UserID::from("Mallory <mallory@example.net>"))]).unwrap().0;
2518        assert!(bound(&mallory, "jane@example.org") && !bound(&mallory, "mallory@example.net"));
2519        assert!(autocrypt_key(&header_of(&mallory, "mallory@example.net"), "mallory@example.net").is_none(), "refused today");
2520        std::fs::write(root.join("others").join(format!("{}.asc", mallory.fingerprint().to_hex())), public(&mallory)).unwrap();
2521        let (jane_fp, mallory_fp) = (Some(jane.fingerprint()), Some(mallory.fingerprint()));
2522        assert_eq!(Keys::load_at(&root).for_address("jane@example.org").map(Cert::fingerprint), jane_fp, "never the newer key of his");
2523        assert_eq!(keep_others_for_their_addresses_at(&root, &mark).unwrap(), 1, "Jane's was learned: its file is there");
2524        let said = record_of(&root.join("others"), &mallory).unwrap();
2525        assert!(said.addresses.is_empty() && said.judged.is_empty());
2526        assert_eq!(Keys::load_at(&root).for_address("jane@example.org").map(Cert::fingerprint), jane_fp);
2527        remove_in(&root, &jane.fingerprint().to_hex()).unwrap();
2528        assert!(Keys::load_at(&root).for_address("jane@example.org").is_none(), "without her key, still never his");
2529        // His next header, his own address bound to it: his alone, learned.
2530        let own = openpgp::packet::UserID::from("Mallory <mallory@example.net>");
2531        let binding = own.bind(&mut signer_of(&bare), &bare, openpgp::packet::signature::SignatureBuilder::new(openpgp::types::SignatureType::PositiveCertification)).unwrap();
2532        let bound_too = bare.clone().insert_packets(vec![openpgp::Packet::from(own), binding.into()]).unwrap().0;
2533        assert!(learn_autocrypt_at(&root, &header_of(&bound_too, "mallory@example.net"), "mallory@example.net").is_some());
2534        let keys = Keys::load_at(&root);
2535        assert_eq!(keys.for_address("mallory@example.net").map(Cert::fingerprint), mallory_fp);
2536        assert!(keys.for_address("jane@example.org").is_none());
2537        let said = record_of(&root.join("others"), &mallory).unwrap();
2538        assert_eq!((said.addresses, said.judged), (vec!["mallory@example.net".to_string()], Vec::<String>::new()));
2539        // A key kept before naming one address in every user ID, bound: judged to serve it.
2540        let bob = open_key(&["Bob <bob@example.org>", "Robert <bob@example.org>"], std::time::Duration::ZERO);
2541        assert_eq!(judged(&bob), vec!["bob@example.org".to_string()]);
2542        let _ = std::fs::remove_dir_all(&root);
2543    }
2544
2545    /// N2's migration and what follows: once on each device, each key kept
2546    /// without what it serves gets it as judged now (Bob's one address;
2547    /// Carol's two, her own, none until she writes). An older Sioul's later
2548    /// write to Bob's key, a user ID of Jane's he bound merged in, adds no
2549    /// address it serves; its new key naming two addresses serves neither.
2550    /// Carol writing from work, then from home, makes her key serve one,
2551    /// then both; a key imported from a file serves every address bound to
2552    /// it.
2553    #[test]
2554    fn others_keys_say_what_they_serve_and_an_older_write_adds_none() {
2555        let root = temporary().join("pgp-n2-migration");
2556        let _ = std::fs::remove_dir_all(&root);
2557        let mark = root.join("state").join("pgp-others.toml");
2558        let others = root.join("others");
2559        std::fs::create_dir_all(&others).unwrap();
2560        let file = |cert: &Cert, end: &str| others.join(format!("{}.{end}", cert.fingerprint().to_hex()));
2561        let bob = open_key(&["Bob <bob@example.org>"], std::time::Duration::ZERO);
2562        let carol = open_key(&["Carol <carol@work.example>", "Carol <carol@home.example>"], std::time::Duration::ZERO);
2563        for cert in [&bob, &carol] {
2564            std::fs::write(file(cert, "asc"), public(cert)).unwrap();
2565        }
2566        assert_eq!(keep_others_for_their_addresses_at(&root, &mark).unwrap(), 2);
2567        assert_eq!(keep_others_for_their_addresses_at(&root, &mark).unwrap(), 0, "once");
2568        let said = |cert: &Cert| toml::from_str::<Served>(&std::fs::read_to_string(file(cert, "toml")).unwrap()).unwrap();
2569        assert_eq!((said(&bob).format, said(&bob).addresses), (2, vec!["bob@example.org".to_string()]));
2570        assert!(said(&carol).addresses.is_empty());
2571        // An older Sioul merges into Bob's file a user ID of Jane's he bound himself.
2572        let jane_id = openpgp::packet::UserID::from("Bob <jane@example.org>");
2573        let binding = jane_id.bind(&mut signer_of(&bob), &bob, openpgp::packet::signature::SignatureBuilder::new(openpgp::types::SignatureType::PositiveCertification)).unwrap();
2574        let more = bob.clone().insert_packets(vec![openpgp::Packet::from(jane_id), binding.into()]).unwrap().0;
2575        assert!(bound(&more, "jane@example.org"));
2576        std::fs::write(file(&bob, "asc"), public(&more)).unwrap();
2577        // And writes a new key naming two addresses, with nothing beside it.
2578        let dave = open_key(&["Dave <dave@example.org>", "Dave <jane@example.org>"], std::time::Duration::ZERO);
2579        std::fs::write(file(&dave, "asc"), public(&dave)).unwrap();
2580        let keys = Keys::load_at(&root);
2581        assert_eq!(keys.for_address("bob@example.org").map(Cert::fingerprint), Some(bob.fingerprint()));
2582        assert!(keys.for_address("jane@example.org").is_none(), "neither Bob's nor Dave's");
2583        assert!(keys.for_address("dave@example.org").is_none());
2584        assert!(keys.for_address("carol@work.example").is_none() && keys.for_address("carol@home.example").is_none());
2585        // Carol writes from work, then from home.
2586        assert!(learn_autocrypt_at(&root, &header_of(&carol.clone().strip_secret_key_material(), "carol@work.example"), "carol@work.example").is_some());
2587        let keys = Keys::load_at(&root);
2588        assert_eq!(keys.for_address("carol@work.example").map(Cert::fingerprint), Some(carol.fingerprint()));
2589        assert!(keys.for_address("carol@home.example").is_none());
2590        assert!(learn_autocrypt_at(&root, &header_of(&carol.clone().strip_secret_key_material(), "carol@home.example"), "carol@home.example").is_some());
2591        let keys = Keys::load_at(&root);
2592        assert!(keys.for_address("carol@work.example").is_some() && keys.for_address("carol@home.example").is_some(), "learned for both: both");
2593        // Imported from a file you chose: every address bound to it.
2594        let erin = open_key(&["Erin <erin@work.example>", "Erin <erin@home.example>"], std::time::Duration::ZERO);
2595        import_at(&root, &public(&erin)).unwrap();
2596        let keys = Keys::load_at(&root);
2597        assert!(keys.for_address("erin@work.example").is_some() && keys.for_address("erin@home.example").is_some());
2598        let _ = std::fs::remove_dir_all(&root);
2599    }
2600
2601    /// Review of 19 October 2026, N3 (probe
2602    /// `probe2_revoked_user_id_and_certificate`): a certificate revoked as a
2603    /// whole never serves. From a header it is kept, so that its revocation
2604    /// stops the copy kept, and never chosen; a revocation learnt later by a
2605    /// lookup stops a key that served; of two keys of Jane's, the newer
2606    /// revoked as compromised, the older live one serves; yours revoked
2607    /// takes no seal and no copy of your own, and signs nothing; nothing is
2608    /// ever encrypted to it; what was encrypted to it before still opens.
2609    #[test]
2610    fn a_revoked_certificate_never_serves() {
2611        let jane = open_key(&["Jane <jane@example.org>"], std::time::Duration::ZERO);
2612        let root = temporary().join("pgp-n3");
2613        let _ = std::fs::remove_dir_all(&root);
2614        assert!(learn_autocrypt_at(&root, &header_of(&revoked(&jane).strip_secret_key_material(), "jane@example.org"), "jane@example.org").is_some());
2615        assert!(Keys::load_at(&root).for_address("jane@example.org").is_none());
2616        assert!(encrypt(b"x", &[&revoked(&jane)], None).is_err(), "nothing is encrypted to it");
2617        // Kept live, then its revocation from a lookup.
2618        let _ = std::fs::remove_dir_all(&root);
2619        learn_autocrypt_at(&root, &autocrypt_header(&jane, "jane@example.org").unwrap(), "jane@example.org").unwrap();
2620        assert!(Keys::load_at(&root).for_address("jane@example.org").is_some());
2621        assert!(!learn_at(&root, &public(&revoked(&jane)), "jane@example.org").is_empty(), "the revocation kept");
2622        assert!(Keys::load_at(&root).for_address("jane@example.org").is_none());
2623        // Her older key live, her newer one revoked as compromised.
2624        let _ = std::fs::remove_dir_all(&root);
2625        let old = open_key(&["Jane <jane@example.org>"], std::time::Duration::from_secs(365 * 24 * 3600));
2626        learn_autocrypt_at(&root, &autocrypt_header(&old, "jane@example.org").unwrap(), "jane@example.org").unwrap();
2627        learn_at(&root, &public(&revoked(&jane)), "jane@example.org");
2628        assert_eq!(Keys::load_at(&root).for_address("jane@example.org").map(Cert::fingerprint), Some(old.fingerprint()));
2629        // Yours, revoked: no seal, no copy of your own, no signature.
2630        let me = open_key(&["Me <me@example.org>"], std::time::Duration::ZERO);
2631        let yours = Keys { own: vec![revoked(&me)], addresses: vec!["me@example.org".into()], ..Keys::default() };
2632        assert!(yours.own_certs().is_empty() && yours.self_cert_for("me@example.org").is_none() && yours.own_for("me@example.org").is_none());
2633        assert!(revoked_whole(&yours.own[0]) && names_address(&yours.own[0], "me@example.org"), "the window can say it");
2634        // A message sent to it before its revocation still opens with it.
2635        let sealed = encrypt(b"Before.", &[&me], None).unwrap();
2636        let none = |_: &Fingerprint| None;
2637        let sessions = SessionKeys::new();
2638        assert_eq!(decrypt(sealed.as_bytes(), &yours, &Unlock::new(&none, &sessions)).unwrap().data, b"Before.");
2639        let _ = std::fs::remove_dir_all(&root);
2640    }
2641
2642    /// Review of 19 October 2026, R2 (probe `review_probe_n3_designated_revoker`):
2643    /// Jane's certificate names her backup key as its designated revoker,
2644    /// which revokes it as compromised. Sequoia says it "could be" revoked,
2645    /// and leaves the check to whoever holds the revoker's certificate: kept
2646    /// here, the revocation checks, and her certificate serves nothing. Not
2647    /// kept here, nothing checks it: it stays as it was. Yours revoked so
2648    /// takes no seal and no copy of your own, and signs nothing.
2649    #[test]
2650    fn a_designated_revokers_revocation_is_honoured_once_it_checks() {
2651        use openpgp::types::{ReasonForRevocation, RevocationKey};
2652        let root = temporary().join("pgp-r2");
2653        let _ = std::fs::remove_dir_all(&root);
2654        let naming = |userid: &str, revoker: &Cert| {
2655            CertBuilder::new()
2656                .set_cipher_suite(CipherSuite::Cv25519)
2657                .add_userid(userid)
2658                .add_transport_encryption_subkey()
2659                .add_signing_subkey()
2660                .set_creation_time(std::time::SystemTime::now() - std::time::Duration::from_secs(3600))
2661                .set_revocation_keys(vec![RevocationKey::new(revoker.primary_key().key().pk_algo(), revoker.fingerprint(), false)])
2662                .generate()
2663                .unwrap()
2664                .0
2665        };
2666        let revoke = |cert: &Cert, by: &Cert| {
2667            let revocation = CertRevocationBuilder::new().set_reason_for_revocation(ReasonForRevocation::KeyCompromised, b"stolen").unwrap().build(&mut signer_of(by), cert, None).unwrap();
2668            cert.clone().insert_packets(vec![openpgp::Packet::from(revocation)]).unwrap().0
2669        };
2670        let backup = open_key(&["Jane's backup <jane-backup@example.org>"], std::time::Duration::ZERO);
2671        let jane = naming("Jane <jane@example.org>", &backup);
2672        let revoked = revoke(&jane, &backup);
2673        assert!(usable(&revoked), "Sequoia alone: it could be");
2674        learn_autocrypt_at(&root, &header_of(&revoked, "jane@example.org"), "jane@example.org").unwrap();
2675        assert!(Keys::load_at(&root).for_address("jane@example.org").is_some(), "its revoker not kept here: nothing checks it");
2676        learn_autocrypt_at(&root, &header_of(&backup, "jane-backup@example.org"), "jane-backup@example.org").unwrap();
2677        let keys = Keys::load_at(&root);
2678        assert!(keys.for_address("jane@example.org").is_none(), "her revoker's revocation checks");
2679        let stored = keys.others.iter().find(|c| c.fingerprint() == jane.fingerprint()).unwrap();
2680        assert!(!keys.usable(stored));
2681        // A revocation made by a key it does not name changes nothing.
2682        let mallory = open_key(&["Mallory <mallory@example.net>"], std::time::Duration::ZERO);
2683        let forged = revoke(&jane, &mallory);
2684        assert!(Keys { others: vec![forged.clone(), mallory.clone()], ..Keys::default() }.usable(&forged));
2685        // Yours, revoked by your own designated revoker kept here: no seal, no copy of your own, no signature.
2686        let mine = open_key(&["Me backup <me-backup@example.org>"], std::time::Duration::ZERO);
2687        let me = revoke(&naming("Me <me@example.org>", &mine), &mine);
2688        let yours = Keys { own: vec![me.clone(), mine.clone()], addresses: vec!["me@example.org".into()], ..Keys::default() };
2689        assert!(yours.own_certs().iter().all(|c| c.fingerprint() != me.fingerprint()));
2690        assert!(yours.self_cert_for("me@example.org").is_none() && yours.own_for("me@example.org").is_none());
2691        let alone = Keys { own: vec![me.clone()], addresses: vec!["me@example.org".into()], ..Keys::default() };
2692        assert!(alone.self_cert_for("me@example.org").is_some() && alone.own_for("me@example.org").is_some(), "its revoker not kept: nothing checks it");
2693        // A message Jane signed: its signature is not taken for good once her revoker's revocation checks.
2694        let noa = open_key(&["Noa <noa@example.org>"], std::time::Duration::ZERO);
2695        let mut jane_signs = here_signer(&jane, "").unwrap();
2696        let signed = encrypt(b"The lease.", &[&noa], Some(&mut jane_signs)).unwrap();
2697        let none = |_: &Fingerprint| None;
2698        let sessions = SessionKeys::new();
2699        let reading = Keys { own: vec![noa.clone()], others: vec![revoked.clone(), backup.clone()], ..Keys::default() };
2700        let checked = decrypt(signed.as_bytes(), &reading, &Unlock::new(&none, &sessions)).unwrap().signatures;
2701        assert_eq!((checked[0].good, checked[0].problem.as_str()), (false, "revoked key"));
2702        let without = Keys { own: vec![noa.clone()], others: vec![revoked.clone()], ..Keys::default() };
2703        assert!(decrypt(signed.as_bytes(), &without, &Unlock::new(&none, &sessions)).unwrap().signatures[0].good, "its revoker unknown here");
2704        let _ = std::fs::remove_dir_all(&root);
2705    }
2706
2707    /// Review of 19 October 2026, Info (probe `probe2_lookups_…`): a lookup
2708    /// that brings your own certificate with a subkey bound by someone
2709    /// else's key adds nothing of it: only what your key signed is merged.
2710    #[test]
2711    fn a_lookup_bringing_yours_adds_only_what_your_key_signed() {
2712        let root = temporary().join("pgp-lookup-yours");
2713        let _ = std::fs::remove_dir_all(&root);
2714        let me = open_key(&["Me <me@example.org>"], std::time::Duration::ZERO);
2715        store_in(&root, me.clone(), true).unwrap();
2716        let mallory = open_key(&["Mallory <mallory@example.net>"], std::time::Duration::ZERO);
2717        let stray = open_key(&[], std::time::Duration::ZERO).keys().subkeys().next().unwrap().key().clone();
2718        let binding = openpgp::packet::signature::SignatureBuilder::new(openpgp::types::SignatureType::SubkeyBinding)
2719            .set_key_flags(openpgp::types::KeyFlags::empty().set_transport_encryption())
2720            .unwrap()
2721            .sign_subkey_binding(&mut signer_of(&mallory), me.primary_key().key(), &stray)
2722            .unwrap();
2723        let mut packets: Vec<openpgp::Packet> = me.clone().strip_secret_key_material().into_packets().collect();
2724        packets.push(openpgp::Packet::from(stray.clone()));
2725        packets.push(binding.into());
2726        let polluted = Cert::from_packets(packets.into_iter()).unwrap();
2727        assert!(!learn_at(&root, &polluted.armored().to_vec().unwrap(), "me@example.org").is_empty());
2728        let stored = Cert::from_bytes(&std::fs::read(root.join("own").join(format!("{}.asc", me.fingerprint().to_hex()))).unwrap()).unwrap();
2729        assert_eq!(stored.keys().subkeys().count(), me.keys().subkeys().count(), "the stray subkey left out");
2730        assert!(stored.is_tsk(), "still your secret key");
2731        let _ = std::fs::remove_dir_all(&root);
2732    }
2733
2734    #[test]
2735    fn autocrypt_carries_the_smallest_key() {
2736        let alice = key("alice@example.org");
2737        let header = autocrypt_header(&alice, "alice@example.org").unwrap();
2738        assert!(header.starts_with("addr=alice@example.org; keydata="));
2739        assert!(header.split_whitespace().all(|word| word.len() <= 100), "foldable: {header}");
2740        let found = autocrypt_key(&header, "alice@example.org").unwrap();
2741        assert_eq!(found.fingerprint(), alice.fingerprint());
2742        assert!(!found.is_tsk());
2743        assert!(autocrypt_key(&header, "mallory@example.org").is_none());
2744    }
2745
2746    /// A key made and exported by GnuPG 2.4.4 (Ubuntu 24.04), cut to its
2747    /// primary key, its user ID and two of its subkeys: the second subkey's
2748    /// binding signature has its second number written over 32 octets, the
2749    /// first a zero, its length said as 256 bits.
2750    const GNUPG_2_4_4: &[u8] = include_bytes!("../tests/fixtures/pgp/gnupg-2.4.4-non-minimal-mpi.asc");
2751
2752    /// The CI's GnuPG wrote about one key signature in 128 so: Sequoia alone
2753    /// drops it, and a subkey (or the whole key, when it is the user ID's)
2754    /// reads as missing (securitykey's test failed once in a while on CI).
2755    /// Read here as its value, every signature still checked by Sequoia.
2756    #[test]
2757    fn a_signature_gnupg_wrote_long_is_read_as_its_value() {
2758        let strict = Cert::from_bytes(GNUPG_2_4_4).unwrap();
2759        assert_eq!(securitykey::parts(&strict).len(), 2, "Sequoia alone: the primary key and one subkey, the other's binding dropped");
2760        let read = certificates_in(GNUPG_2_4_4);
2761        assert_eq!(read.len(), 1);
2762        let parts = securitykey::parts(&read[0]);
2763        assert_eq!(parts.len(), 3, "{parts:?}");
2764        assert!(parts[1..].iter().all(|part| part.role == securitykey::Role::Authentication), "{parts:?}");
2765        assert_eq!(securitykey::parts_in(GNUPG_2_4_4), parts);
2766        // Every signature there checked: the subkeys bound by the primary key.
2767        let policy = policy();
2768        let valid = read[0].with_policy(&policy, None).unwrap();
2769        assert_eq!(valid.keys().subkeys().count(), 2);
2770        // Read once, it is written shortest: nothing left to change.
2771        let written = read[0].to_vec().unwrap();
2772        assert!(matches!(minimal_mpis(&written), std::borrow::Cow::Borrowed(_)));
2773        assert!(matches!(minimal_mpis(GNUPG_2_4_4), std::borrow::Cow::Owned(_)));
2774    }
2775
2776    /// The same fault made by hand on Sequoia's own key: each signature's
2777    /// last number written one octet longer, a zero first. Read back, the
2778    /// bytes are the key's own again; a key already shortest, a file that
2779    /// is no key and one cut short come back as they came.
2780    #[test]
2781    fn numbers_written_long_come_back_shortest() {
2782        let alice = key("alice@example.org").strip_secret_key_material();
2783        let binary = alice.to_vec().unwrap();
2784        let armored = String::from_utf8(alice.armored().to_vec().unwrap()).unwrap();
2785        assert!(matches!(minimal_mpis(&binary), std::borrow::Cow::Borrowed(_)));
2786        assert!(matches!(minimal_mpis(armored.as_bytes()), std::borrow::Cow::Borrowed(_)));
2787        let long = written_long(&binary);
2788        assert!(Cert::from_bytes(&long).ok().is_none_or(|strict| securitykey::parts(&strict).is_empty()), "Sequoia alone refuses what is written long");
2789        assert_eq!(minimal_mpis(&long).as_ref(), binary.as_slice());
2790        assert_eq!(certificates_in(&long), [alice]);
2791        // Nothing to read, or cut short: as it came.
2792        assert!(matches!(minimal_mpis(b"no key here"), std::borrow::Cow::Borrowed(_)));
2793        assert_eq!(minimal_mpis(b"no key here").as_ref(), b"no key here");
2794        let cut = &long[..long.len() - 5];
2795        assert_eq!(minimal_mpis(cut).as_ref(), cut);
2796        assert!(matches!(minimal_mpis(&[]), std::borrow::Cow::Borrowed(_)));
2797    }
2798
2799    /// Where a version 4 signature body's numbers begin: past its two areas
2800    /// and the hash's first two octets.
2801    fn numbers_at(body: &[u8]) -> usize {
2802        let mut at = 4;
2803        for _ in 0..2 {
2804            at += 2 + usize::from(u16::from_be_bytes([body[at], body[at + 1]]));
2805        }
2806        at + 2
2807    }
2808
2809    /// `body` (a signature's) with its last number written anew: `new` gets
2810    /// its length in bits and the octets from it on, and gives the length to
2811    /// say and the octets to write.
2812    fn last_number_as(body: &[u8], new: impl Fn(usize, &[u8]) -> (usize, Vec<u8>)) -> Vec<u8> {
2813        let mut at = numbers_at(body);
2814        let mut last = at;
2815        while at < body.len() {
2816            last = at;
2817            at += 2 + usize::from(u16::from_be_bytes([body[at], body[at + 1]])).div_ceil(8);
2818        }
2819        let bits = usize::from(u16::from_be_bytes([body[last], body[last + 1]]));
2820        let (said, octets) = new(bits, &body[last + 2..]);
2821        let mut out = body[..last].to_vec();
2822        out.extend_from_slice(&u16::try_from(said).unwrap().to_be_bytes());
2823        out.extend_from_slice(&octets);
2824        out
2825    }
2826
2827    /// GnuPG 2.4.4's fault, made by hand: a number one octet longer, a zero
2828    /// first, its length said with it.
2829    fn one_octet_long(bits: usize, value: &[u8]) -> (usize, Vec<u8>) {
2830        let octets = bits.div_ceil(8);
2831        (octets * 8 + 8, std::iter::once(0).chain(value[..octets].iter().copied()).collect())
2832    }
2833
2834    /// The packets of `binary`, each signature's body as `each` writes it.
2835    fn signatures_as(binary: &[u8], each: impl Fn(&[u8]) -> Vec<u8>) -> Vec<u8> {
2836        let mut out = Vec::new();
2837        let mut at = 0;
2838        while at < binary.len() {
2839            let (tag, body) = packet_at(binary, at).unwrap();
2840            let packet = &binary[body.clone()];
2841            put_packet(&mut out, tag, &if tag == 2 { each(packet) } else { packet.to_vec() });
2842            at = body.end;
2843        }
2844        out
2845    }
2846
2847    /// Each signature's last number written one octet longer.
2848    fn written_long(binary: &[u8]) -> Vec<u8> {
2849        signatures_as(binary, |body| last_number_as(body, one_octet_long))
2850    }
2851
2852    /// Binary packets armored as a public key block.
2853    fn armored_block(binary: &[u8]) -> Vec<u8> {
2854        let mut out = Vec::new();
2855        let mut writer = openpgp::armor::Writer::new(&mut out, openpgp::armor::Kind::PublicKey).unwrap();
2856        writer.write_all(binary).unwrap();
2857        writer.finalize().unwrap();
2858        out
2859    }
2860
2861    /// Several keys in one file, each its armored block, some needing the
2862    /// repair and some not, in every order, words between them: every key
2863    /// read, each whole, never fewer than Sequoia alone reads. Until 15
2864    /// October 2026 a first block that needed the repair was read alone,
2865    /// the next ones dropped, and a later one was not repaired.
2866    #[test]
2867    fn concatenated_armored_keys_are_each_read_and_repaired() {
2868        let gnupg = certificates_in(GNUPG_2_4_4).remove(0);
2869        let bob = key("bob@example.org").strip_secret_key_material();
2870        let carol = key("carol@example.org").strip_secret_key_material();
2871        let blocks: [(&Cert, Vec<u8>); 3] = [(&gnupg, GNUPG_2_4_4.to_vec()), (&bob, bob.armored().to_vec().unwrap()), (&carol, armored_block(&written_long(&carol.to_vec().unwrap())))];
2872        let orders: [&[usize]; 12] = [&[0, 1], &[1, 0], &[1, 2], &[2, 1], &[0, 2], &[2, 0], &[0, 1, 2], &[0, 2, 1], &[1, 0, 2], &[1, 2, 0], &[2, 0, 1], &[2, 1, 0]];
2873        for order in orders {
2874            let mut file = Vec::new();
2875            for &i in order {
2876                file.extend_from_slice(&blocks[i].1);
2877                file.extend_from_slice(b"\nSomething said between the keys.\n");
2878            }
2879            let read = certificates_of(&file).unwrap();
2880            let fingerprints: Vec<Fingerprint> = read.iter().map(Cert::fingerprint).collect();
2881            let wanted: Vec<Fingerprint> = order.iter().map(|&i| blocks[i].0.fingerprint()).collect();
2882            assert_eq!(fingerprints, wanted, "{order:?}");
2883            for (cert, &i) in read.iter().zip(order) {
2884                assert_eq!(securitykey::parts(cert), securitykey::parts(blocks[i].0), "{order:?}: key {i} whole");
2885            }
2886            let alone: Vec<Fingerprint> = CertParser::from_bytes(&file).unwrap().filter_map(Result::ok).map(|c| c.fingerprint()).collect();
2887            assert!(alone.iter().all(|f| fingerprints.contains(f)), "{order:?}: never fewer than Sequoia alone");
2888            assert_eq!(matches!(minimal_mpis(&file), std::borrow::Cow::Owned(_)), order.contains(&0) || order.contains(&2), "{order:?}");
2889        }
2890        // Bob's alone, nothing to repair: as it came.
2891        assert!(matches!(minimal_mpis(&blocks[1].1), std::borrow::Cow::Borrowed(_)));
2892    }
2893
2894    /// A key made by GnuPG 2.4.9 with a subkey that signs: its binding carries
2895    /// the subkey's own signature back (the back-signature) in its unhashed area.
2896    const SIGNING_SUBKEY: &[u8] = include_bytes!("../tests/fixtures/pgp/gnupg-2.4.9-signing-subkey.asc");
2897
2898    /// A signature `body` with each signature embedded in its unhashed area
2899    /// written as `each` says; with `to_hashed`, moved into its hashed area.
2900    fn embedded_as(body: &[u8], each: impl Fn(&[u8]) -> Vec<u8>, to_hashed: bool) -> Vec<u8> {
2901        let hashed_end = 6 + usize::from(u16::from_be_bytes([body[4], body[5]]));
2902        let area_len = usize::from(u16::from_be_bytes([body[hashed_end], body[hashed_end + 1]]));
2903        let area = &body[hashed_end + 2..hashed_end + 2 + area_len];
2904        let (mut kept, mut embedded) = (Vec::new(), Vec::new());
2905        let mut at = 0;
2906        while at < area.len() {
2907            // These subpackets are short: one octet of length.
2908            let length = usize::from(area[at]);
2909            assert!(length < 192);
2910            let (kind, inner) = (area[at + 1], &area[at + 2..at + 1 + length]);
2911            if kind & 0x7F == 32 {
2912                let written = each(inner);
2913                put_subpacket_length(&mut embedded, 1 + written.len());
2914                embedded.push(kind);
2915                embedded.extend_from_slice(&written);
2916            } else {
2917                kept.extend_from_slice(&area[at..at + 1 + length]);
2918            }
2919            at += 1 + length;
2920        }
2921        assert!(!embedded.is_empty(), "a back-signature there");
2922        let (hashed, unhashed) = if to_hashed { ([&body[6..hashed_end], &embedded[..]].concat(), kept) } else { (body[6..hashed_end].to_vec(), [&kept[..], &embedded[..]].concat()) };
2923        let mut out = body[..4].to_vec();
2924        out.extend_from_slice(&u16::try_from(hashed.len()).unwrap().to_be_bytes());
2925        out.extend_from_slice(&hashed);
2926        out.extend_from_slice(&u16::try_from(unhashed.len()).unwrap().to_be_bytes());
2927        out.extend_from_slice(&unhashed);
2928        out.extend_from_slice(&body[hashed_end + 2 + area_len..]);
2929        out
2930    }
2931
2932    /// The binding's back-signature written long, as GnuPG 2.4.4 may write
2933    /// any of its signatures: Sequoia alone drops the binding, and the
2934    /// signing subkey with it; read here, the key is whole again. The same
2935    /// number written long in the hashed area, which the binding signs, is
2936    /// left as it came.
2937    #[test]
2938    fn a_back_signature_written_long_is_read_shortest_in_the_unhashed_area_only() {
2939        let canonical = Cert::from_bytes(SIGNING_SUBKEY).unwrap();
2940        let signs = |cert: &Cert| securitykey::parts(cert).iter().any(|part| part.role == securitykey::Role::Signing);
2941        assert!(signs(&canonical));
2942        let mut binary = Vec::new();
2943        std::io::Read::read_to_end(&mut openpgp::armor::Reader::from_bytes(SIGNING_SUBKEY, openpgp::armor::ReaderMode::Tolerant(None)), &mut binary).unwrap();
2944        let binding = |to_hashed: bool| move |body: &[u8]| if body[1] == 0x18 { embedded_as(body, |inner| last_number_as(inner, one_octet_long), to_hashed) } else { body.to_vec() };
2945        let long = signatures_as(&binary, binding(false));
2946        assert!(Cert::from_bytes(&long).ok().is_none_or(|strict| !signs(&strict)), "Sequoia alone drops the signing subkey");
2947        assert!(matches!(minimal_mpis(&long), std::borrow::Cow::Owned(_)));
2948        assert_eq!(Cert::from_bytes(minimal_mpis(&long).as_ref()).unwrap(), canonical);
2949        let read = certificates_in(&long);
2950        assert_eq!(read.len(), 1);
2951        assert!(signs(&read[0]));
2952        // In the hashed area instead: never touched.
2953        let hashed = signatures_as(&binary, binding(true));
2954        assert!(matches!(minimal_mpis(&hashed), std::borrow::Cow::Borrowed(_)), "the signed area as it came");
2955    }
2956
2957    /// Only ever shorter: a number whose length is said one bit too short
2958    /// (a bit past it set, which Sequoia refuses and GnuPG never writes) is
2959    /// left as it came, and so is its whole signature, though another of its
2960    /// numbers be written long.
2961    #[test]
2962    fn a_length_said_too_short_is_left_as_it_came() {
2963        let dave = key("dave@example.org").strip_secret_key_material().to_vec().unwrap();
2964        // The first number said a bit short in as many octets; none when it cannot be.
2965        let short_first = |body: &[u8]| -> Option<Vec<u8>> {
2966            let first = numbers_at(body);
2967            let bits = usize::from(u16::from_be_bytes([body[first], body[first + 1]]));
2968            if bits < 2 || (bits - 1).div_ceil(8) != bits.div_ceil(8) {
2969                return None;
2970            }
2971            let mut out = body.to_vec();
2972            out[first..first + 2].copy_from_slice(&u16::try_from(bits - 1).unwrap().to_be_bytes());
2973            Some(out)
2974        };
2975        let mut checked = 0;
2976        let mut at = 0;
2977        while at < dave.len() {
2978            let (tag, body) = packet_at(&dave, at).unwrap();
2979            if let Some(short) = (tag == 2).then(|| short_first(&dave[body.clone()])).flatten() {
2980                checked += 1;
2981                assert_eq!(signature_shortened(&short), None, "said short: as it came");
2982                assert_eq!(signature_shortened(&last_number_as(&short, one_octet_long)), None, "beside a number written long: as it came");
2983            }
2984            at = body.end;
2985        }
2986        assert!(checked > 0);
2987        let short_key = signatures_as(&dave, |body| short_first(body).unwrap_or_else(|| body.to_vec()));
2988        assert!(matches!(minimal_mpis(&short_key), std::borrow::Cow::Borrowed(_)));
2989    }
2990
2991    /// A signature body (EdDSA) whose unhashed area holds `inner`, embedded,
2992    /// if any, and whose two numbers are `numbers` (their lengths said).
2993    fn nesting(inner: Option<&[u8]>, numbers: &[u8]) -> Vec<u8> {
2994        let mut area = Vec::new();
2995        if let Some(inner) = inner {
2996            put_subpacket_length(&mut area, 1 + inner.len());
2997            area.push(32);
2998            area.extend_from_slice(inner);
2999        }
3000        let mut body = vec![4, 0x19, 22, 10, 0, 0];
3001        body.extend_from_slice(&u16::try_from(area.len()).unwrap().to_be_bytes());
3002        body.extend_from_slice(&area);
3003        body.extend_from_slice(&[0xAB, 0xCD]);
3004        body.extend_from_slice(numbers);
3005        body
3006    }
3007
3008    /// Signatures embedded in embedded signatures, thousands deep (made by
3009    /// hand: no OpenPGP program writes them, Sequoia refuses them): looked
3010    /// into one level only, so the file comes back at once, as it came, on
3011    /// a thread with a small stack (a phone's import runs on Qt's, about
3012    /// 1 MiB), though the deepest's numbers are written long. One level
3013    /// down, a signature written long is still repaired (a back-signature);
3014    /// two levels down, never.
3015    #[test]
3016    fn signatures_nested_deep_are_looked_into_one_level_only() {
3017        // r and s, each the number 1: shortest, then one said in nine bits, a zero octet first.
3018        let shortest: &[u8] = &[0, 1, 1, 0, 1, 1];
3019        let long: &[u8] = &[0, 9, 0, 1, 0, 1, 1];
3020        let packet = |body: &[u8]| {
3021            let mut out = Vec::new();
3022            put_packet(&mut out, 2, body);
3023            out
3024        };
3025        let mut deep = nesting(None, long);
3026        let mut levels = 0;
3027        while deep.len() < 60_000 {
3028            deep = nesting(Some(&deep), shortest);
3029            levels += 1;
3030        }
3031        assert!(levels > 2_000, "{levels} levels");
3032        let file = packet(&nesting(Some(&deep), shortest));
3033        let read = std::thread::Builder::new().stack_size(256 * 1024).spawn(move || matches!(minimal_mpis(&file), std::borrow::Cow::Borrowed(_))).unwrap();
3034        assert!(read.join().unwrap(), "as it came, at once");
3035        // One level down: repaired; two levels down: as it came.
3036        let one = packet(&nesting(Some(&nesting(None, long)), shortest));
3037        assert!(matches!(minimal_mpis(&one), std::borrow::Cow::Owned(_)));
3038        let two = packet(&nesting(Some(&nesting(Some(&nesting(None, long)), shortest)), shortest));
3039        assert!(matches!(minimal_mpis(&two), std::borrow::Cow::Borrowed(_)));
3040    }
3041}