diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 978e951a..c9c42034 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -350,6 +350,10 @@ public final class DecryptionStreamFactory { } } + if (verificationKeyRing == null && missingPublicKeyCallback != null) { + verificationKeyRing = missingPublicKeyCallback.onMissingPublicKeyEncountered(keyId); + } + return verificationKeyRing; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java index 9ca3bfa3..57bcd0f9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java @@ -16,21 +16,27 @@ package org.pgpainless.decryption_verification; import javax.annotation.Nonnull; +import javax.annotation.Nullable; -import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; public interface MissingPublicKeyCallback { /** - * This method gets called if we encounter a signature of an unknown key. + * This method gets called if we encounter a signature made by a key which was not provided for signature verification. + * If you cannot provide the requested key, it is safe to return null here. + * PGPainless will then continue verification with the next signature. * - * Note: It would be super cool to provide the OpenPgp fingerprint here, but unfortunately signatures only contain - * the key id (see https://tools.ietf.org/html/rfc4880#section-5.2.3.5) + * Note: The key-id might belong to a subkey, so be aware that when looking up the {@link PGPPublicKeyRing}, + * you may not only search for the key-id on the key rings primary key! * - * @param keyId ID of the missing key + * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures + * only contain the key id (see https://datatracker.ietf.org/doc/html/rfc4880#section-5.4) * - * @return the key or null + * @param keyId ID of the missing signing (sub)key + * + * @return keyring containing the key or null */ - PGPPublicKey onMissingPublicKeyEncountered(@Nonnull Long keyId); + @Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java new file mode 100644 index 00000000..514ced4d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java @@ -0,0 +1,95 @@ +/* + * Copyright 2021 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.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.KeyRingUtils; + +/** + * Test functionality of the {@link MissingPublicKeyCallback} which is called when during signature verification, + * a signature is encountered which was made by a key that was not provided in + * {@link DecryptionBuilderInterface.VerifyWith#verifyWith(PGPPublicKeyRing)}. + */ +public class VerifyWithMissingPublicKeyCallback { + + @Test + public void testMissingPublicKeyCallback() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing signingSecKeys = PGPainless.generateKeyRing().modernKeyRing("alice", null); + PGPPublicKey signingKey = new KeyRingInfo(signingSecKeys).getSigningSubkeys().get(0); + PGPPublicKeyRing signingPubKeys = KeyRingUtils.publicKeyRingFrom(signingSecKeys); + PGPPublicKeyRing unrelatedKeys = TestKeys.getJulietPublicKeyRing(); + + String msg = "Arguing that you don't care about the right to privacy because you have nothing to hide" + + "is no different than saying you don't care about free speech because you have nothing to say."; + ByteArrayOutputStream signOut = new ByteArrayOutputStream(); + EncryptionStream signingStream = PGPainless.encryptAndOrSign().onOutputStream(signOut) + .withOptions(ProducerOptions.sign(new SigningOptions().addInlineSignature( + SecretKeyRingProtector.unprotectedKeys(), + signingSecKeys, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + ))); + Streams.pipeAll(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)), signingStream); + signingStream.close(); + + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(signOut.toByteArray())) + .doNotDecrypt() + .verifyWith(unrelatedKeys) + .handleMissingPublicKeysWith( + new MissingPublicKeyCallback() { + @Nullable + @Override + public PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId) { + assertEquals(signingKey.getKeyID(), keyId, "Signing key-ID mismatch."); + return signingPubKeys; + } + } + ).build(); + ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); + Streams.pipeAll(verificationStream, plainOut); + verificationStream.close(); + + assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plainOut.toByteArray()); + OpenPgpMetadata metadata = verificationStream.getResult(); + assertTrue(metadata.getVerifiedSignatureKeyFingerprints().contains(new OpenPgpV4Fingerprint(signingKey))); + } +}