1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-10 06:11:08 +01:00

Implement experimental signature verification (correctness only)

This commit is contained in:
Paul Schaub 2022-09-16 00:51:49 +02:00
parent 9366700895
commit 54d7d0c7ae
7 changed files with 497 additions and 1236 deletions

View file

@ -1,278 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.bouncycastle.bcpg.BCPGInputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.bcpg.ModDetectionCodePacket;
import org.bouncycastle.bcpg.OnePassSignaturePacket;
import org.bouncycastle.bcpg.Packet;
import org.bouncycastle.bcpg.PacketTags;
import org.bouncycastle.bcpg.PublicKeyEncSessionPacket;
import org.bouncycastle.bcpg.SignaturePacket;
import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPBEEncryptedData;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
import org.pgpainless.algorithm.OpenPgpPacket;
import org.pgpainless.decryption_verification.automaton.InputAlphabet;
import org.pgpainless.decryption_verification.automaton.NestingPDA;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.pgpainless.exception.MessageNotIntegrityProtectedException;
import org.pgpainless.exception.MissingDecryptionMethodException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.util.Passphrase;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Stack;
public class MessageDecryptionStream extends InputStream {
private final ConsumerOptions options;
NestingPDA automaton = new NestingPDA();
// nested streams, outermost at the bottom of the stack
Stack<Layer> packetLayers = new Stack<>();
List<PublicKeyEncSessionPacket> pkeskList = new ArrayList<>();
List<SymmetricKeyEncSessionPacket> skeskList = new ArrayList<>();
public MessageDecryptionStream(InputStream inputStream, ConsumerOptions options)
throws IOException, PGPException {
this.options = options;
packetLayers.push(Layer.initial(inputStream));
walkLayer();
}
private void walkLayer() throws PGPException, IOException {
if (packetLayers.isEmpty()) {
return;
}
// We are currently in the deepest layer
Layer layer = packetLayers.peek();
BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream;
loop: while (true) {
if (inputStream.nextPacketTag() == -1) {
popLayer();
break loop;
}
OpenPgpPacket tag = nextTagOrThrow(inputStream);
switch (tag) {
case LIT:
automaton.next(InputAlphabet.LiteralData);
PGPLiteralData literalData = new PGPLiteralData(inputStream);
packetLayers.push(Layer.literalMessage(literalData.getDataStream()));
break loop;
case COMP:
automaton.next(InputAlphabet.CompressedData);
PGPCompressedData compressedData = new PGPCompressedData(inputStream);
inputStream = new BCPGInputStream(compressedData.getDataStream());
packetLayers.push(Layer.compressedData(inputStream));
break;
case OPS:
automaton.next(InputAlphabet.OnePassSignatures);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
BCPGOutputStream bcpgOut = new BCPGOutputStream(buf);
while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) {
Packet packet = inputStream.readPacket();
if (packet instanceof OnePassSignaturePacket) {
OnePassSignaturePacket sig = (OnePassSignaturePacket) packet;
sig.encode(bcpgOut);
}
}
PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance()
.getPGPObjectFactory(buf.toByteArray()).nextObject();
break;
case SIG:
automaton.next(InputAlphabet.Signatures);
buf = new ByteArrayOutputStream();
bcpgOut = new BCPGOutputStream(buf);
while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) {
Packet packet = inputStream.readPacket();
if (packet instanceof SignaturePacket) {
SignaturePacket sig = (SignaturePacket) packet;
sig.encode(bcpgOut);
}
}
PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance()
.getPGPObjectFactory(buf.toByteArray()).nextObject();
break;
case PKESK:
PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket();
pkeskList.add(pkeskPacket);
break;
case SKESK:
SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket();
skeskList.add(skeskPacket);
break;
case SED:
if (!options.isIgnoreMDCErrors()) {
throw new MessageNotIntegrityProtectedException();
}
// No break; we continue below!
case SEIPD:
automaton.next(InputAlphabet.EncryptedData);
PGPEncryptedDataList encryptedDataList = assembleEncryptedDataList(inputStream);
for (PGPEncryptedData encData : encryptedDataList) {
if (encData instanceof PGPPBEEncryptedData) {
PGPPBEEncryptedData skenc = (PGPPBEEncryptedData) encData;
for (Passphrase passphrase : options.getDecryptionPassphrases()) {
PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
.getPBEDataDecryptorFactory(passphrase);
InputStream decryptedIn = skenc.getDataStream(decryptorFactory);
packetLayers.push(Layer.encryptedData(new BCPGInputStream(decryptedIn)));
walkLayer();
break loop;
}
}
}
throw new MissingDecryptionMethodException("Cannot decrypt message.");
case MARKER:
inputStream.readPacket(); // discard
break;
case SK:
case PK:
case SSK:
case PSK:
case TRUST:
case UID:
case UATTR:
throw new MalformedOpenPgpMessageException("OpenPGP packet " + tag + " MUST NOT be part of OpenPGP messages.");
case MOD:
ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket();
break;
case EXP_1:
case EXP_2:
case EXP_3:
case EXP_4:
throw new MalformedOpenPgpMessageException("Experimental packet " + tag + " found inside the message.");
}
}
}
private PGPEncryptedDataList assembleEncryptedDataList(BCPGInputStream inputStream)
throws IOException {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
BCPGOutputStream bcpgOut = new BCPGOutputStream(buf);
for (SymmetricKeyEncSessionPacket skesk : skeskList) {
bcpgOut.write(skesk.getEncoded());
}
skeskList.clear();
for (PublicKeyEncSessionPacket pkesk : pkeskList) {
bcpgOut.write(pkesk.getEncoded());
}
pkeskList.clear();
SequenceInputStream sqin = new SequenceInputStream(
new ByteArrayInputStream(buf.toByteArray()), inputStream);
PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) ImplementationFactory.getInstance()
.getPGPObjectFactory(sqin).nextObject();
return encryptedDataList;
}
private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream)
throws IOException, InvalidOpenPgpPacketException {
try {
return OpenPgpPacket.requireFromTag(inputStream.nextPacketTag());
} catch (NoSuchElementException e) {
throw new InvalidOpenPgpPacketException(e.getMessage());
}
}
private void popLayer() throws MalformedOpenPgpMessageException {
if (packetLayers.pop().isNested)
automaton.next(InputAlphabet.EndOfSequence);
}
@Override
public int read() throws IOException {
if (packetLayers.isEmpty()) {
automaton.assertValid();
return -1;
}
int r = -1;
try {
r = packetLayers.peek().inputStream.read();
} catch (IOException e) {
}
if (r == -1) {
popLayer();
try {
walkLayer();
} catch (PGPException e) {
throw new RuntimeException(e);
}
return read();
}
return r;
}
public static class InvalidOpenPgpPacketException extends PGPException {
public InvalidOpenPgpPacketException(String message) {
super(message);
}
}
private static class Layer {
InputStream inputStream;
boolean isNested;
private Layer(InputStream inputStream, boolean isNested) {
this.inputStream = inputStream;
this.isNested = isNested;
}
static Layer initial(InputStream inputStream) {
BCPGInputStream bcpgIn;
if (inputStream instanceof BCPGInputStream) {
bcpgIn = (BCPGInputStream) inputStream;
} else {
bcpgIn = new BCPGInputStream(inputStream);
}
return new Layer(bcpgIn, true);
}
static Layer literalMessage(InputStream inputStream) {
return new Layer(inputStream, false);
}
static Layer compressedData(InputStream inputStream) {
return new Layer(inputStream, true);
}
static Layer encryptedData(InputStream inputStream) {
return new Layer(inputStream, true);
}
}
}

