mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-12-27 21:37:58 +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.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) {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.util.io.Streams;
|
||||
|
||||
public class KeyPrinter {
|
||||
public class ArmorUtils {
|
||||
|
||||
public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException {
|
||||
return toAsciiArmoredString(secretKeys.getEncoded());
|
|
@ -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<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