From 37bbe8bb39e120e7edf09a2fd42ed25cda962c6b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jul 2023 23:15:22 +0200 Subject: [PATCH] Initial implementation of the new revoke-key command from SOP-07 --- pgpainless-sop/build.gradle | 1 + .../org/pgpainless/sop/RevokeKeyImpl.java | 115 ++++++++++++++++++ .../main/java/org/pgpainless/sop/SOPImpl.java | 6 + .../operation/PGPainlessRevokeKeyTest.java | 40 ++++++ version.gradle | 2 +- 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 1a40bb27..3a911a86 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -16,6 +16,7 @@ repositories { dependencies { implementation 'org.jetbrains:annotations:20.1.0' testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" // Logging diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java new file mode 100644 index 00000000..a99b904a --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.CharacterCodingException; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.PGPainless; +import org.pgpainless.exception.WrongPassphraseException; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.util.ArmoredOutputStreamFactory; +import org.pgpainless.util.Passphrase; +import sop.Ready; +import sop.exception.SOPGPException; +import sop.operation.RevokeKey; +import sop.util.UTF8Util; + +public class RevokeKeyImpl implements RevokeKey { + + private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + private boolean armor = true; + + public RevokeKey noArmor() { + this.armor = false; + return this; + } + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords + * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable + */ + public RevokeKey withKeyPassword(byte[] password) + throws SOPGPException.UnsupportedOption, + SOPGPException.PasswordNotHumanReadable { + String string; + try { + string = UTF8Util.decodeUTF8(password); + } catch (CharacterCodingException e) { + throw new SOPGPException.PasswordNotHumanReadable("Cannot UTF8-decode password."); + } + protector.addPassphrase(Passphrase.fromPassword(string)); + return this; + } + + public Ready keys(InputStream keys) throws SOPGPException.BadData { + PGPSecretKeyRingCollection secretKeyRings; + try { + secretKeyRings = PGPainless.readKeyRing().secretKeyRingCollection(keys); + } catch (IOException e) { + throw new SOPGPException.BadData("Cannot decode secret keys.", e); + } + for (PGPSecretKeyRing secretKeys : secretKeyRings) { + protector.addSecretKey(secretKeys); + } + + final List revocationCertificates = new ArrayList<>(); + for (PGPSecretKeyRing secretKeys : secretKeyRings) { + SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); + try { + RevocationAttributes revocationAttributes = RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.KEY_RETIRED) + .withoutDescription(); + if (secretKeys.getPublicKey().getVersion() == PublicKeyPacket.VERSION_6) { + PGPPublicKeyRing revocation = editor.createMinimalRevocationCertificate(protector, revocationAttributes); + revocationCertificates.add(revocation); + } else { + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + PGPSignature revocation = editor.createRevocation(protector, revocationAttributes); + certificate = KeyRingUtils.injectCertification(certificate, revocation); + revocationCertificates.add(certificate); + } + } catch (WrongPassphraseException e) { + throw new SOPGPException.KeyIsProtected("Missing or wrong passphrase for key " + OpenPgpFingerprint.of(secretKeys), e); + } + catch (PGPException e) { + throw new RuntimeException("Cannot generate revocation certificate.", e); + } + } + + return new Ready() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + PGPPublicKeyRingCollection certificateCollection = new PGPPublicKeyRingCollection(revocationCertificates); + if (armor) { + ArmoredOutputStream out = ArmoredOutputStreamFactory.get(outputStream); + certificateCollection.encode(out); + out.close(); + } else { + certificateCollection.encode(outputStream); + } + } + }; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index a0e5f631..f2c6120b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -18,6 +18,7 @@ import sop.operation.GenerateKey; import sop.operation.InlineSign; import sop.operation.InlineVerify; import sop.operation.ListProfiles; +import sop.operation.RevokeKey; import sop.operation.Version; /** @@ -102,6 +103,11 @@ public class SOPImpl implements SOP { return new ListProfilesImpl(); } + @Override + public RevokeKey revokeKey() { + return new RevokeKeyImpl(); + } + @Override public InlineDetach inlineDetach() { return new InlineDetachImpl(); diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java new file mode 100644 index 00000000..8b472121 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import sop.SOP; +import sop.testsuite.operation.RevokeKeyTest; + +public class PGPainlessRevokeKeyTest extends RevokeKeyTest { + + @ParameterizedTest + @MethodSource("provideInstances") // from sop-java's RevokeKeyTest class + @Override + public void revokeUnprotectedKey(SOP sop) throws IOException { + super.revokeUnprotectedKey(sop); + + byte[] key = sop.generateKey().generate().getBytes(); + byte[] revokedKey = sop.revokeKey().keys(key).getBytes(); + + PGPKeyRing certificate = PGPainless.readKeyRing().keyRing(revokedKey); + assertFalse(certificate instanceof PGPSecretKeyRing); + assertTrue(certificate instanceof PGPPublicKeyRing); + + KeyRingInfo info = PGPainless.inspectKeyRing(certificate); + assertTrue(info.getRevocationState().isSoftRevocation()); + } +} diff --git a/version.gradle b/version.gradle index a44b6d8f..b0238be6 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '6.1.0' + sopJavaVersion = '7.0.0-SNAPSHOT' } }