View file

@ -18,11 +18,13 @@ import org.bouncycastle.openpgp.PGPPBEEncryptedData;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
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.operator.PBEDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory;
import org.pgpainless.PGPainless;
@ -38,6 +40,7 @@ import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.util.Passphrase;
import org.pgpainless.util.Tuple;
@ -57,7 +60,7 @@ public class OpenPgpMessageInputStream extends InputStream {
private boolean closed = false;
private Signatures signatures = new Signatures();
private Signatures signatures;
public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options)
throws IOException, PGPException {
@ -69,6 +72,7 @@ public class OpenPgpMessageInputStream extends InputStream {
}
this.options = options;
this.signatures = new Signatures(options);
this.signatures.addDetachedSignatures(options.getDetachedSignatures());
consumePackets();
@ -88,8 +92,9 @@ public class OpenPgpMessageInputStream extends InputStream {
throws IOException, PGPException {
System.out.println("Walk " + automaton);
int tag;
loop: while ((tag = bcpgIn.nextPacketTag()) != -1) {
loop: while ((tag = getTag()) != -1) {
OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag);
System.out.println(nextPacket);
switch (nextPacket) {
// Literal Data - the literal data content is the new input stream
@ -114,9 +119,10 @@ public class OpenPgpMessageInputStream extends InputStream {
// Signatures - either prepended to the message, or corresponding to the One Pass Signatures
case SIG:
boolean isCorrespondingToOPS = automaton.peekStack() == StackAlphabet.ops;
automaton.next(InputAlphabet.Signatures);
PGPSignatureList signatureList = readSignatures();
if (automaton.peekStack() == StackAlphabet.ops) {
if (isCorrespondingToOPS) {
signatures.addOnePassCorrespondingSignatures(signatureList);
} else {
signatures.addPrependedSignatures(signatureList);
@ -246,6 +252,19 @@ public class OpenPgpMessageInputStream extends InputStream {
}
}
private int getTag() throws IOException {
try {
return bcpgIn.nextPacketTag();
} catch (IOException e) {
if ("Stream closed".equals(e.getMessage())) {
// ZipInflater Streams sometimes close under our feet -.-
// Therefore we catch resulting IOEs and return -1 instead.
return -1;
}
throw e;
}
}
private List<Tuple<PGPSecretKeyRing, PGPSecretKey>> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) {
int algorithm = pkesk.getAlgorithm();
List<Tuple<PGPSecretKeyRing, PGPSecretKey>> decryptionKeyCandidates = new ArrayList<>();
@ -276,8 +295,8 @@ public class OpenPgpMessageInputStream extends InputStream {
private PGPOnePassSignatureList readOnePassSignatures() throws IOException {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
BCPGOutputStream bcpgOut = new BCPGOutputStream(buf);
int tag = bcpgIn.nextPacketTag();
while (tag == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) {
int tag;
while ((tag = getTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) {
Packet packet = bcpgIn.readPacket();
if (tag == PacketTags.ONE_PASS_SIGNATURE) {
OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet;
@ -294,13 +313,13 @@ public class OpenPgpMessageInputStream extends InputStream {
private PGPSignatureList readSignatures() throws IOException {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
BCPGOutputStream bcpgOut = new BCPGOutputStream(buf);
int tag = bcpgIn.nextPacketTag();
int tag = getTag();
while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) {
Packet packet = bcpgIn.readPacket();
if (tag == PacketTags.SIGNATURE) {
SignaturePacket sigPacket = (SignaturePacket) packet;
sigPacket.encode(bcpgOut);
tag = bcpgIn.nextPacketTag();
tag = getTag();
}
}
bcpgOut.close();
@ -328,6 +347,16 @@ public class OpenPgpMessageInputStream extends InputStream {
byte b = (byte) r;
signatures.update(b);
} else {
in.close();
in = null;
try {
consumePackets();
} catch (PGPException e) {
throw new RuntimeException(e);
}
signatures.finish();
/*
if (in instanceof OpenPgpMessageInputStream) {
in.close();
in = null;
@ -335,10 +364,12 @@ public class OpenPgpMessageInputStream extends InputStream {
try {
System.out.println("Read consume");
consumePackets();
signatures.finish();
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
*/
}
return r;
}
@ -354,6 +385,16 @@ public class OpenPgpMessageInputStream extends InputStream {
int r = in.read(b, off, len);
if (r == -1) {
in.close();
in = null;
try {
consumePackets();
} catch (PGPException e) {
throw new RuntimeException(e);
}
signatures.finish();
/*
if (in instanceof OpenPgpMessageInputStream) {
in.close();
in = null;
@ -364,6 +405,7 @@ public class OpenPgpMessageInputStream extends InputStream {
throw new RuntimeException(e);
}
}
*/
}
return r;
}
@ -380,6 +422,12 @@ public class OpenPgpMessageInputStream extends InputStream {
in = null;
}
try {
consumePackets();
} catch (PGPException e) {
throw new RuntimeException(e);
}
automaton.next(InputAlphabet.EndOfSequence);
automaton.assertValid();
closed = true;
@ -418,50 +466,133 @@ public class OpenPgpMessageInputStream extends InputStream {
}
private static class Signatures {
final ConsumerOptions options;
List<PGPSignature> detachedSignatures = new ArrayList<>();
List<PGPSignature> prependedSignatures = new ArrayList<>();
List<PGPOnePassSignature> onePassSignatures = new ArrayList<>();
List<PGPSignature> correspondingSignatures = new ArrayList<>();
private Signatures(ConsumerOptions options) {
this.options = options;
}
void addDetachedSignatures(Collection<PGPSignature> signatures) {
for (PGPSignature signature : signatures) {
long keyId = SignatureUtils.determineIssuerKeyId(signature);
PGPPublicKeyRing certificate = findCertificate(keyId);
initialize(signature, certificate, keyId);
}
this.detachedSignatures.addAll(signatures);
}
void addPrependedSignatures(PGPSignatureList signatures) {
System.out.println("Adding " + signatures.size() + " prepended Signatures");
for (PGPSignature signature : signatures) {
long keyId = SignatureUtils.determineIssuerKeyId(signature);
PGPPublicKeyRing certificate = findCertificate(keyId);
initialize(signature, certificate, keyId);
this.prependedSignatures.add(signature);
}
}
void addOnePassSignatures(PGPOnePassSignatureList signatures) {
System.out.println("Adding " + signatures.size() + " OPSs");
for (PGPOnePassSignature ops : signatures) {
PGPPublicKeyRing certificate = findCertificate(ops.getKeyID());
initialize(ops, certificate);
this.onePassSignatures.add(ops);
}
}
void addOnePassCorrespondingSignatures(PGPSignatureList signatures) {
System.out.println("Adding " + signatures.size() + " Corresponding Signatures");
for (PGPSignature signature : signatures) {
correspondingSignatures.add(signature);
}
}
private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) {
if (certificate == null) {
// SHIT
return;
}
PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance()
.getPGPContentVerifierBuilderProvider();
try {
signature.init(verifierProvider, certificate.getPublicKey(keyId));
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
private void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) {
if (certificate == null) {
return;
}
PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance()
.getPGPContentVerifierBuilderProvider();
try {
ops.init(verifierProvider, certificate.getPublicKey(ops.getKeyID()));
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
private PGPPublicKeyRing findCertificate(long keyId) {
for (PGPPublicKeyRing cert : options.getCertificates()) {
PGPPublicKey verificationKey = cert.getPublicKey(keyId);
if (verificationKey != null) {
return cert;
}
}
return null; // TODO: Missing cert for sig
}
public void update(byte b) {
/**
for (PGPSignature prepended : prependedSignatures) {
prepended.update(b);
}
for (PGPOnePassSignature ops : onePassSignatures) {
ops.update(b);
}
for (PGPSignature detached : detachedSignatures) {
detached.update(b);
}
*/
for (PGPSignature prepended : prependedSignatures) {
prepended.update(b);
}
for (PGPOnePassSignature ops : onePassSignatures) {
ops.update(b);
}
for (PGPSignature detached : detachedSignatures) {
detached.update(b);
}
}
public void finish() {
for (PGPSignature detached : detachedSignatures) {
boolean verified = false;
try {
verified = detached.verify();
} catch (PGPException e) {
System.out.println(e.getMessage());
}
System.out.println("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified"));
}
for (PGPSignature prepended : prependedSignatures) {
boolean verified = false;
try {
verified = prepended.verify();
} catch (PGPException e) {
System.out.println(e.getMessage());
}
System.out.println("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified"));
}
for (int i = 0; i < onePassSignatures.size(); i++) {
PGPOnePassSignature ops = onePassSignatures.get(i);
PGPSignature signature = correspondingSignatures.get(correspondingSignatures.size() - i - 1);
boolean verified = false;
try {
verified = ops.verify(signature);
} catch (PGPException e) {
System.out.println(e.getMessage());
}
System.out.println("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified"));
}
}
}

View file

@ -1,339 +0,0 @@
package org.pgpainless.decryption_verification.automaton;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import java.util.Stack;
import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg;
import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops;
import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus;
/**
* Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption.
* <p>
* OpenPGP messages MUST follow certain rules in order to be well-formed.
* Section §11.3. of RFC4880 specifies a formal grammar for OpenPGP messages.
* <p>
* This grammar was transformed into a pushdown automaton, which is implemented below.
* The automaton only ends up in a valid state ({@link #isValid()} iff the OpenPGP message conformed to the
* grammar.
* <p>
* There are some specialties with this implementation though:
* Bouncy Castle combines ESKs and Encrypted Data Packets into a single object, so we do not have to
* handle those manually.
* <p>
* Bouncy Castle further combines OnePassSignatures and Signatures into lists, so instead of pushing multiple
* 'o's onto the stack repeatedly, a sequence of OnePassSignatures causes a single 'o' to be pushed to the stack.
* The same is true for Signatures.
* <p>
* Therefore, a message is valid, even if the number of OnePassSignatures and Signatures does not match.
* If a message contains at least one OnePassSignature, it is sufficient if there is at least one Signature to
* not cause a {@link MalformedOpenPgpMessageException}.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">RFC4880 §11.3. OpenPGP Messages</a>
*/
public class NestingPDA {
/**
* Set of states of the automaton.
* Each state defines its valid transitions in their {@link State#transition(InputAlphabet, NestingPDA)}
* method.
*/
public enum State {
OpenPgpMessage {
@Override
State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException {
StackAlphabet stackItem = automaton.popStack();
if (stackItem != msg) {
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
switch (input) {
case LiteralData:
return LiteralMessage;
case Signatures:
automaton.pushStack(msg);
return OpenPgpMessage;
case OnePassSignatures:
automaton.pushStack(ops);
automaton.pushStack(msg);
return OpenPgpMessage;
case CompressedData:
return CompressedMessage;
case EncryptedData:
return EncryptedMessage;
case EndOfSequence:
default:
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
}
},
LiteralMessage {
@Override
State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException {
StackAlphabet stackItem = automaton.popStack();
switch (input) {
case Signatures:
if (stackItem == ops) {
return CorrespondingSignature;
} else {
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
case EndOfSequence:
if (stackItem == terminus && automaton.stack.isEmpty()) {
return Valid;
} else {
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
case LiteralData:
case OnePassSignatures:
case CompressedData:
case EncryptedData:
default:
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
}
},
CompressedMessage {
@Override
State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException {
StackAlphabet stackItem = automaton.popStack();
switch (input) {
case Signatures:
if (stackItem == ops) {
return CorrespondingSignature;
} else {
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
case EndOfSequence:
if (stackItem == terminus && automaton.stack.isEmpty()) {
return Valid;
} else {
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
case LiteralData:
case OnePassSignatures:
case CompressedData:
case EncryptedData:
default:
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
}
},
EncryptedMessage {
@Override
State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException {
StackAlphabet stackItem = automaton.popStack();
switch (input) {
case Signatures:
if (stackItem == ops) {
return CorrespondingSignature;
} else {
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
case EndOfSequence:
if (stackItem == terminus && automaton.stack.isEmpty()) {
return Valid;
} else {
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
case LiteralData:
case OnePassSignatures:
case CompressedData:
case EncryptedData:
default:
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
}
},
CorrespondingSignature {
@Override
State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException {
StackAlphabet stackItem = automaton.popStack();
if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) {
return Valid;
} else {
throw new MalformedOpenPgpMessageException(this, input, stackItem);
}
}
},
Valid {
@Override
State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException {
throw new MalformedOpenPgpMessageException(this, input, null);
}
},
;
/**
* Pop the automatons stack and transition to another state.
* If no valid transition from the current state is available given the popped stack item and input symbol,
* a {@link MalformedOpenPgpMessageException} is thrown.
* Otherwise, the stack is manipulated according to the valid transition and the new state is returned.
*
* @param input input symbol
* @param automaton automaton
* @return new state of the automaton
* @throws MalformedOpenPgpMessageException in case of an illegal input symbol
*/
abstract State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException;
}
private final Stack<StackAlphabet> stack = new Stack<>();
private State state;
// Some OpenPGP packets have nested contents (e.g. compressed / encrypted data).
NestingPDA nestedSequence = null;
public NestingPDA() {
state = State.OpenPgpMessage;
stack.push(terminus);
stack.push(msg);
}
/**
* Process the next input packet.
*
* @param input input
* @throws MalformedOpenPgpMessageException in case the input packet is illegal here
*/
public void next(InputAlphabet input) throws MalformedOpenPgpMessageException {
_next(input);
}
/**
* Process the next input packet.
* This method returns true, iff the given input triggered a successful closing of this PDAs nested PDA.
* <p>
* This is for example the case, if the current packet is a Compressed Data packet which contains a
* valid nested OpenPGP message and the last input was {@link InputAlphabet#EndOfSequence} indicating the
* end of the Compressed Data packet.
* <p>
* If the input triggered this PDAs nested PDA to close its nested PDA, this method returns false
* in order to prevent this PDA from closing its nested PDA prematurely.
*
* @param input input
* @return true if this just closed its nested sequence, false otherwise
* @throws MalformedOpenPgpMessageException if the input is illegal
*/
private boolean _next(InputAlphabet input) throws MalformedOpenPgpMessageException {
if (nestedSequence != null) {
boolean sequenceInNestedSequenceWasClosed = nestedSequence._next(input);
if (sequenceInNestedSequenceWasClosed) return false; // No need to close out nested sequence too.
} else {
// make a state transition in this automaton
state = state.transition(input, this);
// If the processed packet contains nested sequence, open nested automaton for it
if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) {
nestedSequence = new NestingPDA();
}
}
if (input != InputAlphabet.EndOfSequence) {
return false;
}
// Close nested sequence if needed
boolean nestedIsInnerMost = nestedSequence != null && nestedSequence.isInnerMost();
if (nestedIsInnerMost) {
if (nestedSequence.isValid()) {
// Close nested sequence
nestedSequence = null;
return true;
} else {
throw new MalformedOpenPgpMessageException("Climbing up nested message validation failed." +
" Automaton for current nesting level is not in valid state: " + nestedSequence.getState() + " " + nestedSequence.stack.peek() + " (Input was " + input + ")");
}
}
return false;
}
/**
* Return the current state of the PDA.
*
* @return state
*/
private State getState() {
return state;
}
/**
* Return true, if the PDA is in a valid state (the OpenPGP message is valid).
*
* @return true if valid, false otherwise
*/
public boolean isValid() {
return getState() == State.Valid && stack.isEmpty();
}
public void assertValid() throws MalformedOpenPgpMessageException {
if (!isValid()) {
throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString());
}
}
/**
* Pop an item from the stack.
*
* @return stack item
*/
private StackAlphabet popStack() {
return stack.pop();
}
/**
* Push an item onto the stack.
*
* @param item item
*/
private void pushStack(StackAlphabet item) {
stack.push(item);
}
/**
* Return true, if this packet sequence has no nested sequence.
* A nested sequence is for example the content of a Compressed Data packet.
*
* @return true if PDA is innermost, false if it has a nested sequence
*/
private boolean isInnerMost() {
return nestedSequence == null;
}
@Override
public String toString() {
StringBuilder out = new StringBuilder("State: ").append(state)
.append(", Stack (asc.): ").append(stack)
.append('\n');
if (nestedSequence != null) {
// recursively call toString() on nested PDAs and indent their representation
String nestedToString = nestedSequence.toString();
String[] lines = nestedToString.split("\n");
for (int i = 0; i < lines.length; i++) {
String nestedLine = lines[i];
out.append(i == 0 ? "" : " ") // indent nested PDA
.append(nestedLine)
.append('\n');
}
}
return out.toString();
}
}

View file

@ -5,7 +5,6 @@
package org.pgpainless.exception;
import org.pgpainless.decryption_verification.automaton.InputAlphabet;
import org.pgpainless.decryption_verification.automaton.NestingPDA;
import org.pgpainless.decryption_verification.automaton.PDA;
import org.pgpainless.decryption_verification.automaton.StackAlphabet;
@ -21,21 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException {
super(message);
}
public MalformedOpenPgpMessageException(String message, MalformedOpenPgpMessageException cause) {
super(message, cause);
}
public MalformedOpenPgpMessageException(NestingPDA.State state,
InputAlphabet input,
StackAlphabet stackItem) {
this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack.");
}
public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) {
this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack.");
}
public MalformedOpenPgpMessageException(String message, PDA automaton) {
super(message + automaton.toString());
}
}