Skip to content

feat: take key flags and expiration into account when selecting the key - #8744

Merged
link2xt merged 2 commits into
mainfrom
link2xt/key-selection
Oct 10, 2026
Merged

link2xt merged 2 commits into
mainfrom
link2xt/key-selection

Conversation

@link2xt

@link2xt link2xt commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #6520 (key selection is now based on Autocrypt 2 spec, we select not expired subkey with the closest expiration date and also make sure it is marked with a flag that says the key can be used to encrypt communications).

The scripts for test key generation are in the commit message. I decided to use GnuPG for this (instead of e.g. generated with rPGP or PGPy) because it is the most obvious command line tool that can generate this sort of complex keys (sop cannot be used to add RSA subkey to ECC key in arbitrary ways i think, have not tried sq). Keys with authentication flags etc. typically came from GnuPG before anyway.

If you want to look at the keys, maybe use sq inspect (it seems to reorder expired subkeys to the end however), rpacket dump, or here is the gpg output for the first test so you don't have to run it yourself:

gpg --list-packets output
$ gpg --list-packets alice.tpk.asc
# off=0 ctb=98 tag=6 hlen=2 plen=51
:public key packet:
	version 4, algo 22, created 1190648892, expires 0
	pkey[0]: [80 bits] ed25519 (1.3.6.1.4.1.11591.15.1)
	pkey[1]: [263 bits]
	keyid: 272A026B34145A22
# off=53 ctb=b4 tag=13 hlen=2 plen=25
:user ID packet: "Alice <alice@example.org>"
# off=80 ctb=88 tag=2 hlen=2 plen=144
:signature packet: algo 22, keyid 272A026B34145A22
	version 4, created 1190648892, md5len 0, sigclass 0x13
	digest algo 10, begin of digest 50 61
	hashed subpkt 33 len 21 (issuer fpr v4 28B5F58AAFBA0555921C8782272A026B34145A22)
	hashed subpkt 2 len 4 (sig created 2007-09-24)
	hashed subpkt 27 len 1 (key flags: 03)
	hashed subpkt 11 len 4 (pref-sym-algos: 9 8 7 2)
	hashed subpkt 21 len 5 (pref-hash-algos: 10 9 8 11 2)
	hashed subpkt 22 len 3 (pref-zip-algos: 2 3 1)
	hashed subpkt 30 len 1 (features: 01)
	hashed subpkt 23 len 1 (keyserver preferences: 80)
	subpkt 16 len 8 (issuer key ID 272A026B34145A22)
	data: [255 bits]
	data: [254 bits]
# off=226 ctb=b9 tag=14 hlen=3 plen=397
:public sub key packet:
	version 4, algo 1, created 1190648892, expires 0
	pkey[0]: [3072 bits]
	pkey[1]: [17 bits]
	keyid: 92E762B9084CA740
# off=626 ctb=88 tag=2 hlen=2 plen=120
:signature packet: algo 22, keyid 272A026B34145A22
	version 4, created 1190648892, md5len 0, sigclass 0x18
	digest algo 10, begin of digest 08 e5
	hashed subpkt 33 len 21 (issuer fpr v4 28B5F58AAFBA0555921C8782272A026B34145A22)
	hashed subpkt 2 len 4 (sig created 2007-09-24)
	hashed subpkt 27 len 1 (key flags: 20)
	subpkt 16 len 8 (issuer key ID 272A026B34145A22)
	data: [256 bits]
	data: [256 bits]
# off=748 ctb=b8 tag=14 hlen=2 plen=56
:public sub key packet:
	version 4, algo 18, created 1190648892, expires 0
	pkey[0]: [88 bits] cv25519 (1.3.6.1.4.1.3029.1.5.1)
	pkey[1]: [263 bits]
	pkey[2]: [32 bits]
	keyid: C8F382BD0F35C49E
