Simplify handling of cleartext-signed data

This commit is contained in:
Paul Schaub 2022-02-19 15:41:43 +01:00
parent db58280db6
commit 1753cef10e
8 changed files with 55 additions and 242 deletions

View File

@ -53,7 +53,6 @@ public class ConsumerOptions {
private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE;
private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy();
private boolean cleartextSigned;
/**
* Consider signatures on the message made before the given timestamp invalid.
@ -346,21 +345,4 @@ public class ConsumerOptions {
public MultiPassStrategy getMultiPassStrategy() {
return multiPassStrategy;
}
/**
* INTERNAL method to mark cleartext signed messages.
* Do not call this manually.
*/
public ConsumerOptions setIsCleartextSigned() {
this.cleartextSigned = true;
return this;
}
/**
* Return true if the message is cleartext signed.
* @return cleartext signed
*/
public boolean isCleartextSigned() {
return this.cleartextSigned;
}
}

View File

@ -4,19 +4,14 @@
package org.pgpainless.decryption_verification;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPException;
import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl;
import org.pgpainless.exception.WrongConsumingMethodException;
public class DecryptionBuilder implements DecryptionBuilderInterface {
public static int BUFFER_SIZE = 4096;
@Override
public DecryptWith onInputStream(@Nonnull InputStream inputStream) {
return new DecryptWithImpl(inputStream);
@ -24,11 +19,10 @@ public class DecryptionBuilder implements DecryptionBuilderInterface {
static class DecryptWithImpl implements DecryptWith {
private final BufferedInputStream inputStream;
private final InputStream inputStream;
DecryptWithImpl(InputStream inputStream) {
this.inputStream = new BufferedInputStream(inputStream, BUFFER_SIZE);
this.inputStream.mark(BUFFER_SIZE);
this.inputStream = inputStream;
}
@Override
@ -37,15 +31,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface {
throw new IllegalArgumentException("Consumer options cannot be null.");
}
try {
return DecryptionStreamFactory.create(inputStream, consumerOptions);
} catch (WrongConsumingMethodException e) {
inputStream.reset();
return new VerifyCleartextSignaturesImpl()
.onInputStream(inputStream)
.withOptions(consumerOptions)
.getVerificationStream();
}
return DecryptionStreamFactory.create(inputStream, consumerOptions);
}
}
}

View File

@ -36,6 +36,7 @@ import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSessionKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
@ -46,6 +47,8 @@ import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.EncryptionPurpose;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.exception.FinalIOException;
import org.pgpainless.exception.MessageNotIntegrityProtectedException;
import org.pgpainless.exception.MissingDecryptionMethodException;
@ -53,7 +56,6 @@ import org.pgpainless.exception.MissingLiteralDataException;
import org.pgpainless.exception.MissingPassphraseException;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.exception.UnacceptableAlgorithmException;
import org.pgpainless.exception.WrongConsumingMethodException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
@ -62,6 +64,7 @@ import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.signature.consumer.DetachedSignatureCheck;
import org.pgpainless.signature.consumer.OnePassSignatureCheck;
import org.pgpainless.util.ArmoredInputStreamFactory;
import org.pgpainless.util.CRCingArmoredInputStreamWrapper;
import org.pgpainless.util.PGPUtilWrapper;
import org.pgpainless.util.Passphrase;
@ -77,6 +80,9 @@ public final class DecryptionStreamFactory {
// Maximum nesting depth of packets (e.g. compression, encryption...)
private static final int MAX_PACKET_NESTING_DEPTH = 16;
// Buffer Size for BufferedInputStreams
public static int BUFFER_SIZE = 4096;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
private final List<OnePassSignatureCheck> onePassSignatureChecks = new ArrayList<>();
@ -92,7 +98,8 @@ public final class DecryptionStreamFactory {
@Nonnull ConsumerOptions options)
throws PGPException, IOException {
DecryptionStreamFactory factory = new DecryptionStreamFactory(options);
return factory.parseOpenPGPDataAndCreateDecryptionStream(inputStream);
BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, BUFFER_SIZE);
return factory.parseOpenPGPDataAndCreateDecryptionStream(bufferedIn);
}
public DecryptionStreamFactory(ConsumerOptions options) {
@ -125,44 +132,34 @@ public final class DecryptionStreamFactory {
}
}
private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(InputStream inputStream)
private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(BufferedInputStream bufferedIn)
throws IOException, PGPException {
// Make sure we handle armored and non-armored data properly
BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, 512);
bufferedIn.mark(512);
InputStream decoderStream;
InputStream pgpInStream;
InputStream outerDecodingStream;
PGPObjectFactory objectFactory;
// Workaround for cleartext signed data
// If we below threw a WrongConsumingMethodException, the CleartextSignatureProcessor will prepare the
// message for us and will set options.isCleartextSigned() to true.
// That way we can process long messages without running the issue of resetting the bufferedInputStream
// to invalid marks.
if (options.isCleartextSigned()) {
inputStream = wrapInVerifySignatureStream(bufferedIn, null);
return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream,
null);
}
try {
decoderStream = PGPUtilWrapper.getDecoderStream(bufferedIn);
decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream);
outerDecodingStream = PGPUtilWrapper.getDecoderStream(bufferedIn);
outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(outerDecodingStream);
if (decoderStream instanceof ArmoredInputStream) {
ArmoredInputStream armor = (ArmoredInputStream) decoderStream;
if (outerDecodingStream instanceof ArmoredInputStream) {
ArmoredInputStream armor = (ArmoredInputStream) outerDecodingStream;
// Cleartext Signed Message
// Throw a WrongConsumingMethodException to delegate preparation (extraction of signatures)
// to the CleartextSignatureProcessor which will call us again (see comment above)
if (armor.isClearText()) {
throw new WrongConsumingMethodException("Message appears to be using the Cleartext Signature Framework. " +
"Use PGPainless.verifyCleartextSignedMessage() to verify this message instead.");
bufferedIn.reset();
return parseCleartextSignedMessage(bufferedIn);
}
}
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream);
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
// Parse OpenPGP message
inputStream = processPGPPackets(objectFactory, 1);
pgpInStream = processPGPPackets(objectFactory, 1);
return new DecryptionStream(pgpInStream,
resultBuilder, integrityProtectedEncryptedInputStream,
(outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null);
} catch (EOFException | FinalIOException e) {
// Broken message or invalid decryption session key
throw e;
@ -172,23 +169,44 @@ public final class DecryptionStreamFactory {
// to allow for detached signature verification.
LOGGER.debug("The message appears to not be an OpenPGP message. This is probably data signed with detached signatures?");
bufferedIn.reset();
decoderStream = bufferedIn;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream);
inputStream = wrapInVerifySignatureStream(bufferedIn, objectFactory);
outerDecodingStream = bufferedIn;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
pgpInStream = wrapInVerifySignatureStream(bufferedIn, objectFactory);
} catch (IOException e) {
if (e.getMessage().contains("invalid armor") || e.getMessage().contains("invalid header encountered")) {
// We falsely assumed the data to be armored.
LOGGER.debug("The message is apparently not armored.");
bufferedIn.reset();
decoderStream = bufferedIn;
inputStream = wrapInVerifySignatureStream(bufferedIn, null);
outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(bufferedIn);
pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null);
} else {
throw new FinalIOException(e);
}
}
return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream,
(decoderStream instanceof ArmoredInputStream) ? decoderStream : null);
return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream,
(outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null);
}
private DecryptionStream parseCleartextSignedMessage(BufferedInputStream in)
throws IOException, PGPException {
resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)
.setFileEncoding(StreamEncoding.TEXT);
ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(in);
MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy();
PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream());
for (PGPSignature signature : signatures) {
options.addVerificationOfDetachedSignature(signature);
}
initializeDetachedSignatures(options.getDetachedSignatures());
InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null);
return new DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream,
null);
}
private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) {

View File

@ -1,75 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.cleartext_signatures;
import java.io.IOException;
import java.io.InputStream;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.util.ArmoredInputStreamFactory;
/**
* Processor for cleartext-signed messages.
*/
public class CleartextSignatureProcessor {
private final ArmoredInputStream in;
private final ConsumerOptions options;
public CleartextSignatureProcessor(InputStream inputStream,
ConsumerOptions options)
throws IOException {
if (inputStream instanceof ArmoredInputStream) {
this.in = (ArmoredInputStream) inputStream;
} else {
this.in = ArmoredInputStreamFactory.get(inputStream);
}
this.options = options;
}
/**
* Perform the first pass of cleartext signed message processing:
* Unpack the message from the ascii armor and detach signatures.
* The plaintext message is being written to cache/disk according to the used {@link MultiPassStrategy}.
*
* The result of this method is a {@link DecryptionStream} which will perform the second pass.
* It again outputs the plaintext message and performs signature verification.
*
* The result of {@link DecryptionStream#getResult()} contains information about the messages signatures.
*
* @return validated signature
* @throws IOException if the signature cannot be read.
* @throws PGPException if the signature cannot be initialized.
* @throws SignatureValidationException if the signature is invalid.
*/
public DecryptionStream getVerificationStream() throws IOException, PGPException {
OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)
.setFileEncoding(StreamEncoding.TEXT);
MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy();
PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream());
for (PGPSignature signature : signatures) {
options.addVerificationOfDetachedSignature(signature);
}
options.setIsCleartextSigned();
return PGPainless.decryptAndOrVerify()
.onInputStream(multiPassStrategy.getMessageInputStream())
.withOptions(options);
}
}

