Skip to content

An OpenPGP key on a security key: crates, systems and risks

The question: how can Sioul sign and decrypt mail with an OpenPGP key held on a smart card or a security key such as a YubiKey, on Linux, Windows, macOS and Android, without GnuPG, and what does each way cost in packaging and security? Sioul is GPL-3.0-or-later, with a Rust core and a Qt 6 QML window; its OpenPGP is Sequoia's (sequoia-openpgp 2.4, LGPL-2.0-or-later, with its pure-Rust cryptography).

Researched on 7 October 2026, read-only: nothing was built or installed, and no real key was used. From primary sources linked at each fact: the crates' source code at the versions published that day, the OpenPGP card specification, GnuPG's manual and source, Yubico's, Microsoft's and Android's documentation, pcsc-lite's sources and change log, Flatpak's and the distributions' package records; forum threads and bug reports are named as such. "(unverified)" marks what could not be confirmed at a primary source. "Built", "not built" and "next" say where Sioul stood on 8 October 2026 (version 0.0.3), checked against its code. The feature: client.md ("Your keys on a security key").

In short

  • Possible on every system, without importing anything secret. The private keys stay in the security key; the program sends it what must be signed or opened, and gets the result back. The program keeps only the public part, the certificate, which a security key does not hold: it is found once (the address written on the key, the Web Key Directory of the address's domain, keys.openpgp.org, or a file from gpg --export) and checked against the key's fingerprints and public keys.
  • On computers (Linux, Windows, macOS), the program can talk to the key itself through the system's smart card service (PC/SC), with the openpgp-card crates (MIT OR Apache-2.0, maintained) and about 150 lines of Sequoia glue of its own: the maintained Sequoia adapter no longer exists, and Sequoia's own key store carries a copy to follow. GnuPG is not needed.
  • On Android, the same Rust code can run over a small Java transport: NFC first (reader mode, IsoDep), USB-C second (CCID). No other app is needed. OpenKeychain is "no longer actively developed", and Yubico's Android OpenPGP module would put the protocol in Java a second time.
  • For the person: a PIN to type (and a touch, when the key asks for one) to sign, or to open an encrypted message; encrypting alone needs no key.
  • Packaging: the Flatpak needs --socket=pcsc, and the AppImage a recent pcsc-lite client. Tests can run in CI against a virtual OpenPGP card (opcard-rs through vsmartcard), as Sequoia's do.

1. On a computer: the openpgp-card crates

1.1 What exists (crates.io, read 7 October 2026)

Crate Version, date Licence Notes
openpgp-card 0.7.0, 23 July 2026 (0.6.0 March 2026, 0.5.0 May 2024) MIT OR Apache-2.0 the card protocol, "PGP implementation agnostic"; its repository moved to codeberg.org/openpgp-card/openpgp-card
card-backend 0.2.0, 5 September 2023 MIT OR Apache-2.0 the two traits a transport implements (section 1.3)
card-backend-pcsc 0.5.2, 23 July 2026 MIT OR Apache-2.0 the PC/SC transport, on the pcsc crate
pcsc / pcsc-sys 2.9.0 / 1.3.0, 14 December 2024 MIT bindings to the system's PC/SC
openpgp-card-rpgp 0.8.1, 23 July 2026 MIT OR Apache-2.0 the high-level signer and decryptor, but for rPGP, not Sequoia
openpgp-card-state 0.4.1, 21 March 2026 MIT OR Apache-2.0 shared card metadata and stored User PINs for applications (oct, the SSH agent)
openpgp-card-sequoia 0.2.2, 5 June 2024; depends on sequoia-openpgp ^1.4 and openpgp-card ^0.4.2 MIT OR Apache-2.0 abandoned: the project's README no longer lists it, and Sequoia's own key store says in its source: "openpgp-card-sequoia is deprecated. We've copied what we need, but we should fork it." It cannot be used with Sequoia 2.4
openpgp-card-tools (oct) 0.11.13, 23 July 2026 MIT OR Apache-2.0 the command-line tool, now built on rPGP

The openpgp-card crates are Heiko Schaefer's, all released together on 23 July 2026: maintained. pcsc is Ran Benita's. MIT OR Apache-2.0 and MIT are compatible with GPL-3.0-or-later: Sioul may link them and copy code from them, keeping their copyright notices. Sources: openpgp-card's README, "Architecture" (https://codeberg.org/openpgp-card/openpgp-card); crates.io's record of each crate (https://crates.io/crates/openpgp-card, and likewise for the others); pcsc-rust (https://github.com/bluetech/pcsc-rust); the crates' source.

What follows: no maintained Sequoia adapter for the card exists any more. What remains is openpgp-card 0.7 (the protocol) with card-backend-pcsc 0.5 (the transport), and Sequoia glue of one's own: a crypto::Signer and a crypto::Decryptor, the two traits Sequoia 2.4 asks for (sequoia-openpgp 2.4.1, src/crypto/asymmetric.rs; Sequoia-PGP, https://gitlab.com/sequoia-pgp/sequoia). The glue is small, and already written twice under MIT OR Apache-2.0: in the abandoned openpgp-card-sequoia, and, updated for Sequoia 2 and openpgp-card 0.7, in sequoia-keystore-openpgp-card 0.2.2 (1 October 2026; src/lib.rs, its decrypt_ciphertext and sign, about 150 lines derived from Heiko Schaefer's code, his notice kept; https://gitlab.com/sequoia-pgp/sequoia-keystore):

  • Decrypting: RSA → Cryptogram::RSA(c) → decipher gives the session key. ECDH → the ephemeral point (for Cv25519, without its 0x40 prefix) → decipher gives the shared secret S → Sequoia's public ecdh::decrypt_unwrap(&public, &S, ciphertext, None) unwraps the session key (a quirk of Gnuk on P-256 is handled).
  • Signing: RSA with SHA-256, SHA-384 or SHA-512 (openpgp-card wraps the hash in a DigestInfo, and the card pads and signs); EdDSA → signature_for_hash(SigningAlgo::ECC, digest) → r ‖ s (32 + 32 bytes); ECDSA → the digest cut to the curve's size → r ‖ s.
  • Sequoia's streaming Signer::new(message, signer) takes any S: crypto::Signer + Send + Sync, and PKESK::decrypt(&mut dyn Decryptor, …) takes any decryptor (serialize/stream.rs line 759, packet/pkesk.rs line 103): code that signs and decrypts with a software KeyPair takes a card's signer or decryptor in its place without changing shape.
  • Fingerprints on a card are v4 (20 bytes, card specification 3.4): a card holds v4 keys, and Sequoia's key store reads them as Fingerprint::from_bytes(4, …).

The card specification: Achim Pietig, Functional Specification of the OpenPGP application on ISO Smart Card Operating Systems, version 3.4.1, 2020 (https://gnupg.org/ftp/specs/OpenPGP-smart-card-application-3.4.1.pdf); its §7.2.10.2 gives the DigestInfo for RSA.

1.2 What openpgp-card 0.7 reads and does (source read)

  • Card::<Open>::new(backend) selects the OpenPGP application; card.transaction() gives a Card<Transaction>, the only state in which the card is used.
  • Read without a PIN: application_identifier() (manufacturer and serial: "000615…" for Yubico), fingerprints() (the signature, decryption and authentication slots), key_generation_times(), key_information() (which slots hold a key, generated or imported), url() (the "URL of the public key" data object), cardholder_name() and cardholder_related_data(), login_data(), digital_signature_count() (the signature counter), pw_status_bytes() (the three retry counters, and whether the signature PIN is asked once per signature), user_interaction_flag(slot) (the touch policy), kdf_do(), algorithm_attributes(), public_key_material(slot) (the raw public key), firmware_version(), feature_pinpad_verify().
  • PINs: verify_user_pin(SecretString) (PW1 in mode 82: decrypting), verify_user_signing_pin(SecretString) (PW1 in mode 81: signing; "depending on the configuration of the card, this may enable performing just one signing operation"), verify_admin_pin (PW3, never needed to use the keys), and pinpad variants for readers with a keypad. PINs are secrecy::SecretString, zeroed when dropped.
  • KDF-DO (the card stores a salted, iterated hash of the PIN instead of the PIN) is handled inside verify_* (map_pin(pin, …, kdf_do)): nothing for the application to do.
  • Touch: the card blocks the command until it is touched. A YubiKey "signals a request for a touch event by blinking for up to 15 seconds. If no touch is provided within this period, the operation will fail" (Yubico, "Card edit", https://developers.yubico.com/PGP/Card_edit.html). Its policies: Off (the default), On, Cached ("cached for 15s after use"), Fixed, and Cached-Fixed ("cannot be disabled without deleting the private key") (Yubico, YubiKey Manager's OpenPGP commands, ykman openpgp keys set-touch, https://docs.yubico.com/software/yubikey/tools/ykman/OpenPGP_Commands.html). An application can read the policy beforehand, and say "touch the key" at the right moment.
  • The YubiKey 5 implements OpenPGP card specification 3.4 from firmware 5.2.3: RSA 2048 to 4096; NIST, brainpool and secp256k1 curves; Ed25519 for signing and X25519 for decryption; KDF; attestation. Its default PINs are 123456 (user) and 12345678 (admin) (Yubico, YubiKey 5 technical manual, "OpenPGP", https://docs.yubico.com/hardware/yubikey/yk-tech-manual/yk5-apps-openpgp.html).

1.3 The transport traits (card-backend 0.2, the whole interface)

  • CardBackend: limit_card_caps(caps) -> caps and transaction(reselect: Option<&[u8]>) -> Box<dyn CardTransaction + Send + Sync>.
  • CardTransaction: transmit(apdu, buf_size) -> response (the response with its two status bytes), select(aid) (provided), max_cmd_len() (provided), feature_pinpad_verify/modify, pinpad_verify/modify, was_reset().
  • Everything above it (command chaining, extended lengths, the OpenPGP logic) is in openpgp-card: a new transport (Android's, section 4) is a few dozen lines.

1.4 How the card is reached on each system

  • pcsc-sys links the system's PC/SC at build time (build.rs). Windows: winscard.dll, the Smart Card service, built in; the YubiKey's CCID interface uses Windows' own driver. macOS: the PCSC framework, built in. Linux and the BSDs: libpcsclite, found by pkg-config, which is the client library of pcscd, the PC/SC daemon (pcsc-lite) that holds the USB reader and serves applications over the socket /run/pcscd/pcscd.comm. On Linux, then, a program linked so needs libpcsclite.so.1 to start at all, and pcscd (with the CCID driver, pcsc-lite-ccid or libccid) to find a card.
  • Shared, not exclusive: card-backend-pcsc connects in ShareMode::Shared unless asked otherwise, and wraps each operation in a PC/SC transaction (SCardBeginTransaction), which other shared users respect; a reset by another program is detected, and the application selected again (card-backend-pcsc 0.5.2, PcscTransaction::new). On Windows, "If a transaction is held on the card for more than five seconds with no operations happening on that card, then the card is reset" (Microsoft, SCardBeginTransaction, Remarks, https://learn.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardbegintransaction): a transaction must not wait for a person, so the PIN is asked before it opens.
  • Closing resets the card: card-backend-pcsc has no way to close but dropping, and the pcsc crate's Drop for Card disconnects with Disposition::ResetCard ("Disposition is hard-coded to ResetCard here", pcsc 2.9.0, src/lib.rs line 1668). A reset clears the card's "PIN verified" state. That is the safe behaviour: the card's security state belongs to the card, not to a connection, so a card left verified (LeaveCard) could be used without its PIN by any program connecting next. The costs: the PIN is given again for each operation (from memory, when the application keeps it), as Sequoia's key store does; and when GnuPG shares the card (pcsc-shared, section 2.3), gpg asks for its PIN again after another program used the key.

What Sioul does

  • The card itself, through PC/SC, on computers: openpgp-card 0.7.0, card-backend 0.2.0, card-backend-pcsc 0.5.2 and pcsc 2.9.0, pinned to these exact versions (openpgp-card moved from 0.5 to 0.7 in two years), for every system but Android: built (crates/sioul-sync/Cargo.toml; crates/sioul-sync/src/securitykey/smartcard.rs).
  • Its own Sequoia glue: CardSigner and CardDecryptor are Sequoia's Signer and Decryptor for a key on a card, after sequoia-keystore-openpgp-card 0.2.2: RSA's DigestInfo (built by openpgp-card, padded and signed by the card), EdDSA as r ‖ s, ECDSA cut to the curve's size, ECDH's shared secret unwrapped by Sequoia, Curve25519's 0x40 prefix and Gnuk's P-256 answer handled. The card is opened when Sequoia asks for a signature or a session key, not before: built (crates/sioul-core/src/securitykey.rs).
  • Shared, one operation at a time, reset after: Sioul connects each reader itself in shared mode, since card-backend-pcsc skips, without saying so, a reader it cannot connect to: a key that GnuPG holds would read as no key at all. A sharing violation counts as a key held by another program. One transaction serves one operation; the connection is then dropped, which resets the card: built.
  • What a card says without its PIN: its identifier, holder and URL; the fingerprints, dates, algorithms, touch policies and public parts of its two keys; its three retry counters; whether it asks for the PIN at each signature; its signature counter: built. sioul pgp cards prints it, for diagnosis (crates/sioul-cli/src/pgp.rs).
  • Signing at Send, opening on a click: a message the card signs is built and signed in the writing window when Send is pressed (its PIN, maybe a touch), before the ten seconds of "Undo", which throws the signed copy away; a message encrypted to the card opens only on Open with your security key: built (crates/sioul-sync/src/send.rs, build_draft and send_built; crates/sioul-app/src/mail.rs, Work::SendSigned; client.md).
  • Not yet tried with a real key: everything above the transport is tested against SoftCard, a software key behind the same traits; the transport itself has a unit test of its status words, and a test that reads a real key without its PIN is ignored in every run, to be run by hand with a key plugged in (client.md, "As built").

2. Through GnuPG, or Sequoia's key store

2.1 sequoia-gpg-agent: asking GnuPG's agent to sign and decrypt

  • What it is: 0.6.2 (13 November 2025; a 0.7.0-pqc.1 pre-release beside it), LGPL-2.0-or-later, the same licence as Sequoia (Sequoia-PGP, https://gitlab.com/sequoia-pgp/sequoia-gpg-agent). It speaks Assuan to gpg-agent directly, not through gpg. Its KeyPair implements Sequoia's crypto::Signer and crypto::Decryptor (synchronous wrappers that start a one-thread tokio runtime per operation), so it would slot in exactly where a card's signer would. Agent::card_info() sends LEARN --sendinfo and returns what the agent knows of the connected card (fingerprints, serial).
  • What it needs: GnuPG installed and its agent running (Linux packages; Gpg4win on Windows; GPG Suite or Homebrew on macOS); the card's keys known to the agent (the "stubs" that gpg --card-status writes once into private-keys-v1.d); and still a certificate from elsewhere (section 3). In a Flatpak, --socket=gpg-agent (Flatpak 1.14, 22 August 2022) passes the host's agent socket in. Nothing of it exists on Android.
  • The PIN: asked by gpg-agent's own pinentry window (pinentry-qt, pinentry-gnome3…), not by the application; or by "loopback" (KeyPair::with_password), where the application asks and hands it over. The crate's documentation says loopback "requires explicit opt-in in the gpg-agent configuration", but GnuPG's own manual says of --allow-loopback-pinentry: "Allow is the default" (GnuPG, doc/gpg-agent.texi, read 7 October 2026, https://github.com/gpg/gnupg/blob/master/doc/gpg-agent.texi). gpg-agent and scdaemon keep the card unlocked for the session, as they do for gpg.
  • For someone who already uses gpg with a security key, nothing changes and nothing conflicts: GnuPG stays the card's only user, and the application is one more of its clients. That is its strength. Its weaknesses: GnuPG becomes a requirement on three systems (and is impossible on Android); the PIN window is not the application's (no calm wording, no sentence of its own on the tries left); and errors come back as Assuan codes.

2.2 sequoia-keystore and its openpgp-card back end

  • What it is: sequoia-keystore 0.7.2 (10 February 2026) and sequoia-keystore-openpgp-card 0.2.2 (1 October 2026), LGPL-2.0-or-later (Sequoia-PGP, https://gitlab.com/sequoia-pgp/sequoia-keystore). "Sequoia's key store is a service… The key store is a server. It normally lives in a separate process, but it may be co-located with the application" (its crate documentation). Its back ends: soft keys and gpg-agent by default, openpgp-card and TPM as optional features; requests travel over Cap'n Proto (sequoia-ipc).
  • The card back end (source read) lists cards over PC/SC (card-backend-pcsc, shared mode); keeps a card's key only when its certificate is in the shared certificate directory (openpgp-cert-d, the pgp.cert.d that sq uses), since "OpenPGP cards don't store the OpenPGP key data structures nor do they (always) have all the information required to reconstruct them"; caches the PIN in memory once given (Password) and verifies it again before each operation; returns "Enter PIN and touch the OpenPGP card to unlock the … key" as its prompt when touch is on; and never reads the card's state with check_user_verified, since "on other cards that increase the pin's error count, which is a bit of a disaster".
  • For Sioul, it would bring a second process (or a co-located server), Cap'n Proto, tokio, and the sq home (~/.local/share/sequoia) shared with Sequoia's command-line tools; and nothing on Android. Its value is as the reference implementation of the Sequoia glue (section 1.1) and of the PIN rules above, not as a dependency.

2.3 GnuPG's scdaemon and other programs

  • How scdaemon reaches the card: either through its own CCID driver (libusb: it claims the USB device, and pcscd, with every PC/SC program, then sees no card), or through PC/SC (pcscd), where it connects exclusively by default. GnuPG 2.3 and later dropped the fallback from one to the other: "In GnuPG 2.2, there was a fallback mechanism from CCID driver to PC/SC driver… To support of multiple tokens/readers at the same time, fallback mechanism was removed" (GnuPG, doc/scdaemon.texi, option --disable-ccid, https://github.com/gpg/gnupg/blob/master/doc/scdaemon.texi).
  • It keeps the card after use: "GnuPG keeps lock on yubikey… the yubikey is 'grabbed' lazy as part of the initialization… but then it's not actively released" (GnuPG's forum, thread "GnuPG keeps lock on yubikey", 30 September to 3 October 2025, https://forum.gnupg.org/t/gnupg-keeps-lock-on-yubikey/6826); GnuPG's answer there was to use its integrated CCID driver, not pcscd. Other programs then get a "sharing violation" (0x8010000B; GnuPG's bug tracker, T3267, https://dev.gnupg.org/T3267).
  • The settings (~/.gnupg/scdaemon.conf, then gpgconf --kill scdaemon):
    • disable-ccid: scdaemon goes through pcscd instead of claiming the USB device. Needed where GnuPG is built with its CCID driver (Debian and Ubuntu: scdaemon depends on libusb-1.0-0, https://packages.debian.org/trixie/scdaemon). Not needed on Fedora, which builds GnuPG with --disable-ccid-driver (Fedora's gnupg2.spec, https://src.fedoraproject.org/rpms/gnupg2/blob/rawhide/f/gnupg2.spec); Fedora 44's gnupg2-scdaemon package requires no libusb.
    • pcsc-shared (GnuPG 2.2.28 and later; it began as a GPGTools patch, which Red Hat also carried in 2021 as gnupg-2.2.27-shared.patch; Fedora 44 carries no such patch, so its default is exclusive): scdaemon connects in shared mode, so that gpg and another program can both use the card. Nitrokey's guide for Fedora: "The scdaemon by default expects exclusive access to pcscd… which causes access to fail if other clients, usually web browsers, accessed the card, even if only for reading", and "Shared access can be enabled with echo "pcsc-shared" >> ~/.gnupg/scdaemon.conf (in GnuPG version 2.2.28 or higher)" (Nitrokey, "Fedora GnuPG configuration", https://docs.nitrokey.com/nitrokey3/linux/fedora-gnupg-configuration). GnuPG's manual warns: "This is a somewhat dangerous option because Scdaemon assumes exclusive access to the card and for example caches certain information from the card. Use this option only if you know what you are doing." The information cached is the card's state (PIN verified, data objects), which another program's actions can make stale (a PIN verified by that program, a reset).
    • With neither: gpgconf --kill scdaemon (or gpg-connect-agent "SCD KILLSCD" /bye) frees the card at once; gpg-agent starts scdaemon again the next time gpg needs the card, and the PIN is asked for again then.
  • In practice, without pcsc-shared, scdaemon going through pcscd holds the key exclusively from the first gpg operation (a signature, ssh through gpg-agent, a signed git commit) until the key is pulled out or scdaemon stops, and any other program is refused the card. The reverse holds too: while another program holds a connection, scdaemon's exclusive connection fails, so a program that shares the key with GnuPG must close the card after every operation, never keep it open.

What Sioul does

  • Neither GnuPG nor Sequoia's key store: Sioul talks to the card itself (section 1): built.
  • GnuPG's hold, said plainly: on a sharing violation, Sioul says "Another program holds your security key for itself: GnuPG, most likely." and offers Let GnuPG release it, which runs gpgconf --kill scdaemon on that click only, never by itself. gpgconf is looked for on the PATH, then where GnuPG's installers put it on macOS (Homebrew, GPG Suite) and Windows (Gpg4win); in its Flatpak, Sioul cannot reach the host's GnuPG, and says so, with the command to run in a terminal: built (crates/sioul-sync/src/securitykey.rs). Sioul never changes GnuPG's settings: the lasting fix, pcsc-shared in scdaemon.conf, is documented in client.md ("Your keys on a security key").

3. The certificate, which the card does not hold

3.1 What a card holds, and what it does not

A card holds three private keys (in its signature, decryption and authentication slots) and, for each, its fingerprint (data object C5), its creation time (CD) and, on request, its raw public key material (GENERATE ASYMMETRIC KEY PAIR in reading mode); besides, a URL "for the public key" (5F50), the cardholder's name, login data and the signature counter. It holds no certificate: no user ID, no binding signature, no expiry, no revocation. "OpenPGP cards don't store the OpenPGP key data structures nor do they (always) have all the information required to reconstruct them (in particular, it's missing the ECC algorithm parameters)" (sequoia-keystore-openpgp-card 0.2.2, src/lib.rs). "Fetching the key from the card" therefore means two things: the private operations, done by the card while it is there; and the certificate (the public key, the part others see), which the application must find once and keep, as GnuPG keeps it in its keyring after gpg --card-edit → fetch.

3.2 Where the certificate can be found, in this order

  1. Already here: a certificate whose keys carry the card's fingerprints (imported before, or received in an Autocrypt header).
  2. The card's URL, when one was set (gpg --card-edit → url; often https://keys.openpgp.org/vks/v1/by-fingerprint/<FPR>, or an address of the holder's own). It is read without the PIN, and fetched over HTTPS only.
  3. The Web Key Directory of the account's address: it works for addresses at a domain that publishes one; not for gmail.com (Google publishes none).
  4. keys.openpgp.org by fingerprint: GET /vks/v1/by-fingerprint/<FINGERPRINT> (the fingerprint "MUST be uppercase, and MUST NOT be prefixed with 0x"; five requests a second) (keys.openpgp.org, "API", https://keys.openpgp.org/about/api). "Non-identity information will still be distributed for all keys" (its FAQ, https://keys.openpgp.org/about/faq): when the holder has not confirmed their address there, the certificate comes without user IDs, which is enough to sign and decrypt but not to name the address; Autocrypt needs the user ID.
  5. A file, from gpg --export --armor <fingerprint>: the public part only; nothing secret leaves GnuPG.

Each lookup tells a server that someone at this address wants this key: by Sioul's own rule for lookups ("only when you ask"), steps 2 to 4 run when the person asks, never by themselves.

3.3 The check: does this certificate belong to this card?

  1. Read the card: its identifier (manufacturer and serial), the fingerprints of its signature and decryption slots, key_information() (which slots hold a key), and the public key material of those two slots.
  2. In the certificate, valid under Sequoia's standard policy, the key whose fingerprint is the signature slot's must be alive, not revoked, and able to sign; the one whose fingerprint is the decryption slot's must be alive, not revoked, and able to encrypt (for transport). A fingerprint on a card is only data written by whoever loaded it: each key's public material is also compared with what the card gives (for EdDSA and Cv25519, Sequoia's point carries a 0x40 prefix that the card's does not).
  3. The certificate should name the account's address in a valid user ID. Without one, the key can still sign and decrypt, but others will not find it by this address, and Autocrypt cannot announce it.
  4. After the first signature, the signature is verified with the certificate before the message leaves (one verification, a few milliseconds): a card that signs with another key never sends a broken signature.

3.4 When the card's keys are subkeys of a primary key kept offline

It makes no difference to signing and decrypting. The certificate carries the primary's public key, the user IDs and the binding signatures the primary made once (with, for a signing subkey, the subkey's own back-signature); the card's signing subkey signs (the signature names the subkey as its issuer, and readers check its binding), and its decryption subkey decrypts. What the card cannot do then is certify: extend the expiry, add a user ID, revoke. That is done where the primary key lives (GnuPG on an offline machine), and the application only needs the new certificate afterwards, fetched again or imported. The other common layout, keys generated on the YubiKey with GnuPG (gpg --card-edit → generate), puts the primary key in the signature slot (certifying and signing) and the encryption subkey in the decryption slot: the same check, the same use.

Expiry: an expired certificate stops signing, and stops others from encrypting to its holder. A program can say so a month ahead, and fetch the certificate again on request.

What Sioul does

  • The certificate looked for only on request, at setup and with "Look for a newer version": the URL written on the card (HTTPS only: an http: address is not used), then the Web Key Directory of the person's addresses, then keys.openpgp.org by the signing key's fingerprint; or "Import a file…", from gpg --export --armor. Each certificate found is checked, and the first that belongs to the card is kept: built (crates/sioul-sync/src/securitykey.rs).
  • The check (securitykey::check): each key on the card is in the certificate, not revoked, allowed to do what its slot does, and its public part is the card's (Curve25519's 0x40 prefix and RSA's leading zeros allowed for); a certificate that is not the card's is never kept, and the reason is said: built (crates/sioul-core/src/securitykey.rs). An expired certificate is still the card's: it is said with its date and the gpg --quick-set-expire commands, and a month ahead; an expired encryption subkey still opens what was sent to it, since Sequoia's standard policy allows it (client.md).
  • A certificate without user IDs (keys.openpgp.org's answer while an address is not confirmed there) is kept, signs for the person's own addresses, and is said to name none: built (crates/sioul-app/src/securitykey.rs).
  • Each signature verified with the certificate's key before it is used: built (CardSigner).
  • What Sioul keeps: the certificate, public parts only, with the person's keys; and, in pgp/cards.toml, the card's identifier, the fingerprints of its two keys, the certificate's fingerprint, the addresses it signs for, where the certificate came from, and when. No secret: built. A card that serves an address signs for it alone, and Autocrypt announces its certificate; a key kept on the computer for the same address only opens older mail.

4. Android: a security key over NFC or USB-C

Android has no PC/SC: no pcscd, no libpcsclite. A program reaches a card through Android's own APIs: NFC (IsoDep, ISO 14443-4) or USB host (raw bulk transfers, with the CCID framing done by the program). Three ways were weighed.

4.1 Yubico's yubikit-android

  • What: Apache-2.0 (compatible with GPL-3.0-or-later), 3.2.1 on 8 September 2026 (3.0.0 December 2025, 3.1.0 March 2026, 3.2.0 July 2026), last push 5 October 2026: maintained (Yubico, https://github.com/Yubico/yubikit-android). Maven com.yubico.yubikit:android and com.yubico.yubikit:openpgp (3.2.1). The android module: minSdk 21, compiled with SDK 37.
  • The transport (android module): YubiKitManager.startUsbDiscovery(new UsbConfiguration(), device -> …), which asks for Android's USB permission itself ("permission will automatically be requested by the YubiKitManager"); startNfcDiscovery(new NfcConfiguration().setTimeout(…), activity, device -> …), which "requires an Activity in the foreground"; device.openConnection(SmartCardConnection.class), then sendAndReceive(apdu). It logs through slf4j, whose trace level "may contain sensitive information".
  • The OpenPGP module: OpenPgpSession, with getPinStatus() (tries left), verifyUserPin(pin, extended), sign(payload), decrypt(payload) (RSA) and decrypt(peerPublicKey) (ECDH), getUif(keyRef) (touch), getSignatureCounter(), getKdf(), and administration (keys, PINs, attestation). It returns the card's raw results: the OpenPGP packets (signatures, session keys) would still be Sequoia's work in Rust, and the protocol would then exist twice, in Java on Android and in Rust on computers.
  • Cost: a Gradle dependency. Qt for Android builds with its own build.gradle template; adding a dependency means copying that template into the application's package folder, and keeping it in step with each Qt version. It knows YubiKeys only.

4.2 OpenKeychain and its OpenPGP API

  • Status: OpenKeychain's README, read 7 October 2026: "WARNING: This software is no longer actively developed. We will still apply security fixes where reported, and do basic maintenance work, but no new features or will be worked on." Its last commit is of 16 May 2024 (version 6.0.4, February 2024); GPL-3.0 (https://github.com/open-keychain/open-keychain). Thunderbird for Android still relies on it for OpenPGP: "using OpenPGP email encryption (PGP/MIME) with the “OpenKeychain” app" (Thunderbird for Android, its store description, https://github.com/thunderbird/thunderbird-android/blob/main/app-metadata/net.thunderbird.android/en-US/full_description.txt).
  • How an app calls it: through the openpgp-api library (Apache-2.0, version 12, last push August 2024; com.github.open-keychain.open-keychain:openpgp-api from JitPack; https://github.com/open-keychain/openpgp-api). The app binds OpenKeychain's service (AIDL IOpenPgpService, with one method: Intent execute(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output)), with actions such as ACTION_DETACHED_SIGN, ACTION_SIGN_AND_ENCRYPT and ACTION_DECRYPT_VERIFY. When the person must act (a PIN, an NFC tap, the first "allow this app" screen), the result carries a PendingIntent that the app starts, and OpenKeychain shows its own screens.
  • What the person must do: install OpenKeychain (F-Droid or Play), import their public key there and link the security key to it, then allow the app once in OpenKeychain. The app would then hand OpenKeychain whole messages, which it signs and encrypts itself: Sioul's PGP/MIME code would be bypassed on Android, and the PIN window would be OpenKeychain's.
  • Verdict: an extra app, unmaintained, and a second OpenPGP implementation in the loop: not for Sioul.

4.3 The card protocol spoken from Rust

  • The idea: the same Rust code as on computers (openpgp-card 0.7 and the Sequoia glue), with another CardBackend, whose transmit(apdu) crosses JNI to Java, which hands the bytes to IsoDep.transceive (NFC) or to a CCID bulk transfer (USB) and returns the answer. The two traits (section 1.3) are the whole interface; openpgp-card does the chaining, extended lengths, KDF and the OpenPGP commands.
  • It has been done: libtumpa (Kushal Das, GPL-3.0-or-later, 0.4.5 on 23 July 2026, https://github.com/tumpaproject/libtumpa, src/card/mobile.rs) implements card_backend::CardBackend as a MobileCardBackend, over a synchronous CardBridge trait (begin_session, transmit_apdu, end_session): "vendor-neutral — the bridge speaks generic ISO 7816-4 APDUs, so anything that answers SELECT AID D27600012401… (YubiKey, Nitrokey 3/Pro, Gnuk, any OpenPGP v3 card) works". Its Android side (Tumpa, crates/tauri-plugin-tumpa-card/android/, Kotlin, GPL-3.0-or-later, https://github.com/tumpaproject/tumpa): NFC by reader mode (NfcAdapter.enableReaderMode with FLAG_READER_NFC_A | FLAG_READER_NFC_B | FLAG_READER_SKIP_NDEF_CHECK, a presence check every 5 s, and IsoDep.timeout = 15000, so that a touch can finish); USB through UsbManager, with its own CCID framing (UsbCcidConnection.kt, 588 lines: IccPowerOn, XfrBlock, IccPowerOff, time extensions); a USB device filter on class 0x0B (any CCID reader); uses-feature NFC and USB host with required="false".
  • Two lessons to take: keep one physical session for several logical ones, or the person taps three times for one operation (Tumpa's long comment, "Session lifetime — IMPORTANT"); and do the USB input and output on a worker thread, never on Android's main thread. Two mistakes not to copy: Tumpa's USB code logs every APDU in hexadecimal (Log.d(TAG, "transmit apdu=…")), VERIFY included, so that the PIN (or, with KDF, its hash, which unlocks the card as well) reaches logcat; and it keeps every successful VERIFY APDU, to replay after a card reset. A transport for Sioul would log no APDU and keep no VERIFY.
  • The phone's NFC stack: on /e/OS, OpenKeychain and a YubiKey 5 NFC failed with "transceive length exceeds supported maximum": that system's NFC stack did not support extended-length APDUs (/e/OS's issue tracker, issue 613, on a Samsung S9+, https://gitlab.e.foundation/e/backlog/-/issues/613). A transport can report IsoDep.isExtendedLengthApduSupported() and getMaxTransceiveLength() to openpgp-card (limit_card_caps, max_cmd_len), which then chains its commands. Curve25519 keys fit short APDUs anyway; RSA 4096 needs chaining. Not tried.
  • Touch over NFC: "The YubiKey button and LED are not enabled in contactless mode" (Yubico's YubiKey manual 3.4, §7.7, as quoted in Yubico's forum, thread "[QUESTION] capture OTP via NFC", February 2016, in an archive: https://forum.yubico.com/viewtopic5a3c.html?p=8305): the key cannot be touched while it lies on the phone. Whether a YubiKey 5 then refuses an operation whose touch policy is on, or takes the tap as presence, was not found in Yubico's documentation (unverified): to be tried with a real key before anything is promised.
  • Cost: about 150 lines of Java for NFC (reader mode, IsoDep) and 350 for USB CCID (or yubikit's transport); the JNI bridge, in Sioul's usual way (Java static native methods, extern "C" Rust functions); and the Rust back end (about 100 lines, as libtumpa's). No new app for the person, no Gradle dependency.

4.4 Which fits Sioul

The third way: no app to install, one implementation of the OpenPGP card protocol, in Rust, for every system, Sioul's own words and PIN field, and Sioul's PGP/MIME unchanged. NFC first (one tap per operation), USB-C second. yubikit's transport stays a fallback, if writing the CCID framing costs more than expected.

4.5 What Qt for Android needs

  • The manifest: <uses-permission android:name="android.permission.NFC"/> (a normal permission, granted at install), <uses-feature android:name="android.hardware.nfc" android:required="false"/> and <uses-feature android:name="android.hardware.usb.host" android:required="false"/> (else phones without them could not install the app).
  • NFC by reader mode, not by foreground dispatch: reader mode is enabled on the app's activity (Qt's QtActivity) only while a key is asked for, and disabled after; it delivers the tag to a callback, with no intent to route through Qt's onNewIntent; and FLAG_READER_SKIP_NDEF_CHECK keeps Android from reading the YubiKey's NDEF record (its one-time-password link, which otherwise opens the browser at a tap) (Android, NfcAdapter, https://developer.android.com/reference/android/nfc/NfcAdapter; IsoDep, https://developer.android.com/reference/android/nfc/tech/IsoDep). The activity must be in the foreground: the key is only asked for from the window. Qt's own NFC module is built around NDEF messages (Qt, "Qt NFC on Android", https://doc.qt.io/qt-6/nfc-android.html): not needed.
  • USB: the key is found with UsbManager.getDeviceList() (interface class 0x0B); permission is asked with UsbManager.requestPermission(device, pendingIntent) (Android's dialog, once per plug, or "always" when the app also declares a USB_DEVICE_ATTACHED filter, best left out so that the app does not open itself whenever the key is plugged in); the answer is read back with hasPermission(device), in a broadcast receiver registered as not exported; and transfers run apart from the window: "you should create a new thread on which to carry out all data transmissions, so you don't block the UI thread" (Android, "USB host overview", https://developer.android.com/develop/connectivity/usb/host).
  • The build: card-backend-pcsc (and so pcsc-sys, which looks for libpcsclite with pkg-config on every system but Windows and macOS) only for computer targets, [target.'cfg(not(target_os = "android"))'.dependencies]. jni 0.21 and 0.22 are already in Sioul's lock file.

What Sioul does

Not built on 8 October 2026: on a phone, Sioul finds no security key (securitykey::readers() gives NoReaders on Android, crates/sioul-sync/src/securitykey.rs); the PC/SC crates are left out of the Android build (crates/sioul-sync/Cargo.toml); and the manifest asks for no NFC or USB permission (android/package/AndroidManifest.xml). Next, once a real key has been tried on a computer: NFC, then USB-C, by the third way (client.md, "As built").

5. Packaging, permissions, tests

5.1 Linux

  • pcscd and the CCID driver must be on the system: Fedora's pcsc-lite and pcsc-lite-ccid; Debian's and Ubuntu's pcscd (it brings libccid); Arch's pcsclite and ccid, then systemctl enable --now pcscd.socket. They are often present (GnuPG on Fedora goes through them: gnupg2.spec builds with --disable-ccid-driver, BuildRequires: pcsc-lite-libs and Recommends: pcsc-lite-ccid), never guaranteed. Without the daemon, the program should say so, with the system's command.
  • Building: pcsc-sys needs libpcsclite and pkg-config at build time (libpcsclite-dev on Debian and Ubuntu); nothing for Windows and macOS; nothing for Android, where the crate is left out (section 4.5).
  • pcsc-lite's protocol: the client library and the daemon exchange a protocol version. pcsc-lite 2.3.0 (3 August 2024) moved it from 4.4 to 4.5 ("Update PROTOCOL_VERSION_MINOR to make sure the client and server are in sync", commit 53f57ed7, https://github.com/LudovicRousseau/PCSC/commit/53f57ed7); 2.4.1 (1 January 2026) added "backward version support on the client side" and "on the server side" (4.4); and 2.5.x (May 2026) moved to 4.6 (src/winscard_msg.h: PROTOCOL_VERSION_MINOR 6, ..._BACKWARD 4). Since 1.8.26, on a mismatch, "SCardEstablishContext() returns SCARD_E_SERVICE_STOPPED" (Ludovic Rousseau, pcsc-lite's ChangeLog, https://github.com/LudovicRousseau/PCSC/blob/master/ChangeLog). The systems in October 2026: Fedora 44, 2.4.1; Ubuntu 22.04, 1.9.5; 24.04, 2.0.3; 25.10, 2.3.3; 26.04, 2.4.1; Debian 12, 1.9.9; Debian 13, 2.3.3; Debian testing, 2.5.2 (Launchpad, https://launchpad.net/ubuntu/+source/pcsc-lite; Debian's sources, https://sources.debian.org/src/pcsc-lite/).
  • Flatpak: --socket=pcsc in finish-args (Flatpak 1.3.2, 12 April 2019: "New permission --socket=pcsc for access to smart cards", https://github.com/flatpak/flatpak/releases/tag/1.3.2). It binds the host's /run/pcscd/pcscd.comm into the sandbox and sets PCSCLITE_CSOCK_NAME (Flatpak, common/flatpak-run-sockets.c, https://github.com/flatpak/flatpak/blob/main/common/flatpak-run-sockets.c). The runtime brings the client library: freedesktop-sdk builds pcsc-lite (elements/components/pcsc-lite.bst, 2.5.2 on its 25.08 branch, under the KDE 6.10 runtime, https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/blob/release/25.08/elements/components/pcsc-lite.bst), a client that falls back to older daemons. --device=all does not help: pcscd owns the reader. For the GnuPG route, --socket=gpg-agent (Flatpak 1.14, https://github.com/flatpak/flatpak/releases/tag/1.14.0).
  • AppImage: linuxdeploy copies libpcsclite.so.1 into the AppImage (it is not on AppImage's exclusion list, https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist), taken from the build image, Ubuntu 22.04: pcsc-lite 1.9.5, protocol 4.4 only. Daemons 2.3.0 to 2.4.0 speak 4.5 without backward support: on Debian 13 and Ubuntu 25.10, such an AppImage would be refused the card. The study's fix: a libpcsclite built from pcsc-lite 2.4.1 or later (whose client falls back to 4.4 and speaks 4.5 and 4.6), built in the AppImage's job (meson, the client library alone). Relying on the system's library instead would stop the program from starting at all on a system without pcsc-lite, since pcsc-sys links it.

5.2 Windows and macOS

  • Windows: winscard.dll and the Smart Card service are part of Windows; the YubiKey's CCID interface uses Windows' own driver. Nothing to install, nothing to change in an installer. Gpg4win's scdaemon goes through the same WinSCard, exclusively by default: the same conflict as on Linux, and the same answer.
  • macOS: the PCSC framework is part of macOS (CryptoTokenKit underneath). Nothing to install.

5.3 Android

  • android.permission.NFC (granted at install); uses-feature NFC and USB host with required="false"; the USB permission asked at run time, through Android's dialog (section 4.5). NFC itself must be on, in Android's settings.

5.4 Tests with a virtual card

  • Sequoia's own recipe (sequoia-keystore, .gitlab-ci.yml, job opcard, https://gitlab.com/sequoia-pgp/sequoia-keystore/-/blob/main/.gitlab-ci.yml): apt-get -y install libpcsclite-dev pcscd vsmartcard-vpcd; git clone --depth=1 https://github.com/Nitrokey/opcard-rs/; cargo build --example vpicc --features rsa2048,rsa4096-gen; /sbin/pcscd --disable-polkit --info --foreground &; target/debug/examples/vpicc &; then the tests. The virtual card answers as 0000:00000000, with the PINs 123456 and 12345678. vsmartcard-vpcd 3.3 is in Ubuntu 24.04 (universe, https://packages.ubuntu.com/noble/vsmartcard-vpcd; vsmartcard is Frank Morgner's, https://github.com/frankmorgner/vsmartcard), so a GitHub Actions job on ubuntu-24.04 runs it as it is. opcard-rs is Nitrokey's OpenPGP card (the Nitrokey 3's), LGPL-3.0-only, 1.11.0 on 17 September 2026: specification 3.4, Curve25519, RSA 2048 to 4096, NIST and brainpool curves (https://github.com/Nitrokey/opcard-rs). It is a test tool beside the program, never linked into it.
  • Other virtual cards: container images of SmartPGP (ANSSI, JavaCard; no Curve25519), opcard-rs and CanoKey, for CI: "The virtual OpenPGP cards in these images offer no meaningful protection for private key material!" (openpgp-card's virtual cards, https://gitlab.com/openpgp-card/virtual-cards). Gnuk, emulated through USBIP, needs root and a kernel module: not for CI.
  • What such tests can cover (a card-tests feature, in the job above): resetting the virtual card and loading a test key with the admin PIN (test-only code: the program itself never administers a card); signing a PGP/MIME message with the card, and checking it with the certificate, and with GnuPG; encrypting to the certificate and opening with the card; a wrong PIN, then the tries left read and said; three wrong PINs, and the locked sentence; the card killed mid-operation (vpicc stopped); a certificate whose fingerprints do not match; a certificate without user IDs; a second program holding the card exclusively (a sharing violation).
  • Android: the Rust half against a mock bridge that records APDUs and replays answers (as libtumpa's MockBridge), fed with a transcript recorded from the virtual card; the Java half by hand, with a real key, over NFC, then USB.

What Sioul does

  • CI: libpcsclite-dev in the Linux jobs that build Sioul: built (.github/workflows/build.yml).
  • The Flatpak: --socket=pcsc, beside --device=all, which security keys on websites need: built (packaging/flatpak/com.aurelienpierre.Sioul.yml).
  • The AppImage: pcsc-lite 2.5.2's client, built from its source in the release job (its archive checked by SHA-256) and linked statically into the programs (PCSC_LIB_DIR, PCSC_LIB_NAME=static=pcsclite), its licence shipped beside: built, one step past the study's advice. The AppImage carries no libpcsclite.so, speaks protocols 4.4 to 4.6, and starts on a system without pcsc-lite too (.github/workflows/release.yml).
  • Windows and macOS: nothing added; the code builds on both in CI; untried with a real key.
  • Tests without hardware: SoftCard, a software OpenPGP key behind the same traits as a card (its tries and its lock, a PIN for each signature, RSA's DigestInfo, Curve25519), drives signing, decrypting (RSA and X25519), the certificate check, the PIN's quiet time and the sentence of every failure: built (crates/sioul-core/src/securitykey.rs, its tests). The lookup of certificates runs against a stand-in server on the computer, and "Let GnuPG release it" against a stand-in gpgconf: built (crates/sioul-sync/src/securitykey.rs). The virtual-card job (opcard-rs through vsmartcard): not built, next (client.md, "As built").

6. Security

  • The PIN: typed in the application's own field (password echo), handed to Rust at once, and held as secrecy::SecretString (what openpgp-card takes; zeroed when dropped) or as Sequoia's Password (kept encrypted in memory). Never logged, never in an error message, never written to disk or to the keyring: a PIN in the keyring would bring back what the card is for, a secret usable from the computer alone. Qt's QString copies of the field's text cannot be zeroed: the field is cleared at once, and the PIN never goes through QML properties that persist. On Android, no APDU is ever to be logged (a VERIFY carries the PIN, or its KDF hash, which unlocks the card as well), and none kept to replay.
  • A trap in openpgp-card 0.7.0 (src/ocard/apdu.rs): its Command prints the PIN as "REDACTED" at the debug level, and a chained command is logged as "hidden due to secret data" (line 179); but a command sent in one piece, which a VERIFY always is, is logged whole at the trace level, log::trace!(" -> APDU command: {:02x?}", serialized) (line 224), and the card's answers, session keys among them, are logged at that level too (lines 186 and 228). The chained case was fixed upstream in May 2024 (pull request 121, "Hide ADPU if it came from secret data"); the maintainers' issue on what gets logged is still open: "Right now, the APDU chunking code still logs PINs at the trace level. We should decide if this is what we want." (openpgp-card, issue 75, 28 May 2024, https://codeberg.org/openpgp-card/openpgp-card/issues/75). A program that installs no log logger writes nothing. To keep it so whatever comes later, the study proposed capping the log crate statically in release builds (log = { features = ["release_max_level_info"] }; the log crate's compile-time filters, https://docs.rs/log/latest/log/), and sending the same "hidden" test for the unchained path upstream.
  • KDF-DO, when it is set on the card (GnuPG: gpg --card-edit, admin, kdf-setup, done before the keys are loaded, since it resets the PINs' form; YubiKey Manager's documented commands have none), means that the PIN never travels in clear over USB or NFC; openpgp-card handles it.
  • The User PIN only: the application asks for the User PIN (PW1) and nothing else; never the Admin PIN (PW3) or the Reset Code. Unblocking, changing PINs, touch policies, loading keys: GnuPG, YubiKey Manager or oct, never the mail program. With the Admin PIN locked too, only a reset of the OpenPGP application is left, which erases its keys (YubiKey Manager, ykman openpgp reset): one more reason never to ask for it.
  • "Force signature PIN" (the card's PW1 status byte; gpg --card-edit → forcesig; ykman openpgp access set-signature-policy always|once): when it is forced, a verified PIN allows one signature only, so that each signature is a deliberate act; a program should then ask for the PIN each time, rather than replay a PIN it holds behind the card's back.
  • Touch (On or Cached, for the signing and decryption keys) is what stops a program on the computer from using the key while it is plugged in and its PIN held: every signature or decryption needs a finger on the key. The policy is set with YubiKey Manager; the application reads it and says "touch your key" at the right moment.
  • Someone with the computer or the phone, without the key, cannot sign as the key's holder nor open mail encrypted to them from then on, and cannot copy the private key, which never leaves the card. They can still read what is stored in clear (mail that came unencrypted; the decrypted copy of an encrypted message kept with the draft of an answer; any decrypted file saved); send unsigned mail from the accounts (their passwords are in the keyring); and replace a certificate on disk (the check of each signature against the card catches it, for the holder's own key). A program left running in the session can wait for the next time the key is plugged in and its PIN typed: touch is the defence.
  • Compared with a key kept on the computer: a secret key file, with its passphrase in the keyring, lets any program running as the person sign and decrypt silently, and copy the key for ever. With the card, the key cannot be copied, and with touch it cannot be used silently.
  • Someone with the key, without the PIN, has three tries; then the key is locked (User PIN); then the Admin PIN's three tries; then only a reset. With the key and a PIN watched over a shoulder, they have the holder's signature until the key is revoked: the revocation certificate belongs offline.
  • An off-card backup: when GnuPG generates keys on a card and the person accepts an off-card backup, it writes the encryption key, protected by the passphrase chosen then, to sk_<key ID>.gpg in GnuPG's home folder ("Note: backup of card key saved to '%s'"; card_write_key_to_backup_file, "Write the protected secret key to the file"; GnuPG, g10/keygen.c, https://github.com/gpg/gnupg/blob/master/g10/keygen.c). Such a backup keeps old mail readable if the card is lost; it also means that the decryption key does not live only in the card: anyone with the session and that passphrase can decrypt without the card. Its place is offline, with the revocation certificate.
  • A certificate fetched cannot be forged into one that matches the card (the fingerprints and public keys are checked, and binding signatures need the primary key); an older copy could be served, one without a later revocation or extension: it is fetched over HTTPS only, from the places the person chose.

What Sioul does

  • The PIN: typed in Sioul's own field (qml/SecurityKeyBand.qml), emptied at once, and held in Rust as Sequoia's Password, encrypted in memory, for 15 minutes after its last use. It is forgotten when its key's reader goes (watched while a PIN is held), on Forget the PIN now, and when Sioul closes; a PIN the key refused is never given again. Never written anywhere, never logged: built (crates/sioul-core/src/securitykey.rs, PinMemory; crates/sioul-app/src/securitykey.rs). Of the study's three choices (each time, while Sioul is open, for a quiet time), the quiet time was chosen.
  • A key that asks for its PIN at each signature is given it each time, even while Sioul holds it for decrypting: built.
  • Touch: each key's touch policy is read without the PIN, and "Touch your security key: it blinks." is said just before the key waits. The status word 6600, and 6982 just after the PIN was taken by a key that asks for a touch, are said as a touch missed (6982's meaning still to be confirmed on a YubiKey): built (crates/sioul-sync/src/securitykey/smartcard.rs).
  • The User PIN only: Sioul never asks for the Admin PIN or the Reset Code, never changes or unblocks a PIN, never loads or makes keys; its code holds no such call: built, as a rule.
  • Logs: instead of the static cap, each program caps the log level at "info" as it starts (securitykey::cap_logging), never raising it; Sioul installs no logger, and no crate it builds with installs one; a test holds all three: built (crates/sioul-sync/src/securitykey.rs). The static cap: not built. The fix for the unchained path upstream: not sent.
  • Session keys opened by the card stay in memory until Sioul closes (pgp::SessionKeys, by the SHA-256 of the encrypted bytes), so that a message, its attachments and the quote kept with an answer open again without the key; never on disk: built (crates/sioul-core/src/pgp.rs). Nothing touches the key because a message is shown.
  • The decrypted copy kept with an answer's draft is written in the drafts folder, as the person's own text is (protect_answer, crates/sioul-app/src/crypto.rs): readable by someone with the computer, as said above.

7. Recommendation per platform

The private key never comes out of the card, not even for a moment: the program hands the card what must be signed (a hash) or opened (a wrapped session key), and gets the result back. What it keeps on the device is the public part, which everyone may have anyway (Sioul puts it in the Autocrypt header of every message it sends from an address that has a key). Nothing secret is imported or stored.

  • Linux: Sioul talks to the card itself through PC/SC: openpgp-card 0.7 and card-backend-pcsc 0.5 (MIT OR Apache-2.0), in shared mode, one operation at a time, with Sioul's own Sequoia signer and decryptor (after Sequoia's key store, about 150 lines). The Flatpak gets --socket=pcsc, the AppImage a recent pcsc-lite client. GnuPG's exclusive hold on the key is said plainly, with a button that releases it; pcsc-shared in GnuPG's settings is the lasting fix. Not sequoia-gpg-agent: it would make GnuPG a requirement, its PIN window would not be Sioul's, and it does not exist on Android. Not sequoia-keystore: a server, Cap'n Proto and the sq home, for what 150 lines do.
  • Windows and macOS: the same code. The system's PC/SC (WinSCard, the PCSC framework) is built in, and nothing is to be installed. To be tested with the virtual card on Linux; on Windows and macOS, built by CI, and untried with a real key until someone has one there.
  • Android: Sioul speaks the card protocol itself, with the same Rust as on computers, over a small Java transport: NFC first (reader mode, IsoDep), USB-C second (CCID over Android's USB host API). No app to install. Not yubikit-android's OpenPGP module (the protocol twice, a Gradle dependency, YubiKeys only); not OpenKeychain (an extra app, "no longer actively developed").

What Sioul does

Linux Windows macOS Android
The card through the system's smart card service built: native, Flatpak, AppImage built built not built; NFC, then USB-C, next
Signing at Send, opening on a click, the PIN and touch built built built not built
"Let GnuPG release it" built; in the Flatpak, GnuPG said to be out of reach built (Gpg4win) built (Homebrew, GPG Suite) —
Tried with a real key not yet not yet not yet not yet

"Not yet" follows client.md ("As built"), which records the tests run: a software key on 7 October 2026, the window's states on a demo profile, and no real key.