# off=806 ctb=88 tag=2 hlen=2 plen=126
:signature packet: algo 22, keyid 272A026B34145A22
	version 4, created 1190648892, md5len 0, sigclass 0x18
	digest algo 10, begin of digest 53 91
	hashed subpkt 33 len 21 (issuer fpr v4 28B5F58AAFBA0555921C8782272A026B34145A22)
	hashed subpkt 2 len 4 (sig created 2007-09-24)
	hashed subpkt 27 len 1 (key flags: 0C)
	hashed subpkt 9 len 4 (key expires after 1d0h0m)
	subpkt 16 len 8 (issuer key ID 272A026B34145A22)
	data: [255 bits]
	data: [256 bits]
# off=934 ctb=b8 tag=14 hlen=2 plen=51
:public sub key packet:
	version 4, algo 22, created 1190648892, expires 0
	pkey[0]: [80 bits] ed25519 (1.3.6.1.4.1.11591.15.1)
	pkey[1]: [263 bits]
	keyid: F177AC3118F923CC
# off=987 ctb=88 tag=2 hlen=2 plen=239
:signature packet: algo 22, keyid 272A026B34145A22
	version 4, created 1190648892, md5len 0, sigclass 0x18
	digest algo 10, begin of digest c9 e4
	hashed subpkt 33 len 21 (issuer fpr v4 28B5F58AAFBA0555921C8782272A026B34145A22)
	hashed subpkt 2 len 4 (sig created 2007-09-24)
	hashed subpkt 27 len 1 (key flags: 02)
	subpkt 16 len 8 (issuer key ID 272A026B34145A22)
	subpkt 32 len 117 (signature: v4, class 0x19, algo 22, digest algo 10)
	data: [256 bits]
	data: [252 bits]
# off=1228 ctb=b8 tag=14 hlen=2 plen=56
:public sub key packet:
	version 4, algo 18, created 1190648892, expires 0
	pkey[0]: [88 bits] cv25519 (1.3.6.1.4.1.3029.1.5.1)
	pkey[1]: [263 bits]
	pkey[2]: [32 bits]
	keyid: 36188C6FFC8E267B
# off=1286 ctb=88 tag=2 hlen=2 plen=120
:signature packet: algo 22, keyid 272A026B34145A22
	version 4, created 1190648892, md5len 0, sigclass 0x18
	digest algo 10, begin of digest f3 3f
	hashed subpkt 33 len 21 (issuer fpr v4 28B5F58AAFBA0555921C8782272A026B34145A22)
	hashed subpkt 2 len 4 (sig created 2007-09-24)
	hashed subpkt 27 len 1 (key flags: 0C)
	subpkt 16 len 8 (issuer key ID 272A026B34145A22)
	data: [256 bits]
	data: [255 bits]
# off=1408 ctb=b8 tag=14 hlen=2 plen=56
:public sub key packet:
	version 4, algo 18, created 1190648892, expires 0
	pkey[0]: [88 bits] cv25519 (1.3.6.1.4.1.3029.1.5.1)
	pkey[1]: [263 bits]
	pkey[2]: [32 bits]
	keyid: 9223FCEE7546CDE7
# off=1466 ctb=88 tag=2 hlen=2 plen=126
:signature packet: algo 22, keyid 272A026B34145A22
	version 4, created 1190648892, md5len 0, sigclass 0x18
	digest algo 10, begin of digest bd 02
	hashed subpkt 33 len 21 (issuer fpr v4 28B5F58AAFBA0555921C8782272A026B34145A22)
	hashed subpkt 2 len 4 (sig created 2007-09-24)
	hashed subpkt 27 len 1 (key flags: 0C)
	hashed subpkt 9 len 4 (key expires after 1y0d0h0m)
	subpkt 16 len 8 (issuer key ID 272A026B34145A22)
	data: [255 bits]
	data: [254 bits]
# off=1594 ctb=b8 tag=14 hlen=2 plen=56
:public sub key packet:
	version 4, algo 18, created 1190648892, expires 0
	pkey[0]: [88 bits] cv25519 (1.3.6.1.4.1.3029.1.5.1)
	pkey[1]: [263 bits]
	pkey[2]: [32 bits]
	keyid: FD2C0567967223D8
