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:
parent
5cdbb125b0
commit
0edd8b616f
5 changed files with 235 additions and 8 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue