1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-11-30 00:02:06 +01:00

Add support for creating detached revocation certificates

This commit is contained in:
Paul Schaub 2020-11-20 12:01:39 +01:00
parent 5cdbb125b0
commit 0edd8b616f
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
5 changed files with 235 additions and 8 deletions

View file

@ -38,6 +38,8 @@ import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator; 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.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; 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.UnprotectedKeysProtector;
import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider;
import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.key.util.RevocationAttributes;
import org.pgpainless.key.util.SignatureUtils; import org.pgpainless.key.util.SignatureUtils;
import org.pgpainless.util.Passphrase; import org.pgpainless.util.Passphrase;
@ -271,14 +274,37 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface {
return this; return this;
} }
private PGPSecretKeyRing revokeSubKey(SecretKeyRingProtector protector, PGPPublicKey revokeeSubKey) throws PGPException { @Override
PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); public PGPSignature createRevocationCertificate(OpenPgpV4Fingerprint fingerprint,
PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primaryKey); SecretKeyRingProtector secretKeyRingProtector,
PGPPrivateKey privateKey = primaryKey.extractPrivateKey(protector.getDecryptor(primaryKey.getKeyID())); RevocationAttributes revocationAttributes)
signatureGenerator.init(SignatureType.SUBKEY_REVOCATION.getCode(), privateKey); throws PGPException {
PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(fingerprint.getKeyId());
if (revokeeSubKey == null) {
throw new NoSuchElementException("No subkey with fingerprint " + fingerprint + " found.");
}
// Generate revocation PGPSignature revocationCertificate = generateRevocation(secretKeyRingProtector, revokeeSubKey, revocationAttributes);
PGPSignature subKeyRevocation = signatureGenerator.generateCertification(primaryKey.getPublicKey(), revokeeSubKey); 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); revokeeSubKey = PGPPublicKey.addCertification(revokeeSubKey, subKeyRevocation);
// Inject revoked public key into key ring // Inject revoked public key into key ring
@ -287,6 +313,31 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface {
return PGPSecretKeyRing.replacePublicKeys(secretKeyRing, publicKeyRing); 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 @Override
public WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(@Nullable Passphrase oldPassphrase, public WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(@Nullable Passphrase oldPassphrase,
@Nonnull KeyRingProtectionSettings oldProtectionSettings) { @Nonnull KeyRingProtectionSettings oldProtectionSettings) {

View file

@ -23,10 +23,12 @@ import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.protection.KeyRingProtectionSettings; import org.pgpainless.key.protection.KeyRingProtectionSettings;
import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.util.RevocationAttributes;
import org.pgpainless.key.util.UserId; import org.pgpainless.key.util.UserId;
import org.pgpainless.util.Passphrase; import org.pgpainless.util.Passphrase;
@ -127,6 +129,16 @@ public interface SecretKeyRingEditorInterface {
*/ */
SecretKeyRingEditorInterface revokeSubKey(long subKeyId, SecretKeyRingProtector secretKeyRingProtector) throws PGPException; 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. * Change the passphrase of the whole key ring.
* *

View file

@ -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);
}
}
}

View file

@ -25,7 +25,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams; import org.bouncycastle.util.io.Streams;
public class KeyPrinter { public class ArmorUtils {
public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException { public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException {
return toAsciiArmoredString(secretKeys.getEncoded()); return toAsciiArmoredString(secretKeys.getEncoded());

View file

@ -15,6 +15,7 @@
*/ */
package org.pgpainless.key.modification; 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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -22,14 +23,20 @@ import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless; import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.TestKeys; import org.pgpainless.key.TestKeys;
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface;
import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector;
import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.util.RevocationAttributes;
import org.pgpainless.util.ArmorUtils;
import org.pgpainless.util.Passphrase; import org.pgpainless.util.Passphrase;
public class RevokeSubKeyTest { public class RevokeSubKeyTest {
@ -56,4 +63,45 @@ public class RevokeSubKeyTest {
assertTrue(subKey.getPublicKey().hasRevocation()); 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<PGPPublicKey> 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());
}
} }