# off=1652 ctb=88 tag=2 hlen=2 plen=120
:signature packet: algo 22, keyid 272A026B34145A22
	version 4, created 1190648892, md5len 0, sigclass 0x18
	digest algo 10, begin of digest ad 23
	hashed subpkt 33 len 21 (issuer fpr v4 28B5F58AAFBA0555921C8782272A026B34145A22)
	hashed subpkt 2 len 4 (sig created 2007-09-24)
	hashed subpkt 27 len 1 (key flags: 0C)
	subpkt 16 len 8 (issuer key ID 272A026B34145A22)
	data: [255 bits]
	data: [256 bits]

@link2xt
link2xt added this pull request to stack #8745 September 24, 2026 11:49
@link2xt
link2xt force-pushed the link2xt/key-selection branch 2 times, most recently from beec8b2 to 7be0f1b Compare September 25, 2026 18:29
@link2xt
link2xt marked this pull request as ready for review September 25, 2026 18:30
@link2xt
link2xt force-pushed the link2xt/key-selection branch from 7be0f1b to 8acd034 Compare September 25, 2026 18:32
@link2xt

link2xt commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator Author

I added one test with many subkeys. More things can be tested, e.g. that out of multiple expiring subkeys the one that has closest expiration is date is selected, but this can be postponed for end-to-end autocrypt 2 tests with messages being sent.

@link2xt
link2xt force-pushed the link2xt/key-selection branch from 8acd034 to 715030b Compare September 25, 2026 18:39
@link2xt
link2xt requested a review from Hocuri September 25, 2026 18:42
Comment thread src/pgp/pgp_tests.rs
// Key 4 is the one that should be selected.

// Subkey 4 fingerprint.
let expected_fallback_fingerprint = "cdeb3ba3999bf7880f0ee1f536188c6ffc8e267b";

@link2xt link2xt Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the comment above hex numbers are Key IDs, they correspond to the end of fingerprints,
e.g. for this fingerprint cdeb3ba3999bf7880f0ee1f536188c6ffc8e267b key id is 36188C6FFC8E267B.

Key IDs are really deprecated and should not be used, but GnuPG prints them in --list-packets and in this case they are good enough for referring to the subkeys.

