diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index a6f30435..a8c569a2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -47,6 +47,7 @@ import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; +import org.pgpainless.key.util.KeyIdUtil; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.signature.builder.DirectKeySelfSignatureBuilder; @@ -609,6 +610,25 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { return this; } + @Override + public SecretKeyRingEditorInterface setExpirationDateOfSubkey(@Nullable Date expiration, long keyId, @Nonnull SecretKeyRingProtector secretKeyRingProtector) throws PGPException { + // is primary key + if (keyId == secretKeyRing.getPublicKey().getKeyID()) { + return setExpirationDate(expiration, secretKeyRingProtector); + } + + // is subkey + PGPPublicKey subkey = secretKeyRing.getPublicKey(keyId); + if (subkey != null) { + PGPSignature prevBinding = PGPainless.inspectKeyRing(secretKeyRing).getCurrentSubkeyBindingSignature(keyId); + PGPSignature bindingSig = reissueSubkeyBindingSignature(subkey, expiration, secretKeyRingProtector, prevBinding); + secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, subkey, bindingSig); + } else { + throw new NoSuchElementException("No subkey with ID " + KeyIdUtil.formatKeyId(keyId) + " found."); + } + return this; + } + @Override public PGPPublicKeyRing createMinimalRevocationCertificate( @Nonnull SecretKeyRingProtector secretKeyRingProtector, @@ -699,6 +719,57 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { return builder.build(publicKey); } + private PGPSignature reissueSubkeyBindingSignature( + PGPPublicKey subkey, + Date expiration, + SecretKeyRingProtector secretKeyRingProtector, + PGPSignature prevSubkeyBindingSignature) + throws PGPException { + if (subkey == null) { + throw new IllegalArgumentException("Subkey MUST NOT be null."); + } + if (prevSubkeyBindingSignature == null) { + throw new IllegalArgumentException("Previous subkey binding signature for " + + KeyIdUtil.formatKeyId(subkey.getKeyID()) + " MUST NOT be null."); + } + PGPPublicKey primaryKey = secretKeyRing.getPublicKey(); + PGPSecretKey secretPrimaryKey = secretKeyRing.getSecretKey(); + PGPSecretKey secretSubkey = secretKeyRing.getSecretKey(subkey.getKeyID()); + + if (secretPrimaryKey == null) { + throw new NoSuchElementException("Secret Key Ring does not contain secret-key component for the primary key."); + } + + SubkeyBindingSignatureBuilder builder = new SubkeyBindingSignatureBuilder( + secretPrimaryKey, secretKeyRingProtector, prevSubkeyBindingSignature); + SelfSignatureSubpackets subpackets = builder.getHashedSubpackets(); + if (referenceTime != null) { + subpackets.setSignatureCreationTime(referenceTime); + } + // Set expiration + subpackets.setKeyExpirationTime(subkey, expiration); + subpackets.setSignatureExpirationTime(null); // avoid copying sig exp time + + List previousKeyFlags = SignatureSubpacketsUtil.parseKeyFlags(prevSubkeyBindingSignature); + if (previousKeyFlags != null && previousKeyFlags.contains(KeyFlag.SIGN_DATA)) { + if (secretSubkey == null) { + throw new NoSuchElementException("Secret keyring does not contain secret-key component for subkey " + + KeyIdUtil.formatKeyId(subkey.getKeyID())); + } + + // Create new embedded back-sig + subpackets.clearEmbeddedSignatures(); + try { + subpackets.addEmbeddedSignature( + new PrimaryKeyBindingSignatureBuilder(secretSubkey, secretKeyRingProtector) + .build(primaryKey)); + } catch (IOException e) { + throw new PGPException("Cannot add embedded primary-key back signature.", e); + } + } + return builder.build(subkey); + } + private PGPSignature getPreviousDirectKeySignature() { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); return info.getLatestDirectKeySelfSignature(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java index dd7ed499..f746934e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java @@ -469,6 +469,23 @@ public interface SecretKeyRingEditorInterface { @Nonnull SecretKeyRingProtector secretKeyRingProtector) throws PGPException; + /** + * 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 secretKeyRingProtector 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 + */ + SecretKeyRingEditorInterface setExpirationDateOfSubkey( + @Nullable Date expiration, + long keyId, + @Nonnull SecretKeyRingProtector secretKeyRingProtector) + throws PGPException; + /** * Create a minimal, self-authorizing revocation certificate, containing only the primary key * and a revocation signature. 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..c550e376 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; + +import org.bouncycastle.openpgp.PGPException; +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; + +public class ChangeSubkeyExpirationTimeTest { + + @Test + public void changeExpirationTimeOfSubkey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + 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 PGPException, 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)); + } +}