pgpainless: On the Topic of Expiration
PGPainless aims to provide the user with tools to modify the validity period of keys and user-ids. Right now however, PGPainless’ API does not fully function as expected.
In particular, there are unexpected issues happening when the user changes the primary user-id of a key and later changes the expiration date of the primary key. The reason for this is that OpenPGP technically allows multiple user-ids to be marked as primary. PGPainless simply picks that one with with latest signature creation date on it. Therefore, when renewing a signature in order to reissue the expiration date, the old primary user-id is suddenly the most recently modified one and therefore becomes active again.
There are multiple ways of solving this issue:
- when marking user-id B as active, search for all former primary user-ids and create new signatures for them, marking them as non-primary. That way there is only one user-id marked primary at any point in time. However, instead of one signature, we potentially issue multiple one. This is the “prevent the issue from happening but with increased cost”-way to approach the problem.
- when reissuing signatures (e.g. when changing expiration dates) check what the current primary user-id of the key is and unset the primary user-id flag in the reissued signature if it targets a user-id different from the current primary user id. This approach only needs a single signature to be issued at a time. This is the “fix the issue after the fact”-way of tackling the issue.
I tend to go with the first way, since the second approach would need to be implemented in any other use-case that involves signature reissuing and is therefore error-prone.
Changes to the API
The Sequoia Interoperability Test Suite shows that unfortunately there is no consensus for how to treat some of the many ways signatures with expiration information can be arranged:

This chart does not even contain separate subkey expiration dates. Note specifically the white area in the bottom mid section where implementation behavior differs drastically. So when it comes to re-designing the API for setting expiration dates, I propose to avoid creating such situations from the white area, hence we should avoid setting direct-key-signatures on the primary key. Of course, PGPainless will still continue to evaluate those when processing signature validity.
Requirements for the expiration API are:
- one method for changing the expiration date of the key in its entirety. This method should change the expiration date on the primary user-id.
- one method for changing the expiration date of a specific user-id.
- one method to change the expiration date of a subkey.
One potential edge case are keys without user-ids. These keys do carry a direct-key signature, hence unfortunately we cannot fully ignore the white area from the chart above.
Lets say we have a key without user-ids. Now we change the expiration date, which must re-issue the direct-key signature. Then we add a primary user-id and set another expiration date. Now how do we expire the key now? Do we re-issue both the direct-key and the primary user-id sig?
So probably a sane solution would look like this:
setExpirationDate(date, protector) {
if (hasDirectKeySig) {
reissueDirectKeySig(date)
}
if (hasPrimaryUserId) {
reissuePrimaryUserId(userId, date)
}
if (hasDuplicatePrimaryUserIds) {
reissueNonPrimaryUserIdsForDuplicates()
}
}
setExpirationDate(userId, date, prorector) {
if (userId is primaryUserId) {
setExpirationDate(date, protector)
} else {
reissuePrimaryUserId(userId, date)
}
}
setExpirationDate(keyId, date, protector) {
if (keyIsPrimaryKey) {
setExpirationDate(date, protector)
} else {
reissueSubkeySignature(keyId, date, protector);
}
}
//
setPrimaryUserId(userId, protector) {
reissuePrimaryUserId(userId)
if (hasDuplicatePrimaryUserIds) {
reissueNonPrimaryUserIdsForDuplicates()
}
}
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Comments: 50 (50 by maintainers)
Commits related to this issue
- Rework key modification API. Fixes #225 — committed to pgpainless/pgpainless by vanitasvitae 3 years ago
Sounds sensible. I reverted the logic to return the first user-id. Also renamed the getter method to
getValidAndExpiredUserIds().For the record, fixing this revealed a flaw in PGPainless’ design, namely that it does only differentiate between valid and invalid signatures.
For advanced operations like re-certifying expired keys it would make much more sense to only consider correct, but expired signatures.
I will at some point need to rewrite signature evaluation to come up with an implementation that grants more control over when to reject signatures.
That would also be a great opportunity to improve exception texts for when a key is expired (get rid of those stupid NoSuchElementExceptions in favor for detailed messages like “The primary key is expired at XYZ”.
Okay, I reworked the logic for setting primary user-ids once more, so that the expiration date of the old primary user-id is copied to the new primary user-id and also being removed from the new non-primary user-id sig on the old user-id.
PGPainless should now behave as expected, (I hope).
Release 1,0.0-rc7 has been published and should soon be available 😃
Once you confirm that it fixes your issues I will release 1.0.0! \o/
I released a (last?) snapshot (rc9) for you to try 😃
Hey, sorry for being unresponsive the past days. I hope you had nice holidays and I wish you a happy new year 🎆.
Adding the user-id deletion methods as convenience methods is a great idea, I added those as
removeUserId(). However, I’m not sure how much sense it makes to add theDeleteUserIdRevocationSignatureSubpacketsCallbackclass, as I was able to implement this without.Interesting observation that
SelectUserId.containsEmailAddress("alice@pgpainless.org")would not match “alice@pgpainless.org” due to the missing email parenthesis (‘<’, ‘>’).I added
SelectUserId.byEmail()which implementsor(containsEmailAddress(), exactMatch())as you suggested.Now I will add some more tests and then push another release.
Looks like we will not let you release 1.0.0 very fast.
I will try to make it work for these use cases.