1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-11-26 22:32:07 +01:00

Hide distinction between clearsigned and inline signed message verification

This commit is contained in:
Paul Schaub 2021-11-02 12:12:29 +01:00
parent bd67d9c0fa
commit 59c9ec341e
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
9 changed files with 69 additions and 115 deletions

View file

@ -22,8 +22,6 @@ import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterfac
import org.pgpainless.key.parsing.KeyRingReader; import org.pgpainless.key.parsing.KeyRingReader;
import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.policy.Policy; import org.pgpainless.policy.Policy;
import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignatures;
import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl;
import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.ArmorUtils;
public final class PGPainless { public final class PGPainless {
@ -91,26 +89,6 @@ public final class PGPainless {
return new DecryptionBuilder(); return new DecryptionBuilder();
} }
/**
* Verify a cleartext-signed message.
* Cleartext signed messages are often found in emails and look like this:
* <pre>
* {@code
* -----BEGIN PGP SIGNED MESSAGE-----
* Hash: [Hash algorithm]
* [Human Readable Message Body]
* -----BEGIN PGP SIGNATURE-----
* [Signature]
* -----END PGP SIGNATURE-----
* }
* </pre>
*
* @return builder
*/
public static VerifyCleartextSignatures verifyCleartextSignedMessage() {
return new VerifyCleartextSignaturesImpl();
}
/** /**
* Make changes to a key ring. * Make changes to a key ring.
* This method can be used to change key expiration dates and passphrases, or add/remove/revoke subkeys. * This method can be used to change key expiration dates and passphrases, or add/remove/revoke subkeys.

View file

@ -22,6 +22,8 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.exception.NotYetImplementedException; import org.pgpainless.exception.NotYetImplementedException;
import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.SignatureUtils;
@ -50,6 +52,7 @@ public class ConsumerOptions {
private final Set<Passphrase> decryptionPassphrases = new HashSet<>(); private final Set<Passphrase> decryptionPassphrases = new HashSet<>();
private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE; private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE;
private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy();
/** /**
* Consider signatures on the message made before the given timestamp invalid. * Consider signatures on the message made before the given timestamp invalid.
@ -327,4 +330,26 @@ public class ConsumerOptions {
MissingKeyPassphraseStrategy getMissingKeyPassphraseStrategy() { MissingKeyPassphraseStrategy getMissingKeyPassphraseStrategy() {
return missingKeyPassphraseStrategy; return missingKeyPassphraseStrategy;
} }
/**
* Set a custom multi-pass strategy for processing cleartext-signed messages.
* Uses {@link InMemoryMultiPassStrategy} by default.
*
* @param multiPassStrategy multi-pass caching strategy
* @return builder
*/
public ConsumerOptions setMultiPassStrategy(@Nonnull MultiPassStrategy multiPassStrategy) {
this.multiPassStrategy = multiPassStrategy;
return this;
}
/**
* Return the currently configured {@link MultiPassStrategy}.
* Defaults to {@link InMemoryMultiPassStrategy}.
*
* @return multi-pass strategy
*/
public MultiPassStrategy getMultiPassStrategy() {
return multiPassStrategy;
}
} }

View file

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

View file

@ -121,7 +121,8 @@ public final class DecryptionStreamFactory {
private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(InputStream inputStream) throws IOException, PGPException { private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(InputStream inputStream) throws IOException, PGPException {
// Make sure we handle armored and non-armored data properly // Make sure we handle armored and non-armored data properly
BufferedInputStream bufferedIn = new BufferedInputStream(inputStream); BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, 512);
bufferedIn.mark(512);
InputStream decoderStream; InputStream decoderStream;
PGPObjectFactory objectFactory; PGPObjectFactory objectFactory;

View file

@ -31,11 +31,9 @@ public class CleartextSignatureProcessor {
private final ArmoredInputStream in; private final ArmoredInputStream in;
private final ConsumerOptions options; private final ConsumerOptions options;
private final MultiPassStrategy multiPassStrategy;
public CleartextSignatureProcessor(InputStream inputStream, public CleartextSignatureProcessor(InputStream inputStream,
ConsumerOptions options, ConsumerOptions options)
MultiPassStrategy multiPassStrategy)
throws IOException { throws IOException {
if (inputStream instanceof ArmoredInputStream) { if (inputStream instanceof ArmoredInputStream) {
this.in = (ArmoredInputStream) inputStream; this.in = (ArmoredInputStream) inputStream;
@ -43,7 +41,6 @@ public class CleartextSignatureProcessor {
this.in = ArmoredInputStreamFactory.get(inputStream); this.in = ArmoredInputStreamFactory.get(inputStream);
} }
this.options = options; this.options = options;
this.multiPassStrategy = multiPassStrategy;
} }
/** /**
@ -67,6 +64,7 @@ public class CleartextSignatureProcessor {
.setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm.NULL) .setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm.NULL)
.setFileEncoding(StreamEncoding.TEXT); .setFileEncoding(StreamEncoding.TEXT);
MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy();
PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream()); PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream());
for (PGPSignature signature : signatures) { for (PGPSignature signature : signatures) {

View file

@ -4,7 +4,6 @@
package org.pgpainless.decryption_verification.cleartext_signatures; package org.pgpainless.decryption_verification.cleartext_signatures;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -20,23 +19,7 @@ public interface VerifyCleartextSignatures {
* @param inputStream inputstream * @param inputStream inputstream
* @return api handle * @return api handle
*/ */
WithStrategy onInputStream(InputStream inputStream); VerifyWith onInputStream(InputStream inputStream);
interface WithStrategy {
/**
* Provide a {@link MultiPassStrategy} which is used to store the message content.
* Since cleartext-signed messages cannot be processed in one pass, the message has to be passed twice.
* Therefore the user needs to decide upon a strategy where to cache/store the message between the passes.
* This could be {@link MultiPassStrategy#writeMessageToFile(File)} or {@link MultiPassStrategy#keepMessageInMemory()},
* depending on message size and use-case.
*
* @param multiPassStrategy strategy
* @return api handle
*/
VerifyWith withStrategy(MultiPassStrategy multiPassStrategy);
}
interface VerifyWith { interface VerifyWith {

View file

@ -12,31 +12,18 @@ import org.pgpainless.decryption_verification.ConsumerOptions;
public class VerifyCleartextSignaturesImpl implements VerifyCleartextSignatures { public class VerifyCleartextSignaturesImpl implements VerifyCleartextSignatures {
private InputStream inputStream; private InputStream inputStream;
private MultiPassStrategy multiPassStrategy;
@Override @Override
public WithStrategy onInputStream(InputStream inputStream) { public VerifyWithImpl onInputStream(InputStream inputStream) {
VerifyCleartextSignaturesImpl.this.inputStream = inputStream; VerifyCleartextSignaturesImpl.this.inputStream = inputStream;
return new WithStrategyImpl(); return new VerifyWithImpl();
}
public class WithStrategyImpl implements WithStrategy {
@Override
public VerifyWith withStrategy(MultiPassStrategy multiPassStrategy) {
if (multiPassStrategy == null) {
throw new NullPointerException("MultiPassStrategy cannot be null.");
}
VerifyCleartextSignaturesImpl.this.multiPassStrategy = multiPassStrategy;
return new VerifyWithImpl();
}
} }
public class VerifyWithImpl implements VerifyWith { public class VerifyWithImpl implements VerifyWith {
@Override @Override
public CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException { public CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException {
return new CleartextSignatureProcessor(inputStream, options, multiPassStrategy); return new CleartextSignatureProcessor(inputStream, options);
} }
} }

View file

@ -25,9 +25,9 @@ import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless; import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.decryption_verification.cleartext_signatures.CleartextSignatureProcessor;
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; 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.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.encryption_signing.SigningOptions;
@ -79,12 +79,11 @@ public class CleartextSignatureVerificationTest {
.addVerificationCert(signingKeys); .addVerificationCert(signingKeys);
InMemoryMultiPassStrategy multiPassStrategy = MultiPassStrategy.keepMessageInMemory(); InMemoryMultiPassStrategy multiPassStrategy = MultiPassStrategy.keepMessageInMemory();
CleartextSignatureProcessor processor = PGPainless.verifyCleartextSignedMessage() options.setMultiPassStrategy(multiPassStrategy);
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED))
.withStrategy(multiPassStrategy)
.withOptions(options); .withOptions(options);
DecryptionStream decryptionStream = processor.getVerificationStream();
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.pipeAll(decryptionStream, out); Streams.pipeAll(decryptionStream, out);
decryptionStream.close(); decryptionStream.close();
@ -107,13 +106,11 @@ public class CleartextSignatureVerificationTest {
File tempDir = TestUtils.createTempDirectory(); File tempDir = TestUtils.createTempDirectory();
File file = new File(tempDir, "file"); File file = new File(tempDir, "file");
MultiPassStrategy multiPassStrategy = MultiPassStrategy.writeMessageToFile(file); MultiPassStrategy multiPassStrategy = MultiPassStrategy.writeMessageToFile(file);
CleartextSignatureProcessor processor = PGPainless.verifyCleartextSignedMessage() options.setMultiPassStrategy(multiPassStrategy);
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED))
.withStrategy(multiPassStrategy)
.withOptions(options); .withOptions(options);
DecryptionStream decryptionStream = processor.getVerificationStream();
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.pipeAll(decryptionStream, out); Streams.pipeAll(decryptionStream, out);
decryptionStream.close(); decryptionStream.close();
@ -173,18 +170,6 @@ public class CleartextSignatureVerificationTest {
assertEquals(1, metadata.getVerifiedSignatures().size()); assertEquals(1, metadata.getVerifiedSignatures().size());
} }
@Test
public void consumingCleartextSignedMessageWithNormalAPIThrowsWrongConsumingMethodException() throws IOException, PGPException {
PGPPublicKeyRing certificate = TestKeys.getEmilPublicKeyRing();
ConsumerOptions options = new ConsumerOptions()
.addVerificationCert(certificate);
assertThrows(WrongConsumingMethodException.class, () ->
PGPainless.decryptAndOrVerify()
.onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED))
.withOptions(options));
}
@Test @Test
public void consumingInlineSignedMessageWithCleartextSignedVerificationApiThrowsWrongConsumingMethodException() throws PGPException, IOException { public void consumingInlineSignedMessageWithCleartextSignedVerificationApiThrowsWrongConsumingMethodException() throws PGPException, IOException {
String inlineSignedMessage = "-----BEGIN PGP MESSAGE-----\n" + String inlineSignedMessage = "-----BEGIN PGP MESSAGE-----\n" +
@ -207,11 +192,10 @@ public class CleartextSignatureVerificationTest {
.addVerificationCert(certificate); .addVerificationCert(certificate);
assertThrows(WrongConsumingMethodException.class, () -> assertThrows(WrongConsumingMethodException.class, () ->
PGPainless.verifyCleartextSignedMessage() new VerifyCleartextSignaturesImpl()
.onInputStream(new ByteArrayInputStream(inlineSignedMessage.getBytes(StandardCharsets.UTF_8))) .onInputStream(new ByteArrayInputStream(inlineSignedMessage.getBytes(StandardCharsets.UTF_8)))
.withStrategy(new InMemoryMultiPassStrategy()) .withOptions(options)
.withOptions(options) .getVerificationStream());
.getVerificationStream());
} }
@Test @Test
@ -223,7 +207,7 @@ public class CleartextSignatureVerificationTest {
ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); ByteArrayOutputStream signedOut = new ByteArrayOutputStream();
EncryptionStream signingStream = PGPainless.encryptAndOrSign().onOutputStream(signedOut) EncryptionStream signingStream = PGPainless.encryptAndOrSign().onOutputStream(signedOut)
.withOptions(ProducerOptions.sign(SigningOptions.get() .withOptions(ProducerOptions.sign(SigningOptions.get()
.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT))
.setCleartextSigned()); .setCleartextSigned());
Streams.pipeAll(msgIn, signingStream); Streams.pipeAll(msgIn, signingStream);
@ -232,12 +216,10 @@ public class CleartextSignatureVerificationTest {
String signed = signedOut.toString(); String signed = signedOut.toString();
ByteArrayInputStream signedIn = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8)); ByteArrayInputStream signedIn = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8));
DecryptionStream verificationStream = PGPainless.verifyCleartextSignedMessage() DecryptionStream verificationStream = PGPainless.decryptAndOrVerify()
.onInputStream(signedIn) .onInputStream(signedIn)
.withStrategy(new InMemoryMultiPassStrategy())
.withOptions(new ConsumerOptions() .withOptions(new ConsumerOptions()
.addVerificationCert(TestKeys.getEmilPublicKeyRing())) .addVerificationCert(TestKeys.getEmilPublicKeyRing()));
.getVerificationStream();
ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); ByteArrayOutputStream msgOut = new ByteArrayOutputStream();
Streams.pipeAll(verificationStream, msgOut); Streams.pipeAll(verificationStream, msgOut);

View file

@ -7,7 +7,6 @@ package org.pgpainless.example;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@ -24,11 +23,9 @@ import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.OpenPgpMetadata;
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy;
import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.exception.WrongConsumingMethodException;
import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector;
public class DecryptOrVerify { public class DecryptOrVerify {
@ -97,22 +94,10 @@ public class DecryptOrVerify {
for (String signed : new String[] {INBAND_SIGNED, CLEARTEXT_SIGNED}) { for (String signed : new String[] {INBAND_SIGNED, CLEARTEXT_SIGNED}) {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8)); ByteArrayInputStream in = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8));
BufferedInputStream bufIn = new BufferedInputStream(in);
bufIn.mark(512);
DecryptionStream verificationStream; DecryptionStream verificationStream;
try {
verificationStream = PGPainless.decryptAndOrVerify() verificationStream = PGPainless.decryptAndOrVerify()
.onInputStream(bufIn) .onInputStream(in)
.withOptions(options); .withOptions(options);
} catch (WrongConsumingMethodException e) {
bufIn.reset();
// Cleartext Signed Message
verificationStream = PGPainless.verifyCleartextSignedMessage()
.onInputStream(bufIn)
.withStrategy(new InMemoryMultiPassStrategy())
.withOptions(options)
.getVerificationStream();
}
Streams.pipeAll(verificationStream, out); Streams.pipeAll(verificationStream, out);
verificationStream.close(); verificationStream.close();
@ -140,11 +125,9 @@ public class DecryptOrVerify {
ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray());
DecryptionStream verificationStream = PGPainless.verifyCleartextSignedMessage() DecryptionStream verificationStream = PGPainless.decryptAndOrVerify()
.onInputStream(signedIn) .onInputStream(signedIn)
.withStrategy(new InMemoryMultiPassStrategy()) .withOptions(new ConsumerOptions().addVerificationCert(certificate));
.withOptions(new ConsumerOptions().addVerificationCert(certificate))
.getVerificationStream();
ByteArrayOutputStream plain = new ByteArrayOutputStream(); ByteArrayOutputStream plain = new ByteArrayOutputStream();
Streams.pipeAll(verificationStream, plain); Streams.pipeAll(verificationStream, plain);