diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index 969d5a45..2f04502e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -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 diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 8b0f398b..1a6c27a3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -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 onePassSignatureChecks = new ArrayList<>(); + private final List detachedSignatureChecks = new ArrayList<>(); + private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); private static final KeyFingerPrintCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); - private final Map 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 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) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java index 854b717a..f2b6b47e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -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 recipientKeyIds; private final SubkeyIdentifier decryptionKey; - private final List onePassSignatures; - private final List detachedSignatures; + private final List verifiedInbandSignatures; + private final List invalidInbandSignatures; + private final List verifiedDetachedSignatures; + private final List 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 onePassSignatures, - List detachedSignatures, + List verifiedInbandSignatures, + List invalidInbandSignatures, + List verifiedDetachedSignatures, + List 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 getSignatures() { Set 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 getVerifiedSignatures() { Map 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 getVerifiedInbandSignatures() { + return verifiedInbandSignatures; + } + + public List getVerifiedDetachedSignatures() { + return verifiedDetachedSignatures; + } + + public List getInvalidInbandSignatures() { + return invalidInbandSignatures; + } + + public List 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 recipientFingerprints = new HashSet<>(); private SubkeyIdentifier decryptionKey; - private final List detachedSignatures = new ArrayList<>(); - private final List 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 verifiedInbandSignatures = new ArrayList<>(); + private final List verifiedDetachedSignatures = new ArrayList<>(); + private final List invalidInbandSignatures = new ArrayList<>(); + private final List 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 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)); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java new file mode 100644 index 00000000..ca46d5d0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java @@ -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 opSignatures; + private final List detachedSignatures; + private final ConsumerOptions options; + private final OpenPgpMetadata.Builder resultBuilder; + + public VerifySignatures( + InputStream literalDataStream, + List opSignatures, + List 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 signatures) { + super(inputStream); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java deleted file mode 100644 index 48b983b4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java +++ /dev/null @@ -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 onePassSignatures; - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder; - - private boolean validated = false; - - protected SignatureVerifyingInputStream(@Nonnull InputStream inputStream, - @Nonnull PGPObjectFactory objectFactory, - @Nonnull Map 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; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/CertificateValidator.java index 3cb68623..cfbc7705 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/CertificateValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/CertificateValidator.java @@ -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); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignature.java b/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignatureCheck.java similarity index 82% rename from pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignature.java rename to pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignatureCheck.java index 14f16ba7..7122443d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignature.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/OnePassSignatureCheck.java @@ -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; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index 25f9d9f4..d512efb8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -337,4 +337,12 @@ public final class SignatureUtils { public static String getSignatureDigestPrefix(PGPSignature signature) { return Hex.toHexString(signature.getDigestPrefix()); } + + public static List toList(PGPSignatureList signatures) { + List list = new ArrayList<>(); + for (PGPSignature signature : signatures) { + list.add(signature); + } + return list; + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureVerifier.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureVerifier.java index 4ddf7c00..97a4fb00 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureVerifier.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureVerifier.java @@ -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) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/SignatureVerification.java b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/SignatureVerification.java new file mode 100644 index 00000000..5c944c95 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/cleartext_signatures/SignatureVerification.java @@ -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; + } + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java index 64033615..6ed7b843 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java @@ -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 diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index e4b05a4b..b4f7516e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -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);