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 a5e4d37b..ddd1bfaf 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 @@ -10,6 +10,7 @@ import org.bouncycastle.bcpg.sig.KeyExpirationTime; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; @@ -154,7 +155,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { PGPSignature signature = primaryUserId == null ? info.getLatestDirectKeySelfSignature() : info.getLatestUserIdCertification(primaryUserId); final Date previousKeyExpiration = signature == null ? null : - SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(signature, primaryKey); + SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(signature, primaryKey); // Add new primary user-id signature addUserId( @@ -607,6 +608,23 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { return this; } + @Override + public PGPPublicKeyRing createMinimalRevocationCertificate( + @Nonnull SecretKeyRingProtector secretKeyRingProtector, + @Nullable RevocationAttributes keyRevocationAttributes) + throws PGPException { + // Check reason + if (keyRevocationAttributes != null && !RevocationAttributes.Reason.isKeyRevocation(keyRevocationAttributes.getReason())) { + throw new IllegalArgumentException("Revocation reason MUST be applicable to a key revocation."); + } + + PGPSignature revocation = createRevocation(secretKeyRingProtector, keyRevocationAttributes); + PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); + primaryKey = KeyRingUtils.getStrippedDownPublicKey(primaryKey); + primaryKey = PGPPublicKey.addCertification(primaryKey, revocation); + return new PGPPublicKeyRing(Collections.singletonList(primaryKey)); + } + private PGPSignature reissueNonPrimaryUserId( SecretKeyRingProtector secretKeyRingProtector, String userId, 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 b7d7af13..1735d369 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 @@ -13,6 +13,7 @@ import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.algorithm.KeyFlag; @@ -460,6 +461,22 @@ public interface SecretKeyRingEditorInterface { @Nonnull SecretKeyRingProtector secretKeyRingProtector) throws PGPException; + /** + * 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 v6. + * This method has no side effects on the original key and will leave it intact. + * + * @param secretKeyRingProtector protector to unlock the primary key. + * @param keyRevocationAttributes reason for the revocation (key revocation) + * @return minimal revocation certificate + * + * @throws PGPException in case we cannot generate a revocation signature + */ + PGPPublicKeyRing createMinimalRevocationCertificate(@Nonnull SecretKeyRingProtector secretKeyRingProtector, + @Nullable RevocationAttributes keyRevocationAttributes) + throws PGPException; + /** * Create a detached revocation certificate, which can be used to revoke the whole key. * The original key will not be modified by this method. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 23361c13..e87f04eb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -485,4 +485,13 @@ public final class KeyRingUtils { // Parse the key back into an object return new PGPSecretKeyRing(encoded.toByteArray(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } + + /** + * Strip all user-ids, user-attributes and signatures from the given public key. + * @param bloatedKey public key + * @return stripped public key + */ + public static PGPPublicKey getStrippedDownPublicKey(PGPPublicKey bloatedKey) throws PGPException { + return new PGPPublicKey(bloatedKey.getPublicKeyPacket(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java index 60a02fea..f84af5d3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java @@ -100,6 +100,25 @@ public final class RevocationAttributes { return isHardRevocation(reason.reasonCode); } + /** + * Return true if the given {@link Reason} denotes a key revocation. + * @param reason reason + * @return is key revocation + */ + public static boolean isKeyRevocation(@Nonnull Reason reason) { + return isKeyRevocation(reason.code()); + } + + /** + * Return true if the given reason code denotes a key revocation. + * @param code reason code + * @return is key revocation + */ + public static boolean isKeyRevocation(byte code) { + Reason reason = MAP.get(code); + return reason != USER_ID_NO_LONGER_VALID; + } + private final byte reasonCode; Reason(byte reasonCode) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java index 21d732d9..5cb6163d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java @@ -4,13 +4,20 @@ package org.pgpainless.key.modification; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; @@ -19,6 +26,7 @@ import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.util.CollectionUtils; public class RevocationCertificateTest { @@ -43,4 +51,50 @@ public class RevocationCertificateTest { assertFalse(PGPainless.inspectKeyRing(revokedKey).isKeyValidlyBound(secretKeys.getPublicKey().getKeyID())); } + + @Test + public void createMinimalRevocationCertificateTest() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + + PGPPublicKeyRing minimalRevocationCert = PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + SecretKeyRingProtector.unprotectedKeys(), + RevocationAttributes.createKeyRevocation().withReason(RevocationAttributes.Reason.KEY_RETIRED).withoutDescription()); + + assertEquals(1, minimalRevocationCert.size()); + PGPPublicKey key = minimalRevocationCert.getPublicKey(); + assertEquals(secretKeys.getPublicKey().getKeyID(), key.getKeyID()); + assertEquals(1, CollectionUtils.iteratorToList(key.getSignatures()).size()); + assertFalse(key.getUserIDs().hasNext()); + assertFalse(key.getUserAttributes().hasNext()); + assertNull(key.getTrustData()); + } + + @Test + public void createMinimalRevocationCertificateForFreshKeyTest() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + + PGPPublicKeyRing minimalRevocationCert = PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + SecretKeyRingProtector.unprotectedKeys(), + RevocationAttributes.createKeyRevocation().withReason(RevocationAttributes.Reason.KEY_RETIRED).withoutDescription()); + + assertEquals(1, minimalRevocationCert.size()); + PGPPublicKey key = minimalRevocationCert.getPublicKey(); + assertEquals(secretKeys.getPublicKey().getKeyID(), key.getKeyID()); + assertEquals(1, CollectionUtils.iteratorToList(key.getSignatures()).size()); + assertFalse(key.getUserIDs().hasNext()); + assertFalse(key.getUserAttributes().hasNext()); + assertNull(key.getTrustData()); + } + + @Test + public void createMinimalRevocationCertificate_wrongReason() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + assertThrows(IllegalArgumentException.class, + () -> PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + SecretKeyRingProtector.unprotectedKeys(), + RevocationAttributes.createCertificateRevocation() + .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) + .withoutDescription())); + } }