1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-11 19:29:39 +02:00

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

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