1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-12-23 03:17:58 +01:00

Relax constraints on decryption keys to improve interop with faulty, broken legacy clients that have been very naughty and need punishment

This commit is contained in:
Paul Schaub 2023-04-25 13:28:07 +02:00
parent d5f3dc80bc
commit 0cb0885251
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
3 changed files with 68 additions and 5 deletions

View file

@ -43,15 +43,14 @@ import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory;
import org.bouncycastle.util.io.TeeInputStream; import org.bouncycastle.util.io.TeeInputStream;
import org.pgpainless.PGPainless; import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.EncryptionPurpose;
import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.algorithm.OpenPgpPacket;
import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.decryption_verification.syntax_check.InputSymbol; import org.pgpainless.decryption_verification.syntax_check.InputSymbol;
import org.pgpainless.decryption_verification.syntax_check.PDA; import org.pgpainless.decryption_verification.syntax_check.PDA;
import org.pgpainless.decryption_verification.syntax_check.StackSymbol; import org.pgpainless.decryption_verification.syntax_check.StackSymbol;
import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MessageNotIntegrityProtectedException;
import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.MissingDecryptionMethodException;
@ -674,7 +673,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream {
for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) {
KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys);
for (PGPPublicKey publicKey : info.getEncryptionSubkeys(EncryptionPurpose.ANY)) { for (PGPPublicKey publicKey : info.getDecryptionSubkeys()) {
if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) {
PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID());
decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate));
@ -692,7 +691,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream {
} }
KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date());
List<PGPPublicKey> encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); List<PGPPublicKey> encryptionKeys = info.getDecryptionSubkeys();
for (PGPPublicKey key : encryptionKeys) { for (PGPPublicKey key : encryptionKeys) {
if (key.getKeyID() == keyID) { if (key.getKeyID() == keyID) {
return secretKeys; return secretKeys;

View file

@ -41,11 +41,15 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.exception.KeyException; import org.pgpainless.exception.KeyException;
import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.OpenPgpFingerprint;
import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.util.KeyIdUtil;
import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.key.util.RevocationAttributes;
import org.pgpainless.policy.Policy; import org.pgpainless.policy.Policy;
import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.signature.consumer.SignaturePicker; import org.pgpainless.signature.consumer.SignaturePicker;
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
import org.pgpainless.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Utility class to quickly extract certain information from a {@link PGPPublicKeyRing}/{@link PGPSecretKeyRing}. * Utility class to quickly extract certain information from a {@link PGPPublicKeyRing}/{@link PGPSecretKeyRing}.
@ -55,6 +59,8 @@ public class KeyRingInfo {
private static final Pattern PATTERN_EMAIL_FROM_USERID = Pattern.compile("<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>"); private static final Pattern PATTERN_EMAIL_FROM_USERID = Pattern.compile("<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>");
private static final Pattern PATTERN_EMAIL_EXPLICIT = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$"); private static final Pattern PATTERN_EMAIL_EXPLICIT = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$");
private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingInfo.class);
private final PGPKeyRing keys; private final PGPKeyRing keys;
private final Signatures signatures; private final Signatures signatures;
private final Date referenceDate; private final Date referenceDate;
@ -904,6 +910,7 @@ public class KeyRingInfo {
public @Nonnull List<PGPPublicKey> getEncryptionSubkeys(EncryptionPurpose purpose) { public @Nonnull List<PGPPublicKey> getEncryptionSubkeys(EncryptionPurpose purpose) {
Date primaryExpiration = getPrimaryKeyExpirationDate(); Date primaryExpiration = getPrimaryKeyExpirationDate();
if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { if (primaryExpiration != null && primaryExpiration.before(referenceDate)) {
LOGGER.debug("Certificate is expired: Primary key is expired on " + DateUtil.formatUTCDate(primaryExpiration));
return Collections.emptyList(); return Collections.emptyList();
} }
@ -913,15 +920,18 @@ public class KeyRingInfo {
PGPPublicKey subKey = subkeys.next(); PGPPublicKey subKey = subkeys.next();
if (!isKeyValidlyBound(subKey.getKeyID())) { if (!isKeyValidlyBound(subKey.getKeyID())) {
LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not validly bound.");
continue; continue;
} }
Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey)); Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey));
if (subkeyExpiration != null && subkeyExpiration.before(referenceDate)) { if (subkeyExpiration != null && subkeyExpiration.before(referenceDate)) {
LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is expired on " + DateUtil.formatUTCDate(subkeyExpiration));
continue; continue;
} }
if (!subKey.isEncryptionKey()) { if (!subKey.isEncryptionKey()) {
LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " algorithm is not capable of encryption.");
continue; continue;
} }
@ -947,6 +957,42 @@ public class KeyRingInfo {
return encryptionKeys; return encryptionKeys;
} }
/**
* Return a list of all subkeys that could potentially be used to decrypt a message.
* Contrary to {@link #getEncryptionSubkeys(EncryptionPurpose)}, this method also includes revoked, expired keys,
* as well as keys which do not carry any encryption keyflags.
* Merely keys which use algorithms that cannot be used for encryption at all are excluded.
* That way, decryption of messages produced by faulty implementations can still be decrypted.
*
* @return decryption keys
*/
public @Nonnull List<PGPPublicKey> getDecryptionSubkeys() {
Iterator<PGPPublicKey> subkeys = keys.getPublicKeys();
List<PGPPublicKey> decryptionKeys = new ArrayList<>();
while (subkeys.hasNext()) {
PGPPublicKey subKey = subkeys.next();
// subkeys have been valid at some point
if (subKey.getKeyID() != getKeyId()) {
PGPSignature binding = signatures.subkeyBindings.get(subKey.getKeyID());
if (binding == null) {
LOGGER.debug("Subkey " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " was never validly bound.");
continue;
}
}
// Public-Key algorithm can encrypt
if (!subKey.isEncryptionKey()) {
LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not encryption-capable.");
continue;
}
decryptionKeys.add(subKey);
}
return decryptionKeys;
}
/** /**
* Return a list of all keys which carry the provided key flag in their signature. * Return a list of all keys which carry the provided key flag in their signature.
* *

View file

@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.io.Streams; import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless; import org.pgpainless.PGPainless;
import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.MissingDecryptionMethodException;
@ -189,6 +190,23 @@ public class PreventDecryptionUsingNonEncryptionKeyTest {
} }
@Test @Test
public void canDecryptMessageDespiteMissingKeyFlag() throws IOException, PGPException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY);
ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8));
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(msgIn)
.withOptions(new ConsumerOptions().addDecryptionKey(secretKeys));
Streams.drain(decryptionStream);
decryptionStream.close();
OpenPgpMetadata metadata = decryptionStream.getResult();
assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey());
}
@Test
@Disabled
public void nonEncryptionKeyCannotDecrypt() throws IOException { public void nonEncryptionKeyCannotDecrypt() throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY);