From 5375cd454f5f0c9943343508453a2568eab59d14 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 16:56:32 +0200 Subject: [PATCH] Implement inline-detach for 3 different types of input --- .../org/pgpainless/sop/InlineDetachImpl.java | 85 +++++++++++++- .../org/pgpainless/sop/InlineDetachTest.java | 105 +++++++++++++++++- 2 files changed, 180 insertions(+), 10 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index 517f9e9e..178018aa 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -10,12 +10,20 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.util.io.Streams; +import org.pgpainless.decryption_verification.OpenPgpInputStream; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.exception.WrongConsumingMethodException; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.ReadyWithResult; import sop.Signatures; @@ -43,13 +51,80 @@ public class InlineDetachImpl implements InlineDetach { public Signatures writeTo(OutputStream messageOutputStream) throws SOPGPException.NoSignature, IOException { - PGPSignatureList signatures; - try { - signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(messageInputStream, messageOutputStream); - } catch (WrongConsumingMethodException e) { - throw new IOException(e); + PGPSignatureList signatures = null; + OpenPgpInputStream pgpIn = new OpenPgpInputStream(messageInputStream); + + if (pgpIn.isNonOpenPgp()) { + throw new SOPGPException.BadData("Data appears to be non-OpenPGP."); } + // handle ASCII armor + if (pgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = new ArmoredInputStream(pgpIn); + + // Handle cleartext signature framework + if (armorIn.isClearText()) { + try { + signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, messageOutputStream); + if (signatures == null) { + throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); + } + } catch (WrongConsumingMethodException e) { + throw new SOPGPException.BadData(e); + } + } + // else just dearmor + pgpIn = new OpenPgpInputStream(armorIn); + } + + // if data was not using cleartext signatures framework + if (signatures == null) { + + if (!pgpIn.isBinaryOpenPgp()) { + throw new SOPGPException.BadData("Data was containing ASCII armored non-OpenPGP data."); + } + + // handle binary OpenPGP data + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn); + Object next; + while ((next = objectFactory.nextObject()) != null) { + + if (next instanceof PGPOnePassSignatureList) { + // skip over ops + continue; + } + + if (next instanceof PGPLiteralData) { + // write out contents of literal data packet + PGPLiteralData literalData = (PGPLiteralData) next; + InputStream literalIn = literalData.getDataStream(); + Streams.pipeAll(literalIn, messageOutputStream); + literalIn.close(); + continue; + } + + if (next instanceof PGPCompressedData) { + // decompress compressed data + PGPCompressedData compressedData = (PGPCompressedData) next; + try { + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); + } catch (PGPException e) { + throw new SOPGPException.BadData("Cannot decompress PGPCompressedData", e); + } + continue; + } + + if (next instanceof PGPSignatureList) { + signatures = (PGPSignatureList) next; + } + } + } + + if (signatures == null) { + throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); + } + + // write out signatures if (armor) { ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut); for (PGPSignature signature : signatures) { diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java index 9df0345a..b4bfe815 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -11,29 +11,44 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.List; +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; 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.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.protection.SecretKeyRingProtector; import sop.ByteArrayAndResult; import sop.SOP; import sop.Signatures; import sop.Verification; import sop.enums.InlineSignAs; +import sop.exception.SOPGPException; public class InlineDetachTest { private static final SOP sop = new SOPImpl(); + + /** + * Construct a message which is signed using the cleartext signature framework. + * The message consists of an armor header followed by the dash-escaped message data, followed by an armored signature. + * + * Detaching must result in the unescaped message data plus the signature packet. + * Verifying the signature must work. + * + * @throws IOException in case of an IO error + */ @Test public void detachCleartextSignedMessage() throws IOException { byte[] key = sop.generateKey() @@ -70,6 +85,16 @@ public class InlineDetachTest { assertArrayEquals(data, message); } + /** + * Construct a message which is inline-signed. + * The message consists of a compressed data packet containing an OnePassSignature, a literal data packet and + * a signature packet. + * + * Detaching the message must result in the contents of the literal data packet, plus the signature packet. + * Verification must work. + * + * @throws IOException in case of an IO error + */ @Test public void detachInbandSignedMessage() throws IOException { byte[] key = sop.generateKey() @@ -100,4 +125,74 @@ public class InlineDetachTest { assertEquals(1, verificationList.size()); assertArrayEquals(data, message); } + + /** + * Construct a message which consists of a literal data packet followed by a signatures block. + * Detaching it must result in the contents of the literal data packet plus the signatures block. + * + * Verification must still work. + * + * @throws IOException in case of an IO error + */ + @Test + public void detachOpenPgpMessage() throws IOException { + byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] cert = sop.extractCert().key(key).getBytes(); + + byte[] data = "Hello, World\n".getBytes(StandardCharsets.UTF_8); + byte[] inlineSigned = sop.inlineSign() + .key(key) + .data(data).getBytes(); + + ByteArrayOutputStream literalDataAndSignatures = new ByteArrayOutputStream(); + ArmoredInputStream armorIn = new ArmoredInputStream(new ByteArrayInputStream(inlineSigned)); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(armorIn); + Object next; + while ((next = objectFactory.nextObject()) != null) { + if (next instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) next; + try { + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); + } catch (PGPException e) { + throw new SOPGPException.BadData("Cannot decompress compressed data", e); + } + continue; + } + if (next instanceof PGPLiteralData) { + PGPLiteralData litDat = (PGPLiteralData) next; + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(literalDataAndSignatures, (char) litDat.getFormat(), litDat.getFileName(), litDat.getModificationTime(), new byte[8192]); + Streams.pipeAll(litDat.getDataStream(), litOut); + litOut.close(); + continue; + } + + if (next instanceof PGPSignatureList) { + PGPSignatureList signatures = (PGPSignatureList) next; + for (PGPSignature signature : signatures) { + signature.encode(literalDataAndSignatures); + } + } + } + + // actually detach the message + ByteArrayAndResult detachedMsg = sop.inlineDetach() + .message(literalDataAndSignatures.toByteArray()) + .toByteArrayAndResult(); + + byte[] message = detachedMsg.getBytes(); + byte[] signature = detachedMsg.getResult().getBytes(); + + List verificationList = sop.verify() + .cert(cert) + .signatures(signature) + .data(message); + + assertFalse(verificationList.isEmpty()); + assertEquals(1, verificationList.size()); + assertArrayEquals(data, message); + } }