View File

@ -11,7 +11,7 @@ import java.io.InputStream;
import java.io.OutputStream;
/**
* Since the {@link CleartextSignatureProcessor} needs to read the whole data twice in order to verify signatures,
* Since for verification of cleartext signed messages, we need to read the whole data twice in order to verify signatures,
* a strategy for how to cache the read data is required.
* Otherwise, large data kept in memory could cause {@link OutOfMemoryError OutOfMemoryErrors} or other issues.
*

View File

@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.cleartext_signatures;
import java.io.IOException;
import java.io.InputStream;
import org.pgpainless.decryption_verification.ConsumerOptions;
/**
* Interface defining the API for verification of cleartext signed documents.
*/
public interface VerifyCleartextSignatures {
/**
* Provide the {@link InputStream} which contains the cleartext-signed message.
* @param inputStream inputstream
* @return api handle
*/
VerifyWith onInputStream(InputStream inputStream);
interface VerifyWith {
/**
* Pass in consumer options like verification certificates, acceptable date ranges etc.
*
* @param options options
* @return processor
* @throws IOException in case of an IO error
*/
CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException;
}
}

View File

@ -1,30 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.cleartext_signatures;
import java.io.IOException;
import java.io.InputStream;
import org.pgpainless.decryption_verification.ConsumerOptions;
public class VerifyCleartextSignaturesImpl implements VerifyCleartextSignatures {
private InputStream inputStream;
@Override
public VerifyWithImpl onInputStream(InputStream inputStream) {
VerifyCleartextSignaturesImpl.this.inputStream = inputStream;
return new VerifyWithImpl();
}
public class VerifyWithImpl implements VerifyWith {
@Override
public CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException {
return new CleartextSignatureProcessor(inputStream, options);
}
}
}

