1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-12-23 11:27:57 +01:00

Implement signature verification of cleartext-signatures

This commit is contained in:
Paul Schaub 2021-05-15 18:44:03 +02:00
parent 14ff0e9cc5
commit 225bc78ee1
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
8 changed files with 611 additions and 0 deletions

View file

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

View file

@ -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 == ' ';
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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;

View file

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