Move signature verification to dedicated streams

This commit is contained in:
Paul Schaub 2021-09-02 18:01:06 +02:00
parent ba0e5eb3fe
commit 90a00e0541
12 changed files with 374 additions and 325 deletions

View File

@ -15,21 +15,12 @@
*/
package org.pgpainless.decryption_verification;
import static org.pgpainless.signature.SignatureValidator.signatureWasCreatedInBounds;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.signature.CertificateValidator;
import org.pgpainless.signature.DetachedSignature;
import org.pgpainless.util.IntegrityProtectedInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Decryption Stream that handles updating and verification of detached signatures,
@ -37,10 +28,7 @@ import org.slf4j.LoggerFactory;
*/
public class DecryptionStream extends InputStream {
private static final Logger LOGGER = LoggerFactory.getLogger(DecryptionStream.class);
private final InputStream inputStream;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder;
private boolean isClosed = false;
private final IntegrityProtectedInputStream integrityProtectedInputStream;
@ -50,17 +38,15 @@ public class DecryptionStream extends InputStream {
* Create an input stream that handles decryption and - if necessary - integrity protection verification.
*
* @param wrapped underlying input stream
* @param options options for consuming, eg. decryption key...
* @param resultBuilder builder for decryption metadata like algorithms, recipients etc.
* @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity
* @param armorStream armor stream to verify CRC checksums
*/
DecryptionStream(@Nonnull InputStream wrapped, @Nonnull ConsumerOptions options,
DecryptionStream(@Nonnull InputStream wrapped,
@Nonnull OpenPgpMetadata.Builder resultBuilder,
IntegrityProtectedInputStream integrityProtectedInputStream,
InputStream armorStream) {
this.inputStream = wrapped;
this.options = options;
this.resultBuilder = resultBuilder;
this.integrityProtectedInputStream = integrityProtectedInputStream;
this.armorStream = armorStream;
@ -69,58 +55,27 @@ public class DecryptionStream extends InputStream {
@Override
public int read() throws IOException {
int r = inputStream.read();
maybeUpdateDetachedSignatures(r);
return r;
}
@Override
public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException {
int read = inputStream.read(bytes, offset, length);
if (read != -1) {
maybeUpdateDetachedSignatures(bytes, offset, read);
}
return read;
}
private void maybeUpdateDetachedSignatures(byte[] bytes, int offset, int length) {
for (DetachedSignature s : resultBuilder.getDetachedSignatures()) {
s.getSignature().update(bytes, offset, length);
}
}
private void maybeUpdateDetachedSignatures(int rByte) {
for (DetachedSignature s : resultBuilder.getDetachedSignatures()) {
if (rByte != -1) {
s.getSignature().update((byte) rByte);
}
}
}
@Override
public void close() throws IOException {
if (armorStream != null) {
Streams.drain(armorStream);
}
inputStream.close();
maybeVerifyDetachedSignatures();
if (integrityProtectedInputStream != null) {
integrityProtectedInputStream.close();
}
this.isClosed = true;
}
private void maybeVerifyDetachedSignatures() {
for (DetachedSignature s : resultBuilder.getDetachedSignatures()) {
try {
signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(s.getSignature());
boolean verified = CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(), (PGPPublicKeyRing) s.getSigningKeyRing(), PGPainless.getPolicy());
s.setVerified(verified);
} catch (SignatureValidationException e) {
LOGGER.warn("Could not verify signature of key {}", s.getSigningKeyIdentifier(), e);
}
}
}
/**
* Return the result of the decryption.
* The result contains metadata about the decryption, such as signatures, used keys and algorithms, as well as information

View File

@ -19,10 +19,9 @@ import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
@ -43,6 +42,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
@ -59,12 +59,11 @@ import org.pgpainless.exception.MissingLiteralDataException;
import org.pgpainless.exception.UnacceptableAlgorithmException;
import org.pgpainless.exception.WrongConsumingMethodException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.signature.DetachedSignature;
import org.pgpainless.signature.OnePassSignature;
import org.pgpainless.signature.OnePassSignatureCheck;
import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.util.CRCingArmoredInputStreamWrapper;
import org.pgpainless.util.IntegrityProtectedInputStream;
@ -78,15 +77,17 @@ public final class DecryptionStreamFactory {
private static final int MAX_RECURSION_DEPTH = 16;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
private final List<OnePassSignatureCheck> onePassSignatureChecks = new ArrayList<>();
private final List<DetachedSignature> detachedSignatureChecks = new ArrayList<>();
private static final PGPContentVerifierBuilderProvider verifierBuilderProvider =
ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider();
private static final KeyFingerPrintCalculator keyFingerprintCalculator =
ImplementationFactory.getInstance().getKeyFingerprintCalculator();
private final Map<OpenPgpV4Fingerprint, OnePassSignature> verifiableOnePassSignatures = new HashMap<>();
private IntegrityProtectedInputStream integrityProtectedEncryptedInputStream;
public static DecryptionStream create(@Nonnull InputStream inputStream,
@Nonnull ConsumerOptions options)
throws PGPException, IOException {
@ -109,9 +110,9 @@ public final class DecryptionStreamFactory {
PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId);
SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID());
try {
signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey);
resultBuilder.addDetachedSignature(
new DetachedSignature(signature, signingKeyRing, signingKeyIdentifier));
signature.init(verifierBuilderProvider, signingKey);
DetachedSignature detachedSignature = new DetachedSignature(signature, signingKeyRing, signingKeyIdentifier);
detachedSignatureChecks.add(detachedSignature);
} catch (PGPException e) {
LOGGER.warn("Cannot verify detached signature made by {}. Reason: {}", signingKeyIdentifier, e.getMessage(), e);
}
@ -142,29 +143,35 @@ public final class DecryptionStreamFactory {
inputStream = processPGPPackets(objectFactory, 1);
} catch (EOFException e) {
throw e;
}
catch (MissingLiteralDataException e) {
} catch (MissingLiteralDataException e) {
// Not an OpenPGP message.
// Reset the buffered stream to parse the message as arbitrary binary data
// to allow for detached signature verification.
LOGGER.debug("The message appears to not be an OpenPGP message. This is probably data signed with detached signatures?");
bufferedIn.reset();
inputStream = bufferedIn;
inputStream = wrapInVerifySignatureStream(bufferedIn);
} catch (IOException e) {
if (e.getMessage().contains("invalid armor")) {
// We falsely assumed the data to be armored.
LOGGER.debug("The message is apparently not armored.");
bufferedIn.reset();
inputStream = bufferedIn;
inputStream = wrapInVerifySignatureStream(bufferedIn);
} else {
throw e;
}
}
return new DecryptionStream(inputStream, options, resultBuilder, integrityProtectedEncryptedInputStream,
return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream,
(decoderStream instanceof ArmoredInputStream) ? decoderStream : null);
}
private InputStream wrapInVerifySignatureStream(InputStream bufferedIn) {
return new SignatureInputStream.VerifySignatures(
bufferedIn, onePassSignatureChecks,
detachedSignatureChecks, options,
resultBuilder);
}
private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory, int depth) throws IOException, PGPException {
if (depth >= MAX_RECURSION_DEPTH) {
throw new PGPException("Maximum recursion depth of packages exceeded.");
@ -217,7 +224,7 @@ public final class DecryptionStreamFactory {
return processPGPPackets(objectFactory, ++depth);
}
private InputStream processPGPLiteralData(@Nonnull PGPObjectFactory objectFactory, PGPLiteralData pgpLiteralData, int depth) {
private InputStream processPGPLiteralData(@Nonnull PGPObjectFactory objectFactory, PGPLiteralData pgpLiteralData, int depth) throws IOException {
LOGGER.debug("Depth {}: Found PGPLiteralData", depth);
InputStream literalDataInputStream = pgpLiteralData.getInputStream();
@ -225,13 +232,39 @@ public final class DecryptionStreamFactory {
.setModificationDate(pgpLiteralData.getModificationTime())
.setFileEncoding(StreamEncoding.fromCode(pgpLiteralData.getFormat()));
if (verifiableOnePassSignatures.isEmpty()) {
if (onePassSignatureChecks.isEmpty()) {
LOGGER.debug("No OnePassSignatures found -> We are done");
return literalDataInputStream;
}
return new SignatureVerifyingInputStream(literalDataInputStream,
objectFactory, verifiableOnePassSignatures, options, resultBuilder);
PGPSignatureList signatures = parseSignatures(objectFactory);
List<PGPSignature> signatureList = SignatureUtils.toList(signatures);
for (int i = 0; i < onePassSignatureChecks.size(); i++) {
onePassSignatureChecks.get(i).setSignature(signatureList.get(onePassSignatureChecks.size() - i - 1));
}
return new SignatureInputStream.VerifySignatures(literalDataInputStream,
onePassSignatureChecks, detachedSignatureChecks, options, resultBuilder) {
};
}
private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException {
PGPSignatureList signatureList = null;
Object pgpObject = objectFactory.nextObject();
while (pgpObject != null && signatureList == null) {
if (pgpObject instanceof PGPSignatureList) {
signatureList = (PGPSignatureList) pgpObject;
} else {
pgpObject = objectFactory.nextObject();
}
}
if (signatureList == null || signatureList.isEmpty()) {
throw new IOException("Verification failed - No Signatures found");
}
return signatureList;
}
private InputStream decryptSessionKey(@Nonnull PGPEncryptedDataList encryptedDataList)
@ -377,7 +410,7 @@ public final class DecryptionStreamFactory {
private void processOnePassSignature(PGPOnePassSignature signature) throws PGPException {
final long keyId = signature.getKeyID();
LOGGER.debug("Message contains OnePassSignature from {}", Long.toHexString(keyId));
LOGGER.debug("Encountered OnePassSignature from {}", Long.toHexString(keyId));
// Find public key
PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId);
@ -388,10 +421,8 @@ public final class DecryptionStreamFactory {
PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId);
signature.init(verifierBuilderProvider, verificationKey);
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(verificationKey);
OnePassSignature onePassSignature = new OnePassSignature(signature, verificationKeyRing);
resultBuilder.addOnePassSignature(onePassSignature);
verifiableOnePassSignatures.put(fingerprint, onePassSignature);
OnePassSignatureCheck onePassSignature = new OnePassSignatureCheck(signature, verificationKeyRing);
onePassSignatureChecks.add(onePassSignature);
}
private PGPSecretKeyRing findDecryptionKeyRing(long keyId) {

View File

@ -23,7 +23,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -34,17 +33,19 @@ import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.signature.DetachedSignature;
import org.pgpainless.signature.OnePassSignature;
import org.pgpainless.signature.cleartext_signatures.SignatureVerification;
public class OpenPgpMetadata {
private final Set<Long> recipientKeyIds;
private final SubkeyIdentifier decryptionKey;
private final List<OnePassSignature> onePassSignatures;
private final List<DetachedSignature> detachedSignatures;
private final List<SignatureVerification> verifiedInbandSignatures;
private final List<SignatureVerification.Failure> invalidInbandSignatures;
private final List<SignatureVerification> verifiedDetachedSignatures;
private final List<SignatureVerification.Failure> invalidDetachedSignatures;
private final SymmetricKeyAlgorithm symmetricKeyAlgorithm;
private final CompressionAlgorithm compressionAlgorithm;
private final String fileName;
@ -55,8 +56,10 @@ public class OpenPgpMetadata {
SubkeyIdentifier decryptionKey,
SymmetricKeyAlgorithm symmetricKeyAlgorithm,
CompressionAlgorithm algorithm,
List<OnePassSignature> onePassSignatures,
List<DetachedSignature> detachedSignatures,
List<SignatureVerification> verifiedInbandSignatures,
List<SignatureVerification.Failure> invalidInbandSignatures,
List<SignatureVerification> verifiedDetachedSignatures,
List<SignatureVerification.Failure> invalidDetachedSignatures,
String fileName,
Date modificationDate,
StreamEncoding fileEncoding) {
@ -65,8 +68,10 @@ public class OpenPgpMetadata {
this.decryptionKey = decryptionKey;
this.symmetricKeyAlgorithm = symmetricKeyAlgorithm;
this.compressionAlgorithm = algorithm;
this.detachedSignatures = Collections.unmodifiableList(detachedSignatures);
this.onePassSignatures = Collections.unmodifiableList(onePassSignatures);
this.verifiedInbandSignatures = Collections.unmodifiableList(verifiedInbandSignatures);
this.invalidInbandSignatures = Collections.unmodifiableList(invalidInbandSignatures);
this.verifiedDetachedSignatures = Collections.unmodifiableList(verifiedDetachedSignatures);
this.invalidDetachedSignatures = Collections.unmodifiableList(invalidDetachedSignatures);
this.fileName = fileName;
this.modificationDate = modificationDate;
this.fileEncoding = fileEncoding;
@ -128,11 +133,17 @@ public class OpenPgpMetadata {
*/
public @Nonnull Set<PGPSignature> getSignatures() {
Set<PGPSignature> signatures = new HashSet<>();
for (DetachedSignature detachedSignature : detachedSignatures) {
signatures.add(detachedSignature.getSignature());
for (SignatureVerification v : getVerifiedDetachedSignatures()) {
signatures.add(v.getSignature());
}
for (OnePassSignature onePassSignature : onePassSignatures) {
signatures.add(onePassSignature.getSignature());
for (SignatureVerification v : getVerifiedInbandSignatures()) {
signatures.add(v.getSignature());
}
for (SignatureVerification.Failure f : getInvalidDetachedSignatures()) {
signatures.add(f.getSignatureVerification().getSignature());
}
for (SignatureVerification.Failure f : getInvalidInbandSignatures()) {
signatures.add(f.getSignatureVerification().getSignature());
}
return signatures;
}
@ -158,20 +169,32 @@ public class OpenPgpMetadata {
*/
public Map<SubkeyIdentifier, PGPSignature> getVerifiedSignatures() {
Map<SubkeyIdentifier, PGPSignature> verifiedSignatures = new ConcurrentHashMap<>();
for (DetachedSignature detachedSignature : detachedSignatures) {
if (detachedSignature.isVerified()) {
verifiedSignatures.put(detachedSignature.getSigningKeyIdentifier(), detachedSignature.getSignature());
}
for (SignatureVerification detachedSignature : getVerifiedDetachedSignatures()) {
verifiedSignatures.put(detachedSignature.getSigningKey(), detachedSignature.getSignature());
}
for (OnePassSignature onePassSignature : onePassSignatures) {
if (onePassSignature.isVerified()) {
verifiedSignatures.put(onePassSignature.getSigningKey(), onePassSignature.getSignature());
}
for (SignatureVerification inbandSignatures : verifiedInbandSignatures) {
verifiedSignatures.put(inbandSignatures.getSigningKey(), inbandSignatures.getSignature());
}
return verifiedSignatures;
}
public List<SignatureVerification> getVerifiedInbandSignatures() {
return verifiedInbandSignatures;
}
public List<SignatureVerification> getVerifiedDetachedSignatures() {
return verifiedDetachedSignatures;
}
public List<SignatureVerification.Failure> getInvalidInbandSignatures() {
return invalidInbandSignatures;
}
public List<SignatureVerification.Failure> getInvalidDetachedSignatures() {
return invalidDetachedSignatures;
}
/**
* Return true, if the message is signed and at least one signature on the message was verified successfully.
*
@ -261,14 +284,18 @@ public class OpenPgpMetadata {
private final Set<Long> recipientFingerprints = new HashSet<>();
private SubkeyIdentifier decryptionKey;
private final List<DetachedSignature> detachedSignatures = new ArrayList<>();
private final List<OnePassSignature> onePassSignatures = new ArrayList<>();
private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.NULL;
private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED;
private String fileName;
private StreamEncoding fileEncoding;
private Date modificationDate;
private final List<SignatureVerification> verifiedInbandSignatures = new ArrayList<>();
private final List<SignatureVerification> verifiedDetachedSignatures = new ArrayList<>();
private final List<SignatureVerification.Failure> invalidInbandSignatures = new ArrayList<>();
private final List<SignatureVerification.Failure> invalidDetachedSignatures = new ArrayList<>();
public Builder addRecipientKeyId(Long keyId) {
this.recipientFingerprints.add(keyId);
return this;
@ -284,10 +311,6 @@ public class OpenPgpMetadata {
return this;
}
public List<DetachedSignature> getDetachedSignatures() {
return detachedSignatures;
}
public Builder setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm symmetricKeyAlgorithm) {
this.symmetricKeyAlgorithm = symmetricKeyAlgorithm;
return this;
@ -308,18 +331,29 @@ public class OpenPgpMetadata {
return this;
}
public void addDetachedSignature(DetachedSignature signature) {
this.detachedSignatures.add(signature);
}
public void addOnePassSignature(OnePassSignature onePassSignature) {
this.onePassSignatures.add(onePassSignature);
}
public OpenPgpMetadata build() {
return new OpenPgpMetadata(recipientFingerprints, decryptionKey,
return new OpenPgpMetadata(
recipientFingerprints, decryptionKey,
symmetricKeyAlgorithm, compressionAlgorithm,
onePassSignatures, detachedSignatures, fileName, modificationDate, fileEncoding);
verifiedInbandSignatures, invalidInbandSignatures,
verifiedDetachedSignatures, invalidDetachedSignatures,
fileName, modificationDate, fileEncoding);
}
public void addVerifiedInbandSignature(SignatureVerification signatureVerification) {
this.verifiedInbandSignatures.add(signatureVerification);
}
public void addVerifiedDetachedSignature(SignatureVerification signatureVerification) {
this.verifiedDetachedSignatures.add(signatureVerification);
}
public void addInvalidInbandSignature(SignatureVerification signatureVerification, SignatureValidationException e) {
this.invalidInbandSignatures.add(new SignatureVerification.Failure(signatureVerification, e));
}
public void addInvalidDetachedSignature(SignatureVerification signatureVerification, SignatureValidationException e) {
this.invalidDetachedSignatures.add(new SignatureVerification.Failure(signatureVerification, e));
}
}
}

View File

@ -0,0 +1,162 @@
/*
* 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.decryption_verification;
import static org.pgpainless.signature.SignatureValidator.signatureWasCreatedInBounds;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.PGPainless;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.policy.Policy;
import org.pgpainless.signature.CertificateValidator;
import org.pgpainless.signature.DetachedSignature;
import org.pgpainless.signature.OnePassSignatureCheck;
import org.pgpainless.signature.cleartext_signatures.SignatureVerification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class SignatureInputStream extends FilterInputStream {
protected SignatureInputStream(InputStream inputStream) {
super(inputStream);
}
public static class VerifySignatures extends SignatureInputStream {
private static final Logger LOGGER = LoggerFactory.getLogger(VerifySignatures.class);
private final List<OnePassSignatureCheck> opSignatures;
private final List<DetachedSignature> detachedSignatures;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder;
public VerifySignatures(
InputStream literalDataStream,
List<OnePassSignatureCheck> opSignatures,
List<DetachedSignature> detachedSignatures,
ConsumerOptions options,
OpenPgpMetadata.Builder resultBuilder) {
super(literalDataStream);
this.opSignatures = opSignatures;
this.detachedSignatures = detachedSignatures;
this.options = options;
this.resultBuilder = resultBuilder;
}
@Override
public int read() throws IOException {
final int data = super.read();
final boolean endOfStream = data == -1;
if (endOfStream) {
verifyOnePassSignatures();
verifyDetachedSignatures();
} else {
byte b = (byte) data;
updateOnePassSignatures(b);
updateDetachedSignatures(b);
}
return data;
}
@Override
public int read(@Nonnull byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
final boolean endOfStream = read == -1;
if (endOfStream) {
verifyOnePassSignatures();
verifyDetachedSignatures();
} else {
updateOnePassSignatures(b, off, read);
updateDetachedSignatures(b, off, read);
}
return read;
}
private synchronized void verifyOnePassSignatures() {
Policy policy = PGPainless.getPolicy();
for (OnePassSignatureCheck opSignature : opSignatures) {
if (opSignature.getSignature() == null) {
LOGGER.warn("Found OnePassSignature without respective signature packet -> skip");
continue;
}
try {
signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(opSignature.getSignature());
CertificateValidator.validateCertificateAndVerifyOnePassSignature(opSignature, policy);
resultBuilder.addVerifiedInbandSignature(new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()));
} catch (SignatureValidationException e) {
LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}",
opSignature.getSigningKey(), e.getMessage(), e);
resultBuilder.addInvalidInbandSignature(new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()), e);
}
}
}
private void verifyDetachedSignatures() {
Policy policy = PGPainless.getPolicy();
for (DetachedSignature s : detachedSignatures) {
try {
signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(s.getSignature());
CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(), (PGPPublicKeyRing) s.getSigningKeyRing(), policy);
resultBuilder.addVerifiedDetachedSignature(new SignatureVerification(s.getSignature(), s.getSigningKeyIdentifier()));
} catch (SignatureValidationException e) {
LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}",
s.getSigningKeyIdentifier(), e.getMessage(), e);
resultBuilder.addInvalidDetachedSignature(new SignatureVerification(s.getSignature(), s.getSigningKeyIdentifier()), e);
}
}
}
private void updateOnePassSignatures(byte data) {
for (OnePassSignatureCheck opSignature : opSignatures) {
opSignature.getOnePassSignature().update(data);
}
}
private void updateOnePassSignatures(byte[] bytes, int offset, int length) {
for (OnePassSignatureCheck opSignature : opSignatures) {
opSignature.getOnePassSignature().update(bytes, offset, length);
}
}
private void updateDetachedSignatures(byte b) {
for (DetachedSignature detachedSignature : detachedSignatures) {
detachedSignature.getSignature().update(b);
}
}
private void updateDetachedSignatures(byte[] b, int off, int read) {
for (DetachedSignature detachedSignature : detachedSignatures) {
detachedSignature.getSignature().update(b, off, read);
}
}
}
public static class CleartextSignatures extends SignatureInputStream {
public CleartextSignatures(InputStream inputStream, List<PGPSignature> signatures) {
super(inputStream);
}
}
}

View File

@ -1,203 +0,0 @@
/*
* 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.
*/
package org.pgpainless.decryption_verification;
import static org.pgpainless.signature.SignatureValidator.signatureWasCreatedInBounds;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.pgpainless.PGPainless;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.policy.Policy;
import org.pgpainless.signature.OnePassSignature;
import org.pgpainless.signature.CertificateValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SignatureVerifyingInputStream extends FilterInputStream {
private static final Logger LOGGER = LoggerFactory.getLogger(SignatureVerifyingInputStream.class);
private final PGPObjectFactory objectFactory;
private final Map<OpenPgpV4Fingerprint, OnePassSignature> onePassSignatures;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder;
private boolean validated = false;
protected SignatureVerifyingInputStream(@Nonnull InputStream inputStream,
@Nonnull PGPObjectFactory objectFactory,
@Nonnull Map<OpenPgpV4Fingerprint, OnePassSignature> onePassSignatures,
@Nonnull ConsumerOptions options,
@Nonnull OpenPgpMetadata.Builder resultBuilder) {
super(inputStream);
this.objectFactory = objectFactory;
this.options = options;
this.resultBuilder = resultBuilder;
this.onePassSignatures = onePassSignatures;
LOGGER.debug("Begin verifying OnePassSignatures");
}
private void updateOnePassSignatures(byte data) {
for (OnePassSignature signature : onePassSignatures.values()) {
signature.getOnePassSignature().update(data);
}
}
private void updateOnePassSignatures(byte[] b, int off, int len) {
for (OnePassSignature signature : onePassSignatures.values()) {
signature.getOnePassSignature().update(b, off, len);
}
}
private void validateOnePassSignaturesIfNeeded() throws IOException {
if (validated) {
return;
}
validated = true;
validateOnePassSignaturesIfAny();
}
private void validateOnePassSignaturesIfAny() throws IOException {
if (onePassSignatures.isEmpty()) {
LOGGER.debug("No One-Pass-Signatures found -> No validation");
return;
}
validateOnePassSignatures();
}
private void validateOnePassSignatures() throws IOException {
PGPSignatureList signatureList = findPgpSignatureList();
for (PGPSignature signature : signatureList) {
try {
OpenPgpV4Fingerprint fingerprint = findFingerprintForSignature(signature);
OnePassSignature onePassSignature = findOnePassSignature(fingerprint);
if (onePassSignature == null) {
LOGGER.warn("Found Signature without respective OnePassSignature packet -> skip");
continue;
}
verifySignatureOrThrowSignatureException(signature, onePassSignature);
} catch (SignatureValidationException e) {
LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}",
Long.toHexString(signature.getKeyID()), e.getMessage(), e);
}
}
}
private void verifySignatureOrThrowSignatureException(PGPSignature signature, OnePassSignature onePassSignature)
throws SignatureValidationException {
Policy policy = PGPainless.getPolicy();
signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(signature);
CertificateValidator.validateCertificateAndVerifyOnePassSignature(signature, onePassSignature, policy);
}
private OnePassSignature findOnePassSignature(OpenPgpV4Fingerprint fingerprint) {
if (fingerprint != null) {
return onePassSignatures.get(fingerprint);
}
return null;
}
private PGPSignatureList findPgpSignatureList() throws IOException {
PGPSignatureList signatureList = null;
Object pgpObject = objectFactory.nextObject();
while (pgpObject != null && signatureList == null) {
if (pgpObject instanceof PGPSignatureList) {
signatureList = (PGPSignatureList) pgpObject;
} else {
pgpObject = objectFactory.nextObject();
}
}
if (signatureList == null || signatureList.isEmpty()) {
throw new IOException("Verification failed - No Signatures found");
}
return signatureList;
}
private OpenPgpV4Fingerprint findFingerprintForSignature(PGPSignature signature) {
OpenPgpV4Fingerprint fingerprint = null;
for (OpenPgpV4Fingerprint f : onePassSignatures.keySet()) {
if (f.getKeyId() == signature.getKeyID()) {
fingerprint = f;
break;
}
}
return fingerprint;
}
@Override
public int read() throws IOException {
final int data = super.read();
final boolean endOfStream = data == -1;
if (endOfStream) {
validateOnePassSignaturesIfNeeded();
} else {
updateOnePassSignatures((byte) data);
}
return data;
}
@Override
public int read(@Nonnull byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(@Nonnull byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
final boolean endOfStream = read == -1;
if (endOfStream) {
validateOnePassSignaturesIfNeeded();
} else {
updateOnePassSignatures(b, off, read);
}
return read;
}
@Override
public long skip(long n) {
throw new UnsupportedOperationException("skip() is not supported");
}
@Override
public synchronized void mark(int readlimit) {
throw new UnsupportedOperationException("mark() not supported");
}
@Override
public synchronized void reset() {
throw new UnsupportedOperationException("reset() is not supported");
}
@Override
public boolean markSupported() {
return false;
}
}

View File

@ -265,16 +265,16 @@ public final class CertificateValidator {
}
/**
* Validate the signing key certificate and the given {@link OnePassSignature}.
* Validate the signing key certificate and the given {@link OnePassSignatureCheck}.
*
* @param signature OpenPGP signature from the signed message
* @param onePassSignature corresponding one-pass-signature
* @param policy policy
* @return true if the certificate is valid and the signature is correct, false otherwise.
* @throws SignatureValidationException in case of a validation error
*/
public static boolean validateCertificateAndVerifyOnePassSignature(PGPSignature signature, OnePassSignature onePassSignature, Policy policy)
public static boolean validateCertificateAndVerifyOnePassSignature(OnePassSignatureCheck onePassSignature, Policy policy)
throws SignatureValidationException {
PGPSignature signature = onePassSignature.getSignature();
validateCertificate(signature, onePassSignature.getVerificationKeys(), policy);
PGPPublicKey signingKey = onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID());
verifyOnePassSignature(signature, signingKey, onePassSignature, policy);

View File

@ -19,31 +19,36 @@ import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.decryption_verification.SignatureInputStream;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.SubkeyIdentifier;
/**
* Tuple-class that bundles together a {@link PGPOnePassSignature} object, a {@link PGPPublicKeyRing}
* destined to verify the signature, the {@link PGPSignature} itself and a record of whether or not the signature
* destined to verify the signature, the {@link PGPSignature} itself and a record of whether the signature
* was verified.
*/
public class OnePassSignature {
public class OnePassSignatureCheck {
private final PGPOnePassSignature onePassSignature;
private final PGPPublicKeyRing verificationKeys;
private PGPSignature signature;
private boolean verified;
/**
* Create a new {@link OnePassSignature}.
* Create a new {@link OnePassSignatureCheck}.
*
* @param onePassSignature one-pass signature packet used to initialize the signature verifier.
* @param verificationKeys verification keys
*/
public OnePassSignature(PGPOnePassSignature onePassSignature, PGPPublicKeyRing verificationKeys) {
public OnePassSignatureCheck(PGPOnePassSignature onePassSignature, PGPPublicKeyRing verificationKeys) {
this.onePassSignature = onePassSignature;
this.verificationKeys = verificationKeys;
}
public void setSignature(PGPSignature signature) {
this.signature = signature;
}
/**
* Return true if the signature is verified.
*
@ -75,17 +80,16 @@ public class OnePassSignature {
* Verify the one-pass signature.
* Note: This method only checks if the signature itself is correct.
* It does not check if the signing key was eligible to create the signature, or if the signature is expired etc.
* Those checks are being done by {@link org.pgpainless.decryption_verification.SignatureVerifyingInputStream}.
* Those checks are being done by {@link SignatureInputStream.VerifySignatures}.
*
* @param signature parsed-out signature
* @return true if the signature was verified, false otherwise
* @throws PGPException if signature verification fails with an exception.
*/
public boolean verify(PGPSignature signature) throws PGPException {
this.verified = getOnePassSignature().verify(signature);
if (verified) {
this.signature = signature;
public boolean verify() throws PGPException {
if (signature == null) {
throw new IllegalStateException("No comparison signature provided.");
}
this.verified = getOnePassSignature().verify(signature);
return verified;
}

View File

@ -337,4 +337,12 @@ public final class SignatureUtils {
public static String getSignatureDigestPrefix(PGPSignature signature) {
return Hex.toHexString(signature.getDigestPrefix());
}
public static List<PGPSignature> toList(PGPSignatureList signatures) {
List<PGPSignature> list = new ArrayList<>();
for (PGPSignature signature : signatures) {
list.add(signature);
}
return list;
}
}

View File

@ -424,7 +424,7 @@ public final class SignatureVerifier {
}
}
public static boolean verifyOnePassSignature(PGPSignature signature, PGPPublicKey signingKey, OnePassSignature onePassSignature, Policy policy)
public static boolean verifyOnePassSignature(PGPSignature signature, PGPPublicKey signingKey, OnePassSignatureCheck onePassSignature, Policy policy)
throws SignatureValidationException {
try {
SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature);
@ -435,7 +435,7 @@ public final class SignatureVerifier {
}
try {
if (!onePassSignature.verify(signature)) {
if (!onePassSignature.verify()) {
throw new SignatureValidationException("Bad signature of key " + Long.toHexString(signingKey.getKeyID()));
}
} catch (PGPException e) {

View File

@ -0,0 +1,57 @@
/*
* 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 org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.key.SubkeyIdentifier;
public class SignatureVerification {
private final PGPSignature signature;
private final SubkeyIdentifier signingKey;
public SignatureVerification(PGPSignature signature, SubkeyIdentifier signingKey) {
this.signature = signature;
this.signingKey = signingKey;
}
public PGPSignature getSignature() {
return signature;
}
public SubkeyIdentifier getSigningKey() {
return signingKey;
}
public static class Failure {
private final SignatureVerification signatureVerification;
private final SignatureValidationException validationException;
public Failure(SignatureVerification verification, SignatureValidationException validationException) {
this.signatureVerification = verification;
this.validationException = validationException;
}
public SignatureVerification getSignatureVerification() {
return signatureVerification;
}
public SignatureValidationException getValidationException() {
return validationException;
}
}
}

View File

@ -93,7 +93,7 @@ public class VerifyNotBeforeNotAfterTest {
.withOptions(options);
OpenPgpMetadata metadata = processSignedData(verifier);
assertTrue(metadata.getVerifiedSignatures().containsKey(new SubkeyIdentifier(certificate)));
assertTrue(metadata.containsVerifiedSignatureFrom(certificate));
}
@Test

View File

@ -30,6 +30,7 @@ public class DearmorImpl implements Dearmor {
public Ready data(InputStream data) throws IOException {
InputStream decoder = PGPUtil.getDecoderStream(data);
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
Streams.pipeAll(decoder, outputStream);