View File

@ -6,7 +6,6 @@ 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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
@ -30,11 +29,9 @@ import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.exception.WrongConsumingMethodException;
import org.pgpainless.key.TestKeys;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.signature.consumer.CertificateValidator;
@ -180,35 +177,6 @@ public class CleartextSignatureVerificationTest {
assertEquals(1, metadata.getVerifiedSignatures().size());
}
@Test
public void consumingInlineSignedMessageWithCleartextSignedVerificationApiThrowsWrongConsumingMethodException()
throws IOException {
String inlineSignedMessage = "-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"kA0DAQoTVzbmkxrPNwwBy8BJYgAAAAAAQWgsIEp1bGlldCwgaWYgdGhlIG1lYXN1\n" +
"cmUgb2YgdGh5IGpveQpCZSBoZWFwZWQgbGlrZSBtaW5lLCBhbmQgdGhhdCB0aHkg\n" +
"c2tpbGwgYmUgbW9yZQpUbyBibGF6b24gaXQsIHRoZW4gc3dlZXRlbiB3aXRoIHRo\n" +
"eSBicmVhdGgKVGhpcyBuZWlnaGJvciBhaXIsIGFuZCBsZXQgcmljaCBtdXNpY+KA\n" +
"mXMgdG9uZ3VlClVuZm9sZCB0aGUgaW1hZ2luZWQgaGFwcGluZXNzIHRoYXQgYm90\n" +
"aApSZWNlaXZlIGluIGVpdGhlciBieSB0aGlzIGRlYXIgZW5jb3VudGVyLoh1BAET\n" +
"CgAGBQJhK2q9ACEJEFc25pMazzcMFiEET2ZcTcLEZgvGQl5BVzbmkxrPNwxr8gD+\n" +
"MDfg+qccpsoJVgHIW8mRPBQowXDyw+oNHsf28ii+/pEBAO/RXhFkZBPzlfDJMJVT\n" +
"UwJJeuna1R4yOoWjq0zqRvrg\n" +
"=dBiV\n" +
"-----END PGP MESSAGE-----\n";
PGPPublicKeyRing certificate = TestKeys.getEmilPublicKeyRing();
ConsumerOptions options = new ConsumerOptions()
.addVerificationCert(certificate);
assertThrows(WrongConsumingMethodException.class, () ->
new VerifyCleartextSignaturesImpl()
.onInputStream(new ByteArrayInputStream(inlineSignedMessage.getBytes(StandardCharsets.UTF_8)))
.withOptions(options)
.getVerificationStream());
}
@Test
public void getDecoderStreamMistakensPlaintextForBase64RegressionTest()
throws PGPException, IOException {