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 fb6012fd..6e9627de 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 @@ -9,8 +9,10 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; @@ -45,6 +47,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.MissingLiteralDataException; +import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.implementation.ImplementationFactory; @@ -71,6 +74,7 @@ public final class DecryptionStreamFactory { private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private final List onePassSignatureChecks = new ArrayList<>(); private final List detachedSignatureChecks = new ArrayList<>(); + private final Map onePassSignaturesWithMissingCert = new HashMap<>(); private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); @@ -96,6 +100,8 @@ public final class DecryptionStreamFactory { long issuerKeyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing signingKeyRing = findSignatureVerificationKeyRing(issuerKeyId); if (signingKeyRing == null) { + SignatureValidationException ex = new SignatureValidationException("Missing verification certificate " + Long.toHexString(issuerKeyId)); + resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, null), ex); continue; } PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId); @@ -105,7 +111,8 @@ public final class DecryptionStreamFactory { DetachedSignatureCheck detachedSignature = new DetachedSignatureCheck(signature, signingKeyRing, signingKeyIdentifier); detachedSignatureChecks.add(detachedSignature); } catch (PGPException e) { - LOGGER.warn("Cannot verify detached signature made by {}. Reason: {}", signingKeyIdentifier, e.getMessage(), e); + SignatureValidationException ex = new SignatureValidationException("Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); + resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, signingKeyIdentifier), ex); } } } @@ -223,7 +230,7 @@ public final class DecryptionStreamFactory { .setModificationDate(pgpLiteralData.getModificationTime()) .setFileEncoding(StreamEncoding.fromCode(pgpLiteralData.getFormat())); - if (onePassSignatureChecks.isEmpty()) { + if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) { LOGGER.debug("No OnePassSignatures found -> We are done"); return literalDataInputStream; } @@ -237,6 +244,16 @@ public final class DecryptionStreamFactory { onePassSignatureChecks.get(i).setSignature(signatureList.get(reversedIndex)); } + for (PGPSignature signature : signatureList) { + if (onePassSignaturesWithMissingCert.containsKey(signature.getKeyID())) { + OnePassSignatureCheck check = onePassSignaturesWithMissingCert.remove(signature.getKeyID()); + check.setSignature(signature); + + resultBuilder.addInvalidInbandSignature(new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification certificate " + Long.toHexString(signature.getKeyID()))); + } + } + return new SignatureInputStream.VerifySignatures(literalDataInputStream, onePassSignatureChecks, detachedSignatureChecks, options, resultBuilder) { }; @@ -488,7 +505,7 @@ public final class DecryptionStreamFactory { // Find public key PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId); if (verificationKeyRing == null) { - LOGGER.debug("Missing verification key from {}", Long.toHexString(keyId)); + onePassSignaturesWithMissingCert.put(keyId, new OnePassSignatureCheck(signature, null)); return; } PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java new file mode 100644 index 00000000..0e979ead --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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 org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +public class SignedMessageVerificationWithoutCertIsStillSigned { + + private static final String message = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "owGbwMvMwCGmFN+gfIiXM5zxtG4SQ2Iw74rgzPS81BSFktSKEoW0/CKFlNS0xNKc\n" + + "Eoe0nPzy5KLKghK9ktTiEq6OXhYGMQ4GUzFFFtvXL7+VX9252+LpIheYcaxMQLMO\n" + + "iMtg183AxSkAUynizshwbBMnx4e4tn6NgJYtG/od3HL1y26GvpgqUtr2o37HpC+v\n" + + "GRmudmly/g+Osdt3t6Rb+8t8i8Y94ZJ3P/zNlk015FihXM0JAA==\n" + + "=A8uF\n" + + "-----END PGP MESSAGE-----\n"; + + @Test + public void verifyMissingVerificationCertOptionStillResultsInMessageIsSigned() throws IOException, PGPException { + ConsumerOptions withoutVerificationCert = new ConsumerOptions(); + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) + .withOptions(withoutVerificationCert); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(verificationStream, out); + verificationStream.close(); + + OpenPgpMetadata metadata = verificationStream.getResult(); + + assertTrue(metadata.isSigned(), "Message is signed, even though we miss the verification cert."); + assertFalse(metadata.isVerified(), "Message is not verified because we lack the verification cert."); + } +}