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 a8d7dd48..78ec5a7b 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 @@ -38,6 +38,8 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; @@ -58,6 +60,7 @@ 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.KeyRingUtils; +import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.key.util.SignatureUtils; import org.pgpainless.util.Passphrase; @@ -271,14 +274,37 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { return this; } - private PGPSecretKeyRing revokeSubKey(SecretKeyRingProtector protector, PGPPublicKey revokeeSubKey) throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primaryKey); - PGPPrivateKey privateKey = primaryKey.extractPrivateKey(protector.getDecryptor(primaryKey.getKeyID())); - signatureGenerator.init(SignatureType.SUBKEY_REVOCATION.getCode(), privateKey); + @Override + public PGPSignature createRevocationCertificate(OpenPgpV4Fingerprint fingerprint, + SecretKeyRingProtector secretKeyRingProtector, + RevocationAttributes revocationAttributes) + throws PGPException { + PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(fingerprint.getKeyId()); + if (revokeeSubKey == null) { + throw new NoSuchElementException("No subkey with fingerprint " + fingerprint + " found."); + } - // Generate revocation - PGPSignature subKeyRevocation = signatureGenerator.generateCertification(primaryKey.getPublicKey(), revokeeSubKey); + PGPSignature revocationCertificate = generateRevocation(secretKeyRingProtector, revokeeSubKey, revocationAttributes); + return revocationCertificate; + } + + @Override + public PGPSignature createRevocationCertificate(long subKeyId, + SecretKeyRingProtector secretKeyRingProtector, + RevocationAttributes revocationAttributes) + throws PGPException { + PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(subKeyId); + if (revokeeSubKey == null) { + throw new NoSuchElementException("No subkey with id " + Long.toHexString(subKeyId) + " found."); + } + + PGPSignature revocationCertificate = generateRevocation(secretKeyRingProtector, revokeeSubKey, revocationAttributes); + return revocationCertificate; + } + + private PGPSecretKeyRing revokeSubKey(SecretKeyRingProtector protector, PGPPublicKey revokeeSubKey) + throws PGPException { + PGPSignature subKeyRevocation = generateRevocation(protector, revokeeSubKey, null); revokeeSubKey = PGPPublicKey.addCertification(revokeeSubKey, subKeyRevocation); // Inject revoked public key into key ring @@ -287,6 +313,31 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { return PGPSecretKeyRing.replacePublicKeys(secretKeyRing, publicKeyRing); } + private PGPSignature generateRevocation(SecretKeyRingProtector protector, + PGPPublicKey revokeeSubKey, + RevocationAttributes revocationAttributes) + throws PGPException { + PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); + PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primaryKey); + PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); + subpacketGenerator.setIssuerFingerprint(false, primaryKey); + + if (revocationAttributes != null) { + subpacketGenerator.setRevocationReason(false, revocationAttributes.getReason().code(), revocationAttributes.getDescription()); + } + + PGPSignatureSubpacketVector subPackets = subpacketGenerator.generate(); + signatureGenerator.setHashedSubpackets(subPackets); + + PGPPrivateKey privateKey = primaryKey.extractPrivateKey(protector.getDecryptor(primaryKey.getKeyID())); + SignatureType type = revokeeSubKey.isMasterKey() ? SignatureType.KEY_REVOCATION : SignatureType.SUBKEY_REVOCATION; + signatureGenerator.init(type.getCode(), privateKey); + + // Generate revocation + PGPSignature subKeyRevocation = signatureGenerator.generateCertification(primaryKey.getPublicKey(), revokeeSubKey); + return subKeyRevocation; + } + @Override public WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(@Nullable Passphrase oldPassphrase, @Nonnull KeyRingProtectionSettings oldProtectionSettings) { 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 4e97def5..80a462c0 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 @@ -23,10 +23,12 @@ import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.protection.KeyRingProtectionSettings; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.key.util.UserId; import org.pgpainless.util.Passphrase; @@ -127,6 +129,16 @@ public interface SecretKeyRingEditorInterface { */ SecretKeyRingEditorInterface revokeSubKey(long subKeyId, SecretKeyRingProtector secretKeyRingProtector) throws PGPException; + PGPSignature createRevocationCertificate(OpenPgpV4Fingerprint fingerprint, + SecretKeyRingProtector secretKeyRingProtector, + RevocationAttributes revocationAttributes) + throws PGPException; + + PGPSignature createRevocationCertificate(long subKeyId, + SecretKeyRingProtector secretKeyRingProtector, + RevocationAttributes revocationAttributes) + throws PGPException; + /** * Change the passphrase of the whole key ring. * 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 new file mode 100644 index 00000000..ca9e9c13 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.key.util; + +public final class RevocationAttributes { + + public enum Reason { + NO_REASON((byte) 0), + KEY_SUPERSEDED((byte) 1), + KEY_COMPROMISED((byte) 2), + KEY_RETIRED((byte) 3), + USER_ID_NO_LONGER_VALID((byte) 32), + ; + + private final byte reasonCode; + + Reason(byte reasonCode) { + this.reasonCode = reasonCode; + } + + public byte code() { + return reasonCode; + } + + + @Override + public String toString() { + return code() + " - " + name(); + } + } + + public enum RevocationType { + KEY_REVOCATION, + CERT_REVOCATION + } + + private final Reason reason; + private final String description; + + private RevocationAttributes(Reason reason, String description) { + this.reason = reason; + this.description = description; + } + + public Reason getReason() { + return reason; + } + + public String getDescription() { + return description; + } + + public static WithReason createKeyRevocation() { + return new WithReason(RevocationType.KEY_REVOCATION); + } + + public static WithReason createCertificateRevocation() { + return new WithReason(RevocationType.CERT_REVOCATION); + } + + public static final class WithReason { + + private final RevocationType type; + + private WithReason(RevocationType type) { + this.type = type; + } + + public WithDescription withReason(Reason reason) { + throwIfReasonTypeMismatch(reason, type); + return new WithDescription(reason); + } + + private void throwIfReasonTypeMismatch(Reason reason, RevocationType type) { + if (type == RevocationType.KEY_REVOCATION) { + if (reason == Reason.USER_ID_NO_LONGER_VALID) { + throw new IllegalArgumentException("Reason " + reason + " can only be used for certificate revocations, not to revoke keys."); + } + } else if (type == RevocationType.CERT_REVOCATION) { + switch (reason) { + case KEY_SUPERSEDED: + case KEY_COMPROMISED: + case KEY_RETIRED: + throw new IllegalArgumentException("Reason " + reason + " can only be used for key revocations, not to revoke certificates."); + } + } + } + + } + + public static final class WithDescription { + + private final Reason reason; + + private WithDescription(Reason reason) { + this.reason = reason; + } + + public RevocationAttributes withDescription(String description) { + return new RevocationAttributes(reason, description); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/KeyPrinter.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java similarity index 98% rename from pgpainless-core/src/main/java/org/pgpainless/util/KeyPrinter.java rename to pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index c6458d22..7bae1324 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/KeyPrinter.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -25,7 +25,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; -public class KeyPrinter { +public class ArmorUtils { public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException { return toAsciiArmoredString(secretKeys.getEncoded()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java index 8a46f161..d86fe66b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java @@ -15,6 +15,7 @@ */ 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.assertTrue; @@ -22,14 +23,20 @@ import java.io.IOException; import java.util.Iterator; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.TestKeys; +import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; public class RevokeSubKeyTest { @@ -56,4 +63,45 @@ public class RevokeSubKeyTest { assertTrue(subKey.getPublicKey().hasRevocation()); } + + @Test + public void detachedRevokeSubkeyTest() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(secretKeys); + SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector.forKey(secretKeys, Passphrase.fromPassword("password123")); + + PGPSignature revocationCertificate = PGPainless.modifyKeyRing(secretKeys) + .createRevocationCertificate(fingerprint, protector, RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.KEY_RETIRED) + .withDescription("Key no longer used.")); + + // CHECKSTYLE:OFF + System.out.println("Revocation Certificate:"); + System.out.println(ArmorUtils.toAsciiArmoredString(revocationCertificate.getEncoded())); + // CHECKSTYLE:ON + + PGPPublicKey publicKey = secretKeys.getPublicKey(); + assertFalse(publicKey.hasRevocation()); + + publicKey = PGPPublicKey.addCertification(publicKey, revocationCertificate); + + assertTrue(publicKey.hasRevocation()); + } + + @Test + public void testRevocationSignatureTypeCorrect() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + Iterator keysIterator = secretKeys.getPublicKeys(); + PGPPublicKey primaryKey = keysIterator.next(); + PGPPublicKey subKey = keysIterator.next(); + SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector + .forKey(secretKeys, Passphrase.fromPassword("password123")); + + SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); + PGPSignature keyRevocation = editor.createRevocationCertificate(primaryKey.getKeyID(), protector, null); + PGPSignature subkeyRevocation = editor.createRevocationCertificate(subKey.getKeyID(), protector, null); + + assertEquals(SignatureType.KEY_REVOCATION.getCode(), keyRevocation.getSignatureType()); + assertEquals(SignatureType.SUBKEY_REVOCATION.getCode(), subkeyRevocation.getSignatureType()); + } }