Skip to content

Commit

Permalink
Add support for creating cleartext signed messages and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vanitasvitae committed Sep 27, 2021
1 parent ece5897 commit 526dc0c
Show file tree
Hide file tree
Showing 6 changed files with 453 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ private void prepareOnePassSignatures() throws IOException, PGPException {
}

private void prepareLiteralDataProcessing() throws IOException {
if (options.isCleartextSigned()) {
SigningOptions.SigningMethod firstMethod = options.getSigningOptions().getSigningMethods().values().iterator().next();
armorOutputStream.beginClearText(firstMethod.getHashAlgorithm().getAlgorithmId());
return;
}
literalDataGenerator = new PGPLiteralDataGenerator();
literalDataStream = literalDataGenerator.open(outermostStream,
options.getEncoding().getCode(),
Expand Down Expand Up @@ -214,9 +219,22 @@ public void close() throws IOException {
}

// Literal Data
literalDataStream.flush();
literalDataStream.close();
literalDataGenerator.close();
if (literalDataStream != null) {
literalDataStream.flush();
literalDataStream.close();
}
if (literalDataGenerator != null) {
literalDataGenerator.close();
}

if (options.isCleartextSigned()) {
// Add linebreak between body and signatures
// TODO: We should only add this line if required.
// I.e. if the message already ends with \n, don't add another linebreak.
armorOutputStream.write('\r');
armorOutputStream.write('\n');
armorOutputStream.endClearText();
}

try {
writeSignatures();
Expand Down Expand Up @@ -260,7 +278,8 @@ private void writeSignatures() throws PGPException, IOException {
PGPSignature signature = signatureGenerator.generate();
if (signingMethod.isDetached()) {
resultBuilder.addDetachedSignature(signingKey, signature);
} else {
}
if (!signingMethod.isDetached() || options.isCleartextSigned()) {
signature.encode(outermostStream);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public final class ProducerOptions {
private String fileName = "";
private Date modificationDate = PGPLiteralData.NOW;
private StreamEncoding streamEncoding = StreamEncoding.BINARY;
private boolean cleartextSigned = false;

private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy()
.defaultCompressionAlgorithm();
Expand Down Expand Up @@ -101,6 +102,9 @@ private static void throwIfNull(SigningOptions signingOptions) {
* @return builder
*/
public ProducerOptions setAsciiArmor(boolean asciiArmor) {
if (cleartextSigned && !asciiArmor) {
throw new IllegalArgumentException("Cleartext signing is enabled. Cannot disable ASCII armoring.");
}
this.asciiArmor = asciiArmor;
return this;
}
Expand All @@ -114,6 +118,28 @@ public boolean isAsciiArmor() {
return asciiArmor;
}

public ProducerOptions setCleartextSigned() {
if (signingOptions == null) {
throw new IllegalArgumentException("Signing Options cannot be null if cleartext signing is enabled.");
}
if (encryptionOptions != null) {
throw new IllegalArgumentException("Cannot encode encrypted message as Cleartext Signed.");
}
for (SigningOptions.SigningMethod method : signingOptions.getSigningMethods().values()) {
if (!method.isDetached()) {
throw new IllegalArgumentException("For cleartext signed message, all signatures must be added as detached signatures.");
}
}
cleartextSigned = true;
asciiArmor = true;
compressionAlgorithmOverride = CompressionAlgorithm.UNCOMPRESSED;
return this;
}

public boolean isCleartextSigned() {
return cleartextSigned;
}

/**
* Set the name of the encrypted file.
* Note: This option cannot be used simultaneously with {@link #setForYourEyesOnly()}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ public final class SigningOptions {
public static final class SigningMethod {
private final PGPSignatureGenerator signatureGenerator;
private final boolean detached;
private final HashAlgorithm hashAlgorithm;

private SigningMethod(PGPSignatureGenerator signatureGenerator, boolean detached) {
private SigningMethod(PGPSignatureGenerator signatureGenerator, boolean detached, HashAlgorithm hashAlgorithm) {
this.signatureGenerator = signatureGenerator;
this.detached = detached;
this.hashAlgorithm = hashAlgorithm;
}

/**
Expand All @@ -65,8 +67,8 @@ private SigningMethod(PGPSignatureGenerator signatureGenerator, boolean detached
* @param signatureGenerator signature generator
* @return inline signing method
*/
public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator) {
return new SigningMethod(signatureGenerator, false);
public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) {
return new SigningMethod(signatureGenerator, false, hashAlgorithm);
}

/**
Expand All @@ -77,8 +79,8 @@ public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGener
* @param signatureGenerator signature generator
* @return detached signing method
*/
public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator) {
return new SigningMethod(signatureGenerator, true);
public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) {
return new SigningMethod(signatureGenerator, true, hashAlgorithm);
}

public boolean isDetached() {
Expand All @@ -88,6 +90,10 @@ public boolean isDetached() {
public PGPSignatureGenerator getSignatureGenerator() {
return signatureGenerator;
}

public HashAlgorithm getHashAlgorithm() {
return hashAlgorithm;
}
}

private final Map<SubkeyIdentifier, SigningMethod> signingMethods = new HashMap<>();
Expand Down Expand Up @@ -266,7 +272,9 @@ private void addSigningMethod(PGPSecretKeyRing secretKey,
PGPSecretKey signingSecretKey = secretKey.getSecretKey(signingSubkey.getKeyID());
PGPSignatureGenerator generator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType);
generator.setUnhashedSubpackets(unhashedSubpackets(signingSecretKey).generate());
SigningMethod signingMethod = detached ? SigningMethod.detachedSignature(generator) : SigningMethod.inlineSignature(generator);
SigningMethod signingMethod = detached ?
SigningMethod.detachedSignature(generator, hashAlgorithm) :
SigningMethod.inlineSignature(generator, hashAlgorithm);
signingMethods.put(signingKeyIdentifier, signingMethod);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.bouncycastle;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test;
import org.pgpainless.algorithm.HashAlgorithm;

public class AsciiArmorDashEscapeTest {

@Test
public void testDashEscapingInCleartextArmor() throws IOException {
String withDash = "- This is a leading dash.\n";
String dashEscaped = "-----BEGIN PGP SIGNED MESSAGE-----\n" +
"Hash: SHA512\n" +
"\n" +
"- - This is a leading dash.\n";
ByteArrayOutputStream out = new ByteArrayOutputStream();
ArmoredOutputStream armor = new ArmoredOutputStream(out);

armor.beginClearText(HashAlgorithm.SHA512.getAlgorithmId());
armor.write(withDash.getBytes(StandardCharsets.UTF_8));
armor.endClearText();
armor.close();

assertArrayEquals(dashEscaped.getBytes(StandardCharsets.UTF_8), out.toByteArray());

ArmoredInputStream armorIn = new ArmoredInputStream(new ByteArrayInputStream(out.toByteArray()));
ByteArrayOutputStream plain = new ByteArrayOutputStream();
Streams.pipeAll(armorIn, plain);
assertEquals(withDash, plain.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* 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.example;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.BufferedInputStream;
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.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.DocumentSignatureType;
import org.pgpainless.decryption_verification.ConsumerOptions;
import org.pgpainless.decryption_verification.DecryptionStream;
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.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.exception.WrongConsumingMethodException;
import org.pgpainless.key.protection.SecretKeyRingProtector;

public class DecryptOrVerify {

private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Version: PGPainless\n" +
"Comment: AA21 9149 3B35 E679 8876 DE43 B0D7 8185 F639 B6C9\n" +
"Comment: Signora <signora@pgpainless.org>\n" +
"\n" +
"lFgEYVGUbRYJKwYBBAHaRw8BAQdAki59UUbUouvfd+4hoSAQ79He7cdmTyYTu3Su\n" +
"9Ww0isQAAQCvyi79y6YNzxdQpN8HLPmBd+zq6o/RNK4cBeN+RJrxiBHbtCBTaWdu\n" +
"b3JhIDxzaWdub3JhQHBncGFpbmxlc3Mub3JnPoh4BBMWCgAgBQJhUZRtAhsBBRYC\n" +
"AwEABRUKCQgLBAsJCAcCHgECGQEACgkQsNeBhfY5tskOqgEA3fDHE1n081xiseTl\n" +
"aXV1A/6aXvsnxVo+Lj35Mn7CarwBAO4PVjHvvUydTla3D5JHhZ0p4P5hSG7kPPrB\n" +
"d3nmbH0InF0EYVGUbRIKKwYBBAGXVQEFAQEHQFzDN2Tuaxim9YFRRXeRZyDC42KF\n" +
"9DSohUXEJ/TrM7MlAwEIBwAA/3h1IaQBIGlNZ6TSsuuryW8KtwdxI4Sd1JDzsVML\n" +
"2SGQEFKIdQQYFgoAHQUCYVGUbQIbDAUWAgMBAAUVCgkICwQLCQgHAh4BAAoJELDX\n" +
"gYX2ObbJBzwBAM4RYBuRsRTmEFTrc7FyAqqSrCVpyLkrnYqPTZriySX0AP9K+N1d\n" +
"LIDRkHW7EbK2ITRu6nemFu00+H1bInTCUVxtAZxYBGFRlG0WCSsGAQQB2kcPAQEH\n" +
"QOzydmmSnNw/NoWi0b0pODLNbT2VUFNFurxBoWj8T2oLAAD+Nbk5mZVQ91pDV6Bp\n" +
"SAjCP9/e7odHtipsdlG9lszzC98RcIjVBBgWCgB9BQJhUZRtAhsCBRYCAwEABRUK\n" +
"CQgLBAsJCAcCHgFfIAQZFgoABgUCYVGUbQAKCRBaxbg/GlrWhx43AP40HxpvHNL5\n" +
"m953hWBxZvzIpt98E8+bfR4rCyHY6A5rzQEA8BUI6oqsEPKlGiETYntk7fFhOIyJ\n" +
"bRH+a/LsdaxjpQwACgkQsNeBhfY5tskKHQEA+aanF6ZnSatjDdiKEehYmbqr4BTc\n" +
"UDnu37YkbgLlqPIBAJrPT5XS9oVa5xMsK+c3shnmPVQuK9r/AGwlligJprYH\n" +
"=JHMt\n" +
"-----END PGP PRIVATE KEY BLOCK-----\n";

private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" +
"Version: PGPainless\n" +
"\n" +
"owGbwMvMyCUWdXSHvVTUtXbG0yJJDCDgkZqTk6+jEJ5flJOiyNVRysIoxsXAxsqU\n" +
"GDiVjUGRUwCmQUyRRWnOn9Z/PIseF3Yz6cCEL05nZDj1OClo75WVTjNmJPemW6qV\n" +
"6ki//1K1++2s0qTP+0N11O4z/BVLDDdxnmQryS+5VXjBX7/0Hxnm/eqeX6Zum35r\n" +
"M8e7ufwA\n" +
"=RDiy\n" +
"-----END PGP MESSAGE-----";
private static final String CLEARTEXT_SIGNED = "-----BEGIN PGP SIGNED MESSAGE-----\n" +
"Hash: SHA512\n" +
"\n" +
"Hello, World!\n" +
"\n" +
"-----BEGIN PGP SIGNATURE-----\n" +
"Version: PGPainless\n" +
"\n" +
"iHUEARYKAAYFAmFR1WIAIQkQWsW4Pxpa1ocWIQQinPyF/gyi43GLAixaxbg/GlrW\n" +
"h7qwAP9Vq0PfDdGpM+n4wfR162XBvvVU8KNl+vJI3u7Ghlj0zwEA1VMgwNnCRb9b\n" +
"QUibivG5Slahz8l7PWnGkxbB2naQxgw=\n" +
"=oNIK\n" +
"-----END PGP SIGNATURE-----";

private static PGPSecretKeyRing secretKey;
private static PGPPublicKeyRing certificate;

@BeforeAll
public static void prepare() throws IOException {
secretKey = PGPainless.readKeyRing().secretKeyRing(KEY);
certificate = PGPainless.extractCertificate(secretKey);
}

@Test
public void verifySignatures() throws PGPException, IOException {
ConsumerOptions options = new ConsumerOptions()
.addVerificationCert(certificate);

for (String signed : new String[] {INBAND_SIGNED, CLEARTEXT_SIGNED}) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8));
BufferedInputStream bufIn = new BufferedInputStream(in);
bufIn.mark(512);
DecryptionStream verificationStream;
try {
verificationStream = PGPainless.decryptAndOrVerify()
.onInputStream(bufIn)
.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);
verificationStream.close();

OpenPgpMetadata metadata = verificationStream.getResult();
assertTrue(metadata.isVerified());
assertArrayEquals("Hello, World!\n".getBytes(StandardCharsets.UTF_8), out.toByteArray());
}
}


@Test
public void createVerifyCleartextSignedMessage() throws PGPException, IOException {
for (String msg : new String[] {"Hello World!", "- Hello - World -", "Hello, World!\n", "Hello\nWorld!"}) {
ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8));
ByteArrayOutputStream out = new ByteArrayOutputStream();
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
.onOutputStream(out)
.withOptions(ProducerOptions.sign(SigningOptions.get()
.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)
).setCleartextSigned());

Streams.pipeAll(in, signingStream);
signingStream.close();

ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray());

DecryptionStream verificationStream = PGPainless.verifyCleartextSignedMessage()
.onInputStream(signedIn)
.withStrategy(new InMemoryMultiPassStrategy())
.withOptions(new ConsumerOptions().addVerificationCert(certificate))
.getVerificationStream();

ByteArrayOutputStream plain = new ByteArrayOutputStream();
Streams.pipeAll(verificationStream, plain);
verificationStream.close();

OpenPgpMetadata metadata = verificationStream.getResult();
assertTrue(metadata.isVerified());
assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plain.toByteArray());
}
}
}
Loading

0 comments on commit 526dc0c

Please sign in to comment.