Add support for creating cleartext signed messages and add tests

This commit is contained in:
Paul Schaub 2021-09-27 17:10:00 +02:00
parent ece5897bae
commit 526dc0caac
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
6 changed files with 453 additions and 10 deletions

View File

@ -153,6 +153,11 @@ public final class EncryptionStream extends OutputStream {
}
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(),
@ -214,9 +219,22 @@ public final class EncryptionStream extends OutputStream {
}
// 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();
@ -260,7 +278,8 @@ public final class EncryptionStream extends OutputStream {
PGPSignature signature = signatureGenerator.generate();
if (signingMethod.isDetached()) {
resultBuilder.addDetachedSignature(signingKey, signature);
} else {
}
if (!signingMethod.isDetached() || options.isCleartextSigned()) {
signature.encode(outermostStream);
}
}

View File

@ -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();
@ -101,6 +102,9 @@ public final class ProducerOptions {
* @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;
}
@ -114,6 +118,28 @@ public final class ProducerOptions {
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()}.

View File

@ -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;
}
/**
@ -65,8 +67,8 @@ public final class SigningOptions {
* @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);
}
/**
@ -77,8 +79,8 @@ public final class SigningOptions {
* @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() {
@ -88,6 +90,10 @@ public final class SigningOptions {
public PGPSignatureGenerator getSignatureGenerator() {
return signatureGenerator;
}
public HashAlgorithm getHashAlgorithm() {
return hashAlgorithm;
}
}
private final Map<SubkeyIdentifier, SigningMethod> signingMethods = new HashMap<>();
@ -266,7 +272,9 @@ public final class SigningOptions {
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);
}

View File

@ -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());
}
}

View File

@ -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());
}
}
}

View File

@ -0,0 +1,165 @@
/*
* 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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
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.encryption_signing.EncryptionResult;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.ArmorUtils;
public class Sign {
private static PGPSecretKeyRing secretKey;
private static SecretKeyRingProtector protector;
@BeforeAll
public static void prepare() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
secretKey = PGPainless.generateKeyRing().modernKeyRing("Emilia Example <emilia@example.org>", null);
protector = SecretKeyRingProtector.unprotectedKeys(); // no password
}
/**
* Demonstration of how to use the PGPainless API to sign some message using inband signatures.
* The result is not human-readable, however the resulting text contains both the signed data and the signatures.
*
* @throws PGPException
* @throws IOException
*/
@Test
public void inbandSignedMessage() throws PGPException, IOException {
String message = "\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.";
InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8));
ByteArrayOutputStream signedOut = new ByteArrayOutputStream();
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
.onOutputStream(signedOut)
.withOptions(ProducerOptions.sign(SigningOptions.get()
.addInlineSignature(protector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT))
);
Streams.pipeAll(messageIn, signingStream);
signingStream.close();
String signedMessage = signedOut.toString();
assertTrue(signedMessage.startsWith("-----BEGIN PGP MESSAGE-----"));
assertTrue(signedMessage.endsWith("-----END PGP MESSAGE-----\n"));
assertFalse(signedMessage.contains("Derivative Works")); // hot human-readable
}
/**
* Demonstration of how to create a detached signature for a message.
* A detached signature can be distributed alongside the message/file itself.
*
* The message/file doesn't need to be altered for detached signature creation.
*
* @throws PGPException
* @throws IOException
*/
@Test
public void detachedSignedMessage() throws PGPException, IOException {
String message = "\"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"";
InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8));
// The output stream below is named 'ignoreMe' because the output of the signing stream can be ignored.
// After signing, you want to distribute the original value of 'message' along with the 'detachedSignature'
// from below.
ByteArrayOutputStream ignoreMe = new ByteArrayOutputStream();
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
.onOutputStream(ignoreMe)
.withOptions(ProducerOptions.sign(SigningOptions.get()
.addDetachedSignature(protector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT))
.setAsciiArmor(false)
);
Streams.pipeAll(messageIn, signingStream);
signingStream.close();
EncryptionResult result = signingStream.getResult();
PGPPublicKey signingKey = PGPainless.inspectKeyRing(secretKey).getSigningSubkeys().get(0);
PGPSignature signature = result.getDetachedSignatures().get(new SubkeyIdentifier(secretKey, signingKey.getKeyID())).iterator().next();
String detachedSignature = ArmorUtils.toAsciiArmoredString(signature.getEncoded());
assertTrue(detachedSignature.startsWith("-----BEGIN PGP SIGNATURE-----"));
// Now distribute 'message' and 'detachedSignature'.
}
/**
* Demonstration of how to sign a text message in a way that keeps the message content
* human-readable by utilizing the OpenPGP Cleartext Signature Framework.
* The resulting message contains the original (dash-escaped) message and the signatures.
*
* @throws PGPException
* @throws IOException
*/
@Test
public void cleartextSignedMessage() throws PGPException, IOException {
String message = "" +
"Copyright [yyyy] [name of copyright owner]\n" +
"\n" +
"Licensed under the Apache License, Version 2.0 (the \"License\");\n" +
"you may not use this file except in compliance with the License.\n" +
"You may obtain a copy of the License at\n" +
"\n" +
" http://www.apache.org/licenses/LICENSE-2.0\n" +
"\n" +
"Unless required by applicable law or agreed to in writing, software\n" +
"distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
"See the License for the specific language governing permissions and\n" +
"limitations under the License.";
InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8));
ByteArrayOutputStream signedOut = new ByteArrayOutputStream();
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
.onOutputStream(signedOut)
.withOptions(ProducerOptions.sign(SigningOptions.get()
.addDetachedSignature(protector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) // Human-readable text document
.setCleartextSigned() // <- Explicitly use Cleartext Signature Framework!!!
);
Streams.pipeAll(messageIn, signingStream);
signingStream.close();
String signedMessage = signedOut.toString();
assertTrue(signedMessage.startsWith("-----BEGIN PGP SIGNED MESSAGE-----"));
assertTrue(signedMessage.contains("WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND")); // msg is human readable
assertTrue(signedMessage.endsWith("-----END PGP SIGNATURE-----\n"));
}
}