Repository navigation
feat: take key flags and expiration into account when selecting the key - #8744
Conversation
beec8b2 to
7be0f1b
Compare
7be0f1b to
8acd034
Compare
|
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. |
8acd034 to
715030b
Compare
| // Key 4 is the one that should be selected. | ||
|
|
||
| // Subkey 4 fingerprint. | ||
| let expected_fallback_fingerprint = "cdeb3ba3999bf7880f0ee1f536188c6ffc8e267b"; |
There was a problem hiding this comment.
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.
| .find(|subkey| subkey.algorithm().can_encrypt()) | ||
| .filter(|subkey| subkey.algorithm().can_encrypt()) | ||
| .filter_map(|subkey| { | ||
| let signature = subkey.signatures.first()?; |
There was a problem hiding this comment.
This only ever looks at the first signature, is this on purpose? If so, maybe worth a comment
There was a problem hiding this comment.
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.
| /// 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() { |
There was a problem hiding this comment.
Would be good to additionally have a test that if there are multiple expiring subkeys, the one with the nearest expiration date is selected.
There was a problem hiding this comment.
I added separate test_expiring_subkey_selection.
715030b to
e2c886b
Compare
95ed2c5 to
cf00463
Compare
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
```
cf00463 to
2fe1d35
Compare
| /// 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Right, that makes sense. Thanks!
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 thegpgoutput for the first test so you don't have to run it yourself:gpg --list-packets output