diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index 5fdd2da5..ad24ec04 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -8,6 +8,7 @@ import java.util.* import java.util.function.Predicate import javax.annotation.Nonnull import kotlin.NoSuchElementException +import openpgp.openPgpKeyId import org.bouncycastle.bcpg.sig.KeyExpirationTime import org.bouncycastle.extensions.getKeyExpirationDate import org.bouncycastle.extensions.publicKeyAlgorithm @@ -460,6 +461,28 @@ class SecretKeyRingEditor( return this } + override fun setExpirationDateOfSubkey( + expiration: Date?, + keyId: Long, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface = apply { + // is primary key + if (keyId == secretKeyRing.publicKey.keyID) { + return setExpirationDate(expiration, protector) + } + + // is subkey + val subkey = + secretKeyRing.getPublicKey(keyId) + ?: throw NoSuchElementException("No subkey with ID ${keyId.openPgpKeyId()} found.") + val prevBinding = + inspectKeyRing(secretKeyRing).getCurrentSubkeyBindingSignature(keyId) + ?: throw NoSuchElementException( + "Previous subkey binding signaure for ${keyId.openPgpKeyId()} MUST NOT be null.") + val bindingSig = reissueSubkeyBindingSignature(subkey, expiration, protector, prevBinding) + secretKeyRing = injectCertification(secretKeyRing, subkey, bindingSig) + } + override fun createMinimalRevocationCertificate( protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? @@ -676,6 +699,45 @@ class SecretKeyRingEditor( .build() } + private fun reissueSubkeyBindingSignature( + subkey: PGPPublicKey, + expiration: Date?, + protector: SecretKeyRingProtector, + prevSubkeyBindingSignature: PGPSignature + ): PGPSignature { + val primaryKey = secretKeyRing.publicKey + val secretPrimaryKey = secretKeyRing.secretKey + val secretSubkey: PGPSecretKey? = secretKeyRing.getSecretKey(subkey.keyID) + + val builder = + SubkeyBindingSignatureBuilder(secretPrimaryKey, protector, prevSubkeyBindingSignature) + builder.hashedSubpackets.apply { + // set expiration + setSignatureCreationTime(referenceTime) + setKeyExpirationTime(subkey, expiration) + setSignatureExpirationTime(null) // avoid copying sig exp time + + // signing-capable subkeys need embedded primary key binding sig + SignatureSubpacketsUtil.parseKeyFlags(prevSubkeyBindingSignature)?.let { flags -> + if (flags.contains(KeyFlag.SIGN_DATA)) { + if (secretSubkey == null) { + throw NoSuchElementException( + "Secret key does not contain secret-key" + + " component for subkey ${subkey.keyID.openPgpKeyId()}") + } + + // create new embedded back-sig + clearEmbeddedSignatures() + addEmbeddedSignature( + PrimaryKeyBindingSignatureBuilder(secretSubkey, protector) + .build(primaryKey)) + } + } + } + + return builder.build(subkey) + } + private fun selectUserIds(predicate: Predicate): List = inspectKeyRing(secretKeyRing).validUserIds.filter { predicate.test(it) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt index b8fb993c..140ff905 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt @@ -469,6 +469,24 @@ interface SecretKeyRingEditorInterface { protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface + /** + * Set the expiration date for the subkey identified by the given keyId to the given expiration + * date. If the key is supposed to never expire, then an expiration date of null is expected. + * + * @param expiration new expiration date of null + * @param keyId id of the subkey + * @param protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a new subkey-binding or self-signature with + * the changed expiration date + */ + @Throws(PGPException::class) + fun setExpirationDateOfSubkey( + expiration: Date?, + keyId: Long, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + /** * Create a minimal, self-authorizing revocation certificate, containing only the primary key * and a revocation signature. This type of revocation certificates was introduced in OpenPGP diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java new file mode 100644 index 00000000..e1926b67 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.DateUtil; + +import java.io.IOException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ChangeSubkeyExpirationTimeTest { + + @Test + public void changeExpirationTimeOfSubkey() { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + Date now = secretKeys.getPublicKey().getCreationTime(); + Date inAnHour = new Date(now.getTime() + 1000 * 60 * 60); + PGPPublicKey encryptionKey = PGPainless.inspectKeyRing(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDateOfSubkey( + inAnHour, + encryptionKey.getKeyID(), + SecretKeyRingProtector.unprotectedKeys()) + .done(); + + JUtils.assertDateEquals(inAnHour, PGPainless.inspectKeyRing(secretKeys) + .getSubkeyExpirationDate(OpenPgpFingerprint.of(encryptionKey))); + } + + @Test + public void changeExpirationTimeOfExpiredSubkey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing( + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: CA52 4D5D E3D8 9CD9 105B BA45 3761 076B C6B5 3000\n" + + "Comment: Alice \n" + + "\n" + + "lFgEZXHykRYJKwYBBAHaRw8BAQdATArrVxPEpuA/wcayAxRl/v1tIYJSe4MCA/fO\n" + + "84CFgpcAAP9uZkLjoBIQAjUTEiS8Wk3sui3u4mJ4WVQEpNhQSpq37g8gtBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iJUEExYKAEcFAmVx8pIJEDdhB2vGtTAA\n" + + "FiEEylJNXePYnNkQW7pFN2EHa8a1MAACngECmwEFFgIDAQAECwkIBwUVCgkICwWJ\n" + + "CWYBgAKZAQAAG3oA/0iJbwyjGOTa2RlgBKdmFjlBG5uwMGheKge/aZBbdUd8AQCB\n" + + "8NFWmyLlne4hDMM2g8RFf/W156wnyTH7jTQLx2sZDJxYBGVx8pIWCSsGAQQB2kcP\n" + + "AQEHQLQt6ns7yTxLvIWXqFCekh6QEvUumhHvCTjZPXa/UxCNAAEA+FHhZ1uik6PN\n" + + "Pwli9Tp9QGddf3pwQw+OL/K7gpZO3sgQHYjVBBgWCgB9BQJlcfKSAp4BApsCBRYC\n" + + "AwEABAsJCAcFFQoJCAtfIAQZFgoABgUCZXHykgAKCRCRKlHdDPaYKjyZAQD10Km4\n" + + "Qs37yF9bntS+z9Va7AMUuBlzYF5H/nXCRuqQTAEA60q++7Xwj94yLfoAfxH0V6Wd\n" + + "L2rDJCDZ3FFMlycToQMACgkQN2EHa8a1MADmDgD9EGzH6pPYRW5vWQGXNsr7PMWK\n" + + "LlBnevc0DaVWEHTu9tcA/iezQ9R+A90qcE1+HeNIJbSB89yIoJje2vePRV/JakAI\n" + + "nF0EZXHykhIKKwYBBAGXVQEFAQEHQOiLc02OQJD9qdpsyR6bJ52Cu8rUMlEJOELz\n" + + "1858OoQyAwEIBwAA/3YkHGmnVaQvUpSwlCInOvHvjLNLH9b9Lh/OxiuSoMgIEASI\n" + + "dQQYFgoAHQUCZXHykgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEDdhB2vGtTAA\n" + + "1nkBAPAUcHxI1O+fE/QzuLANLHDeWc3Mc09KKnWoTkt/kk5VAQCIPlKQAcmmKdYE\n" + + "Tiz8woSKLQKswKr/jVMqnUiGPsU/DoiSBBgWCgBECRA3YQdrxrUwABYhBMpSTV3j\n" + + "2JzZEFu6RTdhB2vGtTAABYJlcfL6Ap4BApsMBRYCAwEABAsJCAcFFQoJCAsFiQAA\n" + + "AGgAAMNmAQDN/TML2zdgBNkfh7TIqbI4Flx54Yi7qEjSXg0Z+tszHgD/e1Bf+xEs\n" + + "BC9ewVsyQsnj3B0FliGYaPiQeoY/FGBmYQs=\n" + + "=5Ur6\n" + + "-----END PGP PRIVATE KEY BLOCK-----" + ); + assertNotNull(secretKeys); + + // subkey is expired at 2023-12-07 16:29:46 UTC + OpenPgpFingerprint encryptionSubkey = new OpenPgpV4Fingerprint("2E541354A23C9943375EC27A3EF133ED8720D636"); + JUtils.assertDateEquals( + DateUtil.parseUTCDate("2023-12-07 16:29:46 UTC"), + PGPainless.inspectKeyRing(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); + + // re-validate the subkey by setting its expiry to null (no expiry) + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDateOfSubkey(null, encryptionSubkey.getKeyId(), SecretKeyRingProtector.unprotectedKeys()) + .done(); + + assertNull(PGPainless.inspectKeyRing(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); + } +}