Comment thread src/pgp.rs
Comment thread src/pgp.rs
Comment thread src/pgp/pgp_tests.rs Outdated
Comment thread src/pgp.rs
.find(|subkey| subkey.algorithm().can_encrypt())
.filter(|subkey| subkey.algorithm().can_encrypt())
.filter_map(|subkey| {
let signature = subkey.signatures.first()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only ever looks at the first signature, is this on purpose? If so, maybe worth a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment. Selecting the "best" signature is left for certificate merging (#8344), we select the signature when we receive the key so we don't have to select it here.

There are also expiring signatures (the signature itself may expire rather than the key), but apparently nobody has seen them in the wild and we ignore signature expiration. Otherwise theoretically you may want to store multiple signatures and look at the one that is not expired yet at the time when you select the key.

Comment thread src/pgp/pgp_tests.rs
/// If there is an expiring subkey, it is preferred, otherwise non-expiring subkey is selected.
/// Non-encryption subkeys such as RSA subkey for authentication are ignored.
#[test]
fn test_select_pk_for_encryption() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to additionally have a test that if there are multiple expiring subkeys, the one with the nearest expiration date is selected.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added separate test_expiring_subkey_selection.

@link2xt
link2xt force-pushed the link2xt/key-selection branch 5 times, most recently from 95ed2c5 to cf00463 Compare October 8, 2026 16:35
Test keys were generated with GnuPG 2.4.9 from Arch Linux.

The script that was used to generate the key for test_select_pk_for_encryption:
```
export GNUPGHOME="$PWD/gnupghome"
rm -fr "$GNUPGHOME"
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"

# --faked-system-time example is from the gpg man page
GPG="gpg --batch --quiet --faked-system-time 20070924T154812"

USERID='Alice <alice@example.org>'

# Generate the primary key.
# "default" means certification+signing primary key
# "never" to make the key not expire, otherwise gpg generates expiring key by default.
$GPG --passphrase '' --quick-generate-key "$USERID" ed25519 default never

# --quick-add-key requires that keys are referred to by the fingerprint.
# Output of --with-colons is described in /usr/share/doc/gnupg/DETAILS
FINGERPRINT="$($GPG --list-secret-keys --with-colons | awk -F: '$1 == "fpr" { print $10 }')"

# Authentication-only RSA key, should not be used for encryption.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" rsa auth never

# Expired (considering the fake date) subkey.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr 1d

# Signing subkey.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" ed25519 sign never

# Usable encryption subkey.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr never

# Expiring subkey that is not expired in the beginning of 2008.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr 1y

# Another encryption subkey that should not be used because the first one is preferred.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr never

$GPG --armor --export "$FINGERPRINT" > alice.tpk.asc
```

Key for test_select_only_expired_subkey was generated with:
```
#!/bin/sh
set -e
export GNUPGHOME="$PWD/gnupghome"
rm -fr  "$GNUPGHOME"
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"

GPG="gpg --batch --quiet --faked-system-time 20070924T154812"
USERID='Alice <alice@example.org>'
$GPG --passphrase '' --quick-generate-key "$USERID" ed25519 default never
FINGERPRINT="$($GPG --list-secret-keys --with-colons | awk -F: '$1 == "fpr" { print $10 }')"
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr 1y
$GPG --armor --export "$FINGERPRINT" > alice.tpk.asc
```

Key for test_expiring_subkey_selection was generated with:
```
#!/bin/sh
set -e
export GNUPGHOME="$PWD/gnupghome"
rm -fr  "$GNUPGHOME"
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"

GPG="gpg --batch --quiet"
USERID='Alice <alice@example.org>'

$GPG --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key "$USERID" ed25519 default never
FINGERPRINT="$($GPG --list-secret-keys --with-colons | awk -F: '$1 == "fpr" { print $10 }')"

$GPG --passphrase '' --faked-system-time 20250101T000000 --quick-add-key "$FINGERPRINT" cv25519 encr never

$GPG --passphrase '' --faked-system-time 20260601T000000 --quick-add-key "$FINGERPRINT" cv25519 encr 1m
$GPG --passphrase '' --faked-system-time 20260601T000000 --quick-add-key "$FINGERPRINT" cv25519 encr 2w
$GPG --passphrase '' --faked-system-time 20260608T000000 --quick-add-key "$FINGERPRINT" cv25519 encr 2w

$GPG --armor --export "$FINGERPRINT" > alice.tpk.asc
```
@link2xt
link2xt force-pushed the link2xt/key-selection branch from cf00463 to 2fe1d35 Compare October 8, 2026 16:58

@hko-s hko-s left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good (and exciting to see ac2 code take shape!), left one question.

Comment thread src/pgp.rs
/// Returns `None` if the public key cannot be used for encryption.
/// The key is selected according to
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-03.html#section-4.3-4>.
/// If multiple keys are available, the one that will expire sooner is selected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there code in some other place that removes ac2 subkeys that will expire "very soon"? (Encrypting to an ac2 subkey that will expire in one second is probably wrong - so I wonder if there are additional defenses against it).


And as a small (non-actionable) nit, just for the record:
I personally think selecting the "soonest subkey to expire" is not sensible for non-ac2 keys. However, since the OpenPGP WG has not adopted a draft about key selection yet, there is no official guidance about key selection.
I don't expect that this approach will be a problem in practice, in Delta, so I don't suggest changing it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there code in some other place that removes ac2 subkeys that will expire "very soon"? (Encrypting to an ac2 subkey that will expire in one second is probably wrong - so I wonder if there are additional defenses against it).

I think this is addressed in Autocrypt 2 by keeping the key around for longer than the expiration date. Expiration date is how long the key can be used for sending, and receiver can keep the private key for a bit longer to be able to decrypt messages sent 5 second before expiration, but received a few days after expiration.

Autocrypt 2 draft currently says "Because message delivery is not instantaneous, a subkey should be retained for a grace period beyond its validity window." in https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-03.html#name-timing-of-secret-key-destru

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that makes sense. Thanks!

@link2xt
link2xt merged commit 036b928 into main Oct 10, 2026
54 of 55 checks passed
@link2xt
link2xt deleted the link2xt/key-selection branch October 10, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delta Chat encrypts to subkey that is not marked with encryption flag

3 participants