diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 8a1270e1..8b63af66 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -33,6 +33,8 @@ import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor; import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; import org.pgpainless.key.parsing.KeyRingReader; import org.pgpainless.policy.Policy; +import org.pgpainless.signature.cleartext_signatures.VerifyCleartextSignatures; +import org.pgpainless.signature.cleartext_signatures.VerifyCleartextSignaturesImpl; import org.pgpainless.symmetric_encryption.SymmetricEncryptorDecryptor; import org.pgpainless.util.Passphrase; @@ -100,12 +102,22 @@ public class PGPainless { /** * Create a {@link DecryptionStream}, which can be used to decrypt and/or verify data using OpenPGP. + * * @return builder */ public static DecryptionBuilder decryptAndOrVerify() { return new DecryptionBuilder(); } + /** + * Verify a cleartext-signed message. + * + * @return builder + */ + public static VerifyCleartextSignatures verifyCleartextSignedMessage() { + return new VerifyCleartextSignaturesImpl(); + } + public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys) { return new SecretKeyRingEditor(secretKeys); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/CleartextSignatureProcessor.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/CleartextSignatureProcessor.java new file mode 100644 index 00000000..08c5083e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/CleartextSignatureProcessor.java @@ -0,0 +1,256 @@ +/* + * 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.signature.cleartext_signatures; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.util.Strings; +import org.pgpainless.PGPainless; +import org.pgpainless.exception.SignatureValidationException; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.signature.SignatureChainValidator; + +/** + * Processor for cleartext-signed messages. + * Based on Bouncycastle's {@link org.bouncycastle.openpgp.examples.ClearSignedFileProcessor}. + */ +public class CleartextSignatureProcessor { + + private final ArmoredInputStream in; + private final PGPPublicKeyRingCollection verificationKeys; + private final MultiPassStrategy multiPassStrategy; + + public CleartextSignatureProcessor(InputStream inputStream, + PGPPublicKeyRingCollection verificationKeys, + MultiPassStrategy multiPassStrategy) + throws IOException { + if (inputStream instanceof ArmoredInputStream) { + this.in = (ArmoredInputStream) inputStream; + } else { + this.in = new ArmoredInputStream(inputStream); + } + this.verificationKeys = verificationKeys; + this.multiPassStrategy = multiPassStrategy; + } + + /** + * Unpack the message from the ascii armor and process the signature. + * This method only returns the signature, if it is correct. + * + * After the message has been processed, the content can be retrieved from the {@link MultiPassStrategy}. + * If an {@link InMemoryMultiPassStrategy} was used, the message can be accessed via {@link InMemoryMultiPassStrategy#getBytes()}. + * If {@link MultiPassStrategy#writeMessageToFile(File)} was used, the message content was written to the given file. + * + * @return validated signature + * @throws IOException if the signature cannot be read. + * @throws PGPException if the signature cannot be initialized. + * @throws SignatureValidationException if the signature is invalid. + */ + public PGPSignature process() throws IOException, PGPException { + if (!in.isClearText()) { + throw new IllegalStateException("Message is not cleartext."); + } + + OutputStream out = new BufferedOutputStream(multiPassStrategy.getMessageOutputStream()); + + ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); + int lookAhead = readInputLine(lineOut, in); + byte[] lineSep = getLineSeparator(); + + if (lookAhead != -1 && in.isClearText()) { + byte[] line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); + out.write(lineSep); + + while (lookAhead != -1 && in.isClearText()) { + lookAhead = readInputLine(lineOut, lookAhead, in); + line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); + out.write(lineSep); + } + } else { + if (lookAhead != -1) { + byte[] line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); + out.write(lineSep); + } + } + + out.close(); + + PGPObjectFactory objectFactory = new PGPObjectFactory(in, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); + PGPSignatureList signatures = (PGPSignatureList) objectFactory.nextObject(); + PGPSignature signature = signatures.get(0); + + PGPPublicKeyRing signingKeyRing = null; + PGPPublicKey signingKey = null; + for (PGPPublicKeyRing ring : verificationKeys) { + signingKey = ring.getPublicKey(signature.getKeyID()); + if (signingKey != null) { + signingKeyRing = ring; + break; + } + } + if (signingKey == null) { + throw new SignatureValidationException("Missing public key " + Long.toHexString(signature.getKeyID())); + } + + signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); + + InputStream sigIn = new BufferedInputStream(multiPassStrategy.getMessageInputStream()); + lookAhead = readInputLine(lineOut, sigIn); + processLine(signature, lineOut.toByteArray()); + + if (lookAhead != -1) { + do { + lookAhead = readInputLine(lineOut, lookAhead, sigIn); + signature.update((byte) '\r'); + signature.update((byte) '\n'); + processLine(signature, lineOut.toByteArray()); + } while (lookAhead != -1); + } + sigIn.close(); + + SignatureChainValidator.validateSignature(signature, signingKeyRing, PGPainless.getPolicy()); + return signature; + } + + private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) + throws IOException { + bOut.reset(); + + int lookAhead = -1; + int ch; + + while ((ch = fIn.read()) >= 0) { + bOut.write(ch); + if (ch == '\r' || ch == '\n') { + lookAhead = readPassedEOL(bOut, ch, fIn); + break; + } + } + + return lookAhead; + } + + private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn) + throws IOException { + bOut.reset(); + + int ch = lookAhead; + + do { + bOut.write(ch); + if (ch == '\r' || ch == '\n') { + lookAhead = readPassedEOL(bOut, ch, fIn); + break; + } + } + while ((ch = fIn.read()) >= 0); + + if (ch < 0) { + lookAhead = -1; + } + + return lookAhead; + } + + private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn) + throws IOException { + int lookAhead = fIn.read(); + + if (lastCh == '\r' && lookAhead == '\n') { + bOut.write(lookAhead); + lookAhead = fIn.read(); + } + + return lookAhead; + } + + + private static byte[] getLineSeparator() { + String nl = Strings.lineSeparator(); + byte[] nlBytes = new byte[nl.length()]; + + for (int i = 0; i != nlBytes.length; i++) { + nlBytes[i] = (byte) nl.charAt(i); + } + + return nlBytes; + } + + private static void processLine(PGPSignature sig, byte[] line) { + int length = getLengthWithoutWhiteSpace(line); + if (length > 0) { + sig.update(line, 0, length); + } + } + + private static void processLine(OutputStream aOut, PGPSignatureGenerator sGen, byte[] line) + throws IOException { + // note: trailing white space needs to be removed from the end of + // each line for signature calculation RFC 4880 Section 7.1 + int length = getLengthWithoutWhiteSpace(line); + if (length > 0) { + sGen.update(line, 0, length); + } + + aOut.write(line, 0, line.length); + } + + private static int getLengthWithoutSeparatorOrTrailingWhitespace(byte[] line) { + int end = line.length - 1; + + while (end >= 0 && isWhiteSpace(line[end])) { + end--; + } + + return end + 1; + } + + private static boolean isLineEnding(byte b) { + return b == '\r' || b == '\n'; + } + + private static int getLengthWithoutWhiteSpace(byte[] line) { + int end = line.length - 1; + + while (end >= 0 && isWhiteSpace(line[end])) { + end--; + } + + return end + 1; + } + + private static boolean isWhiteSpace(byte b) { + return isLineEnding(b) || b == '\t' || b == ' '; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/InMemoryMultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/InMemoryMultiPassStrategy.java new file mode 100644 index 00000000..49dfa005 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/InMemoryMultiPassStrategy.java @@ -0,0 +1,38 @@ +/* + * 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.signature.cleartext_signatures; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +public class InMemoryMultiPassStrategy implements MultiPassStrategy { + + private final ByteArrayOutputStream cache = new ByteArrayOutputStream(); + + @Override + public ByteArrayOutputStream getMessageOutputStream() { + return cache; + } + + @Override + public ByteArrayInputStream getMessageInputStream() { + return new ByteArrayInputStream(cache.toByteArray()); + } + + public byte[] getBytes() { + return getMessageOutputStream().toByteArray(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/MultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/MultiPassStrategy.java new file mode 100644 index 00000000..67b9f22b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/MultiPassStrategy.java @@ -0,0 +1,75 @@ +/* + * 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.signature.cleartext_signatures; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface MultiPassStrategy { + + OutputStream getMessageOutputStream() throws IOException; + + InputStream getMessageInputStream() throws IOException; + + /** + * Write the message content out to a file and re-read it to verify signatures. + * This strategy is best suited for larger messages (eg. plaintext signed files) which might not fit into memory. + * After the message has been processed completely, the messages content are available at the provided file. + * + * @param file target file + * @return strategy + */ + static MultiPassStrategy writeMessageToFile(File file) { + + return new MultiPassStrategy() { + @Override + public OutputStream getMessageOutputStream() throws IOException { + if (!file.exists()) { + boolean created = file.createNewFile(); + if (!created) { + throw new IOException("New file '" + file.getAbsolutePath() + "' was not created."); + } + } + return new FileOutputStream(file); + } + + @Override + public InputStream getMessageInputStream() throws IOException { + if (!file.exists()) { + throw new IOException("File '" + file.getAbsolutePath() + "' does no longer exist."); + } + return new FileInputStream(file); + } + }; + } + + /** + * Read the message content into memory. + * This strategy is best suited for small messages which fit into memory. + * After the message has been processed completely, the message content can be accessed by calling + * {@link ByteArrayOutputStream#toByteArray()} on {@link #getMessageOutputStream()}. + * + * @return strategy + */ + static InMemoryMultiPassStrategy keepMessageInMemory() { + return new InMemoryMultiPassStrategy(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/VerifyCleartextSignatures.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/VerifyCleartextSignatures.java new file mode 100644 index 00000000..9078d361 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/VerifyCleartextSignatures.java @@ -0,0 +1,73 @@ +/* + * 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.signature.cleartext_signatures; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; + +public interface VerifyCleartextSignatures { + + /** + * Provide the {@link InputStream} which contains the cleartext-signed message. + * @param inputStream inputstream + * @return api handle + */ + WithStrategy 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 { + + /** + * Pass in the verification key ring. + * + * @param publicKey verification key + * @return processor + * @throws PGPException if the keys cannot be converted to a {@link PGPPublicKeyRingCollection}. + * @throws IOException if the keys cannot be parsed properly + */ + CleartextSignatureProcessor verifyWith(PGPPublicKeyRing publicKey) throws PGPException, IOException; + + /** + * Pass in the verification key ring collection. + * + * @param publicKeys verification keys + * @return processor + * @throws IOException if the keys cannot be parsed properly + */ + CleartextSignatureProcessor verifyWith(PGPPublicKeyRingCollection publicKeys) throws IOException; + + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/VerifyCleartextSignaturesImpl.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/VerifyCleartextSignaturesImpl.java new file mode 100644 index 00000000..0b2af101 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/VerifyCleartextSignaturesImpl.java @@ -0,0 +1,64 @@ +/* + * 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.signature.cleartext_signatures; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; + +public class VerifyCleartextSignaturesImpl implements VerifyCleartextSignatures { + + private InputStream inputStream; + private MultiPassStrategy multiPassStrategy; + private PGPPublicKeyRingCollection verificationKeys; + + @Override + public WithStrategy onInputStream(InputStream inputStream) { + VerifyCleartextSignaturesImpl.this.inputStream = inputStream; + return new WithStrategyImpl(); + } + + 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 { + + @Override + public CleartextSignatureProcessor verifyWith(PGPPublicKeyRing publicKey) throws PGPException, IOException { + VerifyCleartextSignaturesImpl.this.verificationKeys = new PGPPublicKeyRingCollection(Collections.singleton(publicKey)); + return new CleartextSignatureProcessor(inputStream, verificationKeys, multiPassStrategy); + } + + @Override + public CleartextSignatureProcessor verifyWith(PGPPublicKeyRingCollection publicKeys) throws IOException { + VerifyCleartextSignaturesImpl.this.verificationKeys = publicKeys; + return new CleartextSignatureProcessor(inputStream, verificationKeys, multiPassStrategy); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/package-info.java new file mode 100644 index 00000000..27b09a40 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2018 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. + */ +/** + * Classes related to cleartext signature verification. + */ +package org.pgpainless.signature.cleartext_signatures; diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/CleartextSignatureVerificationTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/CleartextSignatureVerificationTest.java new file mode 100644 index 00000000..7fd38795 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/CleartextSignatureVerificationTest.java @@ -0,0 +1,74 @@ +/* + * 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.signature; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.TestKeys; +import org.pgpainless.signature.cleartext_signatures.CleartextSignatureProcessor; +import org.pgpainless.signature.cleartext_signatures.InMemoryMultiPassStrategy; +import org.pgpainless.signature.cleartext_signatures.MultiPassStrategy; + +public class CleartextSignatureVerificationTest { + + @Test + public void cleartextSignVerification() throws IOException, PGPException { + String message = "Ah, Juliet, if the measure of thy joy\n" + + "Be heaped like mine, and that thy skill be more\n" + + "To blazon it, then sweeten with thy breath\n" + + "This neighbor air, and let rich music’s tongue\n" + + "Unfold the imagined happiness that both\n" + + "Receive in either by this dear encounter.\n"; + String signed = "-----BEGIN PGP SIGNED MESSAGE-----\n" + + "Hash: SHA512\n" + + "\n" + + "Ah, Juliet, if the measure of thy joy\n" + + "Be heaped like mine, and that thy skill be more\n" + + "To blazon it, then sweeten with thy breath\n" + + "This neighbor air, and let rich music’s tongue\n" + + "Unfold the imagined happiness that both\n" + + "Receive in either by this dear encounter.\n" + + "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "iHUEARMKAB0WIQRPZlxNwsRmC8ZCXkFXNuaTGs83DAUCYJ/x5gAKCRBXNuaTGs83\n" + + "DFRwAP9/4wMvV3WcX59Clo7mkRce6iwW3VBdiN+yMu3tjmHB2wD/RfE28Q1v4+eo\n" + + "ySNgbyvqYYsNr0fnBwaG3aaj+u5ExiE=\n" + + "=Z2SO\n" + + "-----END PGP SIGNATURE-----"; + PGPPublicKeyRing signingKeys = TestKeys.getEmilPublicKeyRing(); + + InMemoryMultiPassStrategy multiPassStrategy = MultiPassStrategy.keepMessageInMemory(); + CleartextSignatureProcessor processor = PGPainless.verifyCleartextSignedMessage() + .onInputStream(new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8))) + .withStrategy(multiPassStrategy) + .verifyWith(signingKeys); + + PGPSignature signature = processor.process(); + + assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); + assertArrayEquals(message.getBytes(StandardCharsets.UTF_8), multiPassStrategy.getBytes()); + } +}