diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureException.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureException.java new file mode 100644 index 0000000000..88b2887319 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureException.java @@ -0,0 +1,15 @@ +package org.bouncycastle.openpgp; + +public class PGPSignatureException + extends PGPException +{ + public PGPSignatureException(String message) + { + super(message); + } + + public PGPSignatureException(String message, Exception cause) + { + super(message, cause); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/BcOpenPGPImplementation.java b/pg/src/main/java/org/bouncycastle/openpgp/api/BcOpenPGPImplementation.java new file mode 100644 index 0000000000..b826ea6c26 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/BcOpenPGPImplementation.java @@ -0,0 +1,109 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptorBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilderProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcSessionKeyDataDecryptorFactory; + +import java.io.InputStream; + +public class BcOpenPGPImplementation + extends OpenPGPImplementation +{ + @Override + public PGPObjectFactory pgpObjectFactory(InputStream packetInputStream) + { + return new BcPGPObjectFactory(packetInputStream); + } + + @Override + public PGPContentVerifierBuilderProvider pgpContentVerifierBuilderProvider() + { + return new BcPGPContentVerifierBuilderProvider(); + } + + @Override + public PBESecretKeyDecryptorBuilderProvider pbeSecretKeyDecryptorBuilderProvider() + { + return new BcPBESecretKeyDecryptorBuilderProvider(); + } + + @Override + public PGPDataEncryptorBuilder pgpDataEncryptorBuilder(int symmetricKeyAlgorithm) + { + return new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm); + } + + @Override + public PublicKeyKeyEncryptionMethodGenerator publicKeyKeyEncryptionMethodGenerator(PGPPublicKey encryptionSubkey) + { + return new BcPublicKeyKeyEncryptionMethodGenerator(encryptionSubkey); + } + + @Override + public PBEKeyEncryptionMethodGenerator pbeKeyEncryptionMethodGenerator(char[] messagePassphrase) + { + return new BcPBEKeyEncryptionMethodGenerator(messagePassphrase); + } + + @Override + public PBEKeyEncryptionMethodGenerator pbeKeyEncryptionMethodGenerator(char[] messagePassphrase, S2K.Argon2Params argon2Params) + { + return new BcPBEKeyEncryptionMethodGenerator(messagePassphrase, argon2Params); + } + + @Override + public PGPContentSignerBuilder pgpContentSignerBuilder(int publicKeyAlgorithm, int hashAlgorithm) + { + return new BcPGPContentSignerBuilder(publicKeyAlgorithm, hashAlgorithm); + } + + @Override + public PBEDataDecryptorFactory pbeDataDecryptorFactory(char[] messagePassphrase) + throws PGPException + { + return new BcPBEDataDecryptorFactory(messagePassphrase, pgpDigestCalculatorProvider()); + } + + @Override + public SessionKeyDataDecryptorFactory sessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) + { + return new BcSessionKeyDataDecryptorFactory(sessionKey); + } + + @Override + public PublicKeyDataDecryptorFactory publicKeyDataDecryptorFactory(PGPPrivateKey decryptionKey) + { + return new BcPublicKeyDataDecryptorFactory(decryptionKey); + } + + @Override + public PGPDigestCalculatorProvider pgpDigestCalculatorProvider() + throws PGPException + { + return new BcPGPDigestCalculatorProvider(); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/EncryptedDataPacketType.java b/pg/src/main/java/org/bouncycastle/openpgp/api/EncryptedDataPacketType.java new file mode 100644 index 0000000000..e8f5a67aa8 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/EncryptedDataPacketType.java @@ -0,0 +1,37 @@ +package org.bouncycastle.openpgp.api; + +/** + * Encryption Mode. + */ +public enum EncryptedDataPacketType +{ + /** + * Symmetrically-Encrypted Data packet. + * This method is deprecated, as it does not protect against malleability. + * + * @deprecated + */ + @Deprecated + SED, // deprecated + /** + * Symmetrically-Encrypted-Integrity-Protected Data packet version 1. + * This method protects the message using symmetric encryption as specified in RFC4880. + * Support for this encryption mode is signalled using + * {@link org.bouncycastle.bcpg.sig.Features#FEATURE_MODIFICATION_DETECTION}. + */ + SEIPDv1, // v4 + + /** + * Symmetrically-Encrypted-Integrity-Protected Data packet version 2. + * This method protects the message using an AEAD encryption scheme specified in RFC9580. + * Support for this feature is signalled using {@link org.bouncycastle.bcpg.sig.Features#FEATURE_SEIPD_V2}. + */ + SEIPDv2, // v6 + + /** + * LibrePGP OCB-Encrypted Data packet. + * This method protects the message using an AEAD encryption scheme specified in LibrePGP. + * Support for this feature is signalled using {@link org.bouncycastle.bcpg.sig.Features#FEATURE_AEAD_ENCRYPTED_DATA}. + */ + LIBREPGP_OED // "v5" +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/JcaOpenPGPImplementation.java b/pg/src/main/java/org/bouncycastle/openpgp/api/JcaOpenPGPImplementation.java new file mode 100644 index 0000000000..c1db969bbd --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/JcaOpenPGPImplementation.java @@ -0,0 +1,157 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptorBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilderProvider; +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JceSessionKeyDataDecryptorFactoryBuilder; + +import java.io.InputStream; +import java.security.Provider; +import java.security.SecureRandom; + +public class JcaOpenPGPImplementation + extends OpenPGPImplementation +{ + private final Provider provider; + private final SecureRandom secureRandom; + + public JcaOpenPGPImplementation() + { + this(new BouncyCastleProvider(), CryptoServicesRegistrar.getSecureRandom()); + } + + public JcaOpenPGPImplementation(Provider provider, SecureRandom secureRandom) + { + this.provider = provider; + this.secureRandom = secureRandom; + } + + @Override + public PGPObjectFactory pgpObjectFactory(InputStream packetInputStream) + { + return new JcaPGPObjectFactory(packetInputStream); + } + + @Override + public PGPContentVerifierBuilderProvider pgpContentVerifierBuilderProvider() + { + JcaPGPContentVerifierBuilderProvider p = new JcaPGPContentVerifierBuilderProvider(); + p.setProvider(provider); + return p; + } + + @Override + public PBESecretKeyDecryptorBuilderProvider pbeSecretKeyDecryptorBuilderProvider() + { + JcaPGPDigestCalculatorProviderBuilder dp = new JcaPGPDigestCalculatorProviderBuilder(); + dp.setProvider(provider); + JcePBESecretKeyDecryptorBuilderProvider p = new JcePBESecretKeyDecryptorBuilderProvider(dp); + return p; + } + + @Override + public PGPDataEncryptorBuilder pgpDataEncryptorBuilder(int symmetricKeyAlgorithm) + { + JcePGPDataEncryptorBuilder b = new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm); + b.setProvider(provider); + b.setSecureRandom(secureRandom); + return b; + } + + @Override + public PublicKeyKeyEncryptionMethodGenerator publicKeyKeyEncryptionMethodGenerator(PGPPublicKey encryptionSubkey) + { + JcePublicKeyKeyEncryptionMethodGenerator g = new JcePublicKeyKeyEncryptionMethodGenerator(encryptionSubkey); + g.setProvider(provider); + g.setSecureRandom(secureRandom); + return g; + } + + @Override + public PBEKeyEncryptionMethodGenerator pbeKeyEncryptionMethodGenerator(char[] messagePassphrase) + { + JcePBEKeyEncryptionMethodGenerator g = new JcePBEKeyEncryptionMethodGenerator(messagePassphrase); + g.setProvider(provider); + g.setSecureRandom(secureRandom); + return g; + } + + @Override + public PBEKeyEncryptionMethodGenerator pbeKeyEncryptionMethodGenerator(char[] messagePassphrase, S2K.Argon2Params argon2Params) + { + JcePBEKeyEncryptionMethodGenerator g = new JcePBEKeyEncryptionMethodGenerator(messagePassphrase, argon2Params); + g.setProvider(provider); + g.setSecureRandom(secureRandom); + return g; + } + + @Override + public PGPContentSignerBuilder pgpContentSignerBuilder(int publicKeyAlgorithm, int hashAlgorithm) + { + JcaPGPContentSignerBuilder b = new JcaPGPContentSignerBuilder(publicKeyAlgorithm, hashAlgorithm); + b.setProvider(provider); + b.setDigestProvider(provider); + b.setSecureRandom(secureRandom); + return b; + } + + @Override + public PBEDataDecryptorFactory pbeDataDecryptorFactory(char[] messagePassphrase) + throws PGPException + { + return new JcePBEDataDecryptorFactoryBuilder(pgpDigestCalculatorProvider()) + .setProvider(provider) + .build(messagePassphrase); + } + + @Override + public SessionKeyDataDecryptorFactory sessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) + { + return new JceSessionKeyDataDecryptorFactoryBuilder() + .setProvider(provider) + .build(sessionKey); + } + + @Override + public PublicKeyDataDecryptorFactory publicKeyDataDecryptorFactory(PGPPrivateKey decryptionKey) + { + return new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(provider) + .setContentProvider(provider) + .build(decryptionKey); + } + + @Override + public PGPDigestCalculatorProvider pgpDigestCalculatorProvider() + throws PGPException + { + return new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(provider) + .build(); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/KeyPassphraseProvider.java b/pg/src/main/java/org/bouncycastle/openpgp/api/KeyPassphraseProvider.java new file mode 100644 index 0000000000..58c5fac2d6 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/KeyPassphraseProvider.java @@ -0,0 +1,112 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.util.Arrays; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public interface KeyPassphraseProvider +{ + /** + * Return the passphrase for the given key. + * This callback is only fired, if the key is locked and a passphrase is required to unlock it. + * Returning null means, that the passphrase is not available. + * + * @param key the locked (sub-)key. + * @return passphrase or null + */ + char[] getKeyPassword(OpenPGPKey.OpenPGPSecretKey key); + + class DefaultKeyPassphraseProvider + implements KeyPassphraseProvider + { + private final Map passphraseMap = new HashMap<>(); + private final List unassociatedPassphrases = new ArrayList<>(); + private KeyPassphraseProvider callback; + + public DefaultKeyPassphraseProvider() + { + + } + + public DefaultKeyPassphraseProvider(OpenPGPKey key, char[] passphrase) + { + for (OpenPGPKey.OpenPGPSecretKey subkey : key.getSecretKeys().values()) + { + passphraseMap.put(subkey, passphrase); + } + } + + @Override + public char[] getKeyPassword(OpenPGPKey.OpenPGPSecretKey key) + { + if (key.isLocked()) + { + char[] passphrase = passphraseMap.get(key); + if (passphrase != null) + { + return passphrase; + } + + for (char[] unassociatedPassphrase : unassociatedPassphrases) + { + passphrase = unassociatedPassphrase; + if (key.isPassphraseCorrect(passphrase)) + { + addPassphrase(key, passphrase); + return passphrase; + } + } + + if (callback != null) + { + passphrase = callback.getKeyPassword(key); + addPassphrase(key, passphrase); + } + return passphrase; + } + else + { + return null; + } + } + + public DefaultKeyPassphraseProvider addPassphrase(char[] passphrase) + { + boolean found = false; + for (char[] existing : unassociatedPassphrases) + { + found |= (Arrays.areEqual(existing, passphrase)); + } + + if (!found) + { + unassociatedPassphrases.add(passphrase); + } + return this; + } + + public DefaultKeyPassphraseProvider addPassphrase(OpenPGPKey key, char[] passphrase) + { + for (OpenPGPKey.OpenPGPSecretKey subkey : key.getSecretKeys().values()) + { + addPassphrase(subkey, passphrase); + } + return this; + } + + public DefaultKeyPassphraseProvider addPassphrase(OpenPGPKey.OpenPGPSecretKey key, char[] passphrase) + { + passphraseMap.put(key, passphrase); + return this; + } + + public DefaultKeyPassphraseProvider setMissingPassphraseCallback(KeyPassphraseProvider callback) + { + this.callback = callback; + return this; + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/MessageEncryptionMechanism.java b/pg/src/main/java/org/bouncycastle/openpgp/api/MessageEncryptionMechanism.java new file mode 100644 index 0000000000..0fb1d36673 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/MessageEncryptionMechanism.java @@ -0,0 +1,134 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.AEADAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; + +/** + * Encryption mode (SEIPDv1 / SEIPDv2 / OED) and algorithms. + */ +public class MessageEncryptionMechanism +{ + private final EncryptedDataPacketType mode; + private final int symmetricKeyAlgorithm; + private final int aeadAlgorithm; + + /** + * Create a {@link MessageEncryptionMechanism} tuple. + * + * @param mode encryption mode (packet type) + * @param symmetricKeyAlgorithm symmetric key algorithm for message encryption + * @param aeadAlgorithm aead algorithm for message encryption + */ + private MessageEncryptionMechanism(EncryptedDataPacketType mode, + int symmetricKeyAlgorithm, + int aeadAlgorithm) + { + this.mode = mode; + this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; + this.aeadAlgorithm = aeadAlgorithm; + } + + public EncryptedDataPacketType getMode() + { + return mode; + } + + public int getSymmetricKeyAlgorithm() + { + return symmetricKeyAlgorithm; + } + + public int getAeadAlgorithm() + { + return aeadAlgorithm; + } + + /** + * The data will not be encrypted. + * Useful for sign-only operations. + * + * @return unencrypted encryption setup + */ + public static MessageEncryptionMechanism unencrypted() + { + int none = 0; + return new MessageEncryptionMechanism(EncryptedDataPacketType.SEIPDv1, + SymmetricKeyAlgorithmTags.NULL, none); + } + + /** + * The data will be encrypted and integrity protected using a SEIPDv1 packet. + * + * @param symmetricKeyAlgorithm symmetric cipher algorithm for message encryption + * @return sym. enc. integrity protected encryption setup + */ + public static MessageEncryptionMechanism integrityProtected(int symmetricKeyAlgorithm) + { + int none = 0; + return new MessageEncryptionMechanism(EncryptedDataPacketType.SEIPDv1, symmetricKeyAlgorithm, none); + } + + /** + * The data will be OCB-encrypted as specified by the non-standard LibrePGP document. + * + * @param symmetricKeyAlgorithm symmetric key algorithm which will be combined with OCB to form + * an OCB-encrypted data packet + * @return LibrePGP OCB encryption setup + */ + public static MessageEncryptionMechanism librePgp(int symmetricKeyAlgorithm) + { + return new MessageEncryptionMechanism(EncryptedDataPacketType.LIBREPGP_OED, + symmetricKeyAlgorithm, AEADAlgorithmTags.OCB); + } + + /** + * The data will be AEAD-encrypted using the method described in RFC9580. + * + * @param symmetricKeyAlgorithm symmetric cipher algorithm + * @param aeadAlgorithm AEAD algorithm + * @return AEAD encryption setup + */ + public static MessageEncryptionMechanism aead(int symmetricKeyAlgorithm, int aeadAlgorithm) + { + return new MessageEncryptionMechanism(EncryptedDataPacketType.SEIPDv2, symmetricKeyAlgorithm, aeadAlgorithm); + } + + /** + * Return true, if the message will be encrypted. + * + * @return is encrypted + */ + public boolean isEncrypted() + { + return symmetricKeyAlgorithm != SymmetricKeyAlgorithmTags.NULL; + } + + @Override + public int hashCode() + { + return mode.hashCode() + + 13 * symmetricKeyAlgorithm + + 17 * aeadAlgorithm; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + if (this == obj) + { + return true; + } + if (!(obj instanceof MessageEncryptionMechanism)) + { + return false; + } + MessageEncryptionMechanism m = (MessageEncryptionMechanism) obj; + return getMode() == m.getMode() + && getSymmetricKeyAlgorithm() == m.getSymmetricKeyAlgorithm() + && getAeadAlgorithm() == m.getAeadAlgorithm(); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/MissingPassphraseCallback.java b/pg/src/main/java/org/bouncycastle/openpgp/api/MissingPassphraseCallback.java new file mode 100644 index 0000000000..2e547018a0 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/MissingPassphraseCallback.java @@ -0,0 +1,13 @@ +package org.bouncycastle.openpgp.api; + +public interface MissingPassphraseCallback +{ + /** + * Return a passphrase for message decryption. + * Returning null means, that no passphrase is available and decryption is aborted. + * + * @return passphrase + */ + char[] getPassphrase(); + +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java new file mode 100644 index 0000000000..c34d3c2207 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java @@ -0,0 +1,2049 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.FingerprintUtil; +import org.bouncycastle.bcpg.PacketFormat; +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites; +import org.bouncycastle.bcpg.sig.PreferredAlgorithms; +import org.bouncycastle.openpgp.*; +import org.bouncycastle.openpgp.api.exception.IncorrectPGPSignatureException; +import org.bouncycastle.openpgp.api.exception.MalformedPGPSignatureException; +import org.bouncycastle.openpgp.api.exception.MissingIssuerCertException; +import org.bouncycastle.openpgp.api.util.UTCUtil; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.util.Iterable; +import org.bouncycastle.util.encoders.Hex; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * OpenPGP certificates (TPKs - transferable public keys) are long-living structures that may change during + * their lifetime. A key-holder may add new components like subkeys or identities, along with associated + * binding self-signatures to the certificate and old components may expire / get revoked at some point. + * Since any such changes may have an influence on whether a data signature is valid at a given time, or what subkey + * should be used when generating an encrypted / signed message, an API is needed that provides a view on the + * certificate that takes into consideration a relevant window in time. + *

+ * Compared to a {@link PGPPublicKeyRing}, an {@link OpenPGPCertificate} has been evaluated at (or rather for) + * a given evaluation time. It offers a clean API for accessing the key-holder's preferences at a specific + * point in time and makes sure, that relevant self-signatures on certificate components are validated and verified. + * + * @see OpenPGP for Application Developers - Chapter 4 + * for background information on the terminology used in this class. + */ +public class OpenPGPCertificate +{ + private final OpenPGPImplementation implementation; + + private final PGPKeyRing keyRing; + + private final OpenPGPPrimaryKey primaryKey; + private final Map subkeys; + + // Note: get() needs to be accessed with OpenPGPCertificateComponent.getPublicComponent() to ensure + // proper functionality with secret key components. + private final Map componentSignatureChains; + + public OpenPGPCertificate(PGPKeyRing keyRing) + { + this(keyRing, OpenPGPImplementation.getInstance()); + } + + /** + * Instantiate an {@link OpenPGPCertificate} from a parsed {@link PGPPublicKeyRing}. + * + * @param keyRing public key ring + * @param implementation OpenPGP implementation + */ + public OpenPGPCertificate(PGPKeyRing keyRing, OpenPGPImplementation implementation) + { + this.implementation = implementation; + + this.keyRing = keyRing; + this.subkeys = new HashMap<>(); + this.componentSignatureChains = new LinkedHashMap<>(); + + Iterator rawKeys = keyRing.getPublicKeys(); + + PGPPublicKey rawPrimaryKey = rawKeys.next(); + this.primaryKey = new OpenPGPPrimaryKey(rawPrimaryKey, this); + processPrimaryKey(primaryKey); + + while (rawKeys.hasNext()) + { + PGPPublicKey rawSubkey = rawKeys.next(); + OpenPGPSubkey subkey = new OpenPGPSubkey(rawSubkey, this); + subkeys.put(new KeyIdentifier(rawSubkey), subkey); + processSubkey(subkey); + } + } + + /** + * Parse an {@link OpenPGPCertificate} (or {@link OpenPGPKey}) from its ASCII armored representation. + * @param armor ASCII armored key or certificate + * @return certificate or key + * @throws IOException + */ + public static OpenPGPCertificate fromAsciiArmor(String armor) + throws IOException + { + return fromAsciiArmor(armor, OpenPGPImplementation.getInstance()); + } + + /** + * Parse an {@link OpenPGPCertificate} (or {@link OpenPGPKey}) from its ASCII armored representation. + * @param armor ASCII armored key or certificate + * @param implementation OpenPGP implementation + * @return certificate or key + * @throws IOException + */ + public static OpenPGPCertificate fromAsciiArmor( + String armor, + OpenPGPImplementation implementation) + throws IOException + { + return fromBytes( + armor.getBytes(StandardCharsets.UTF_8), + implementation); + } + + public static OpenPGPCertificate fromBytes( + byte[] bytes, + OpenPGPImplementation implementation) + throws IOException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(bytes); + InputStream decoderStream = PGPUtil.getDecoderStream(bIn); + BCPGInputStream pIn = BCPGInputStream.wrap(decoderStream); + PGPObjectFactory objectFactory = implementation.pgpObjectFactory(pIn); + Object object = objectFactory.nextObject(); + + // TODO: Is it dangerous, if we don't explicitly fail upon encountering secret key material here? + // Could it lead to a situation where we need to be cautious with the certificate API design to + // prevent the user from doing dangerous things like accidentally publishing their private key? + + if (object instanceof PGPSecretKeyRing) + { + return new OpenPGPKey((PGPSecretKeyRing) object, implementation); + } + else if (object instanceof PGPPublicKeyRing) + { + return new OpenPGPCertificate((PGPPublicKeyRing) object, implementation); + } + else + { + throw new IOException("Neither a certificate, nor secret key."); + } + } + + + /** + * Return the primary key of the certificate. + * + * @return primary key + */ + public OpenPGPPrimaryKey getPrimaryKey() + { + return primaryKey; + } + + /** + * Return a {@link Map} containing the subkeys of this certificate, keyed by their {@link KeyIdentifier}. + * Note: This map does NOT contain the primary key ({@link #getPrimaryKey()}). + * + * @return subkeys + */ + public Map getSubkeys() + { + return new HashMap<>(subkeys); + } + + /** + * Return a {@link List} containing all {@link OpenPGPCertificateComponent components} of the certificate. + * Components are primary key, subkeys and identities (user-ids, user attributes). + * + * @return list of components + */ + public List getComponents() + { + return new ArrayList<>(componentSignatureChains.keySet()); + } + + /** + * Return all {@link OpenPGPComponentKey OpenPGPComponentKeys} in the certificate. + * The return value is a {@link List} containing the {@link OpenPGPPrimaryKey} and all + * {@link OpenPGPSubkey OpenPGPSubkeys}. + * + * @return list of all component keys + */ + public List getKeys() + { + List keys = new ArrayList<>(); + keys.add(primaryKey); + keys.addAll(subkeys.values()); + return keys; + } + + /** + * Return the {@link OpenPGPComponentKey} identified by the passed in {@link KeyIdentifier}. + * + * @param identifier key identifier + * @return component key + */ + public OpenPGPComponentKey getKey(KeyIdentifier identifier) + { + if (identifier.matches(getPrimaryKey().getPGPPublicKey())) + { + return primaryKey; + } + + return subkeys.get(identifier); + } + + /** + * Return the {@link OpenPGPComponentKey} that likely issued the passed in {@link PGPSignature}. + * + * @param signature signature + * @return issuer (sub-)key + */ + public OpenPGPComponentKey getSigningKeyFor(PGPSignature signature) + { + List keyIdentifiers = signature.getKeyIdentifiers(); + // issuer is primary key + if (KeyIdentifier.matches(keyIdentifiers, getPrimaryKey().getKeyIdentifier(), true)) + { + return primaryKey; + } + + for (KeyIdentifier subkeyIdentifier : subkeys.keySet()) + { + if (KeyIdentifier.matches(keyIdentifiers, subkeyIdentifier, true)) + { + return subkeys.get(subkeyIdentifier); + } + } + + return null; // external issuer + } + + /** + * Return the {@link PGPKeyRing} that this certificate is based on. + * + * @return underlying key ring + */ + public PGPKeyRing getPGPKeyRing() + { + return keyRing; + } + + public PGPPublicKeyRing getPGPPublicKeyRing() + { + if (keyRing instanceof PGPPublicKeyRing) + { + return (PGPPublicKeyRing) keyRing; + } + + List list = new ArrayList<>(); + for (Iterator it = keyRing.getPublicKeys(); it.hasNext(); ) + { + list.add(it.next()); + } + return new PGPPublicKeyRing(list); + } + + public KeyIdentifier getKeyIdentifier() + { + return primaryKey.getKeyIdentifier(); + } + + /** + * Return a list of ALL (sub-)key's identifiers, including those of expired / revoked / unbound keys. + * @return all keys identifiers + */ + public List getAllKeyIdentifiers() + { + List identifiers = new ArrayList<>(); + for (Iterator it = keyRing.getPublicKeys(); it.hasNext(); ) + { + PGPPublicKey key = it.next(); + identifiers.add(key.getKeyIdentifier()); + } + return identifiers; + } + + public static OpenPGPCertificate join(OpenPGPCertificate certificate, String armored) + throws IOException, PGPException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(armored.getBytes()); + InputStream decoderStream = PGPUtil.getDecoderStream(bIn); + BCPGInputStream wrapper = BCPGInputStream.wrap(decoderStream); + PGPObjectFactory objFac = certificate.implementation.pgpObjectFactory(wrapper); + + Object next; + while ((next = objFac.nextObject()) != null) + { + if (next instanceof PGPPublicKeyRing) + { + PGPPublicKeyRing publicKeys = (PGPPublicKeyRing) next; + OpenPGPCertificate otherCert = new OpenPGPCertificate(publicKeys, certificate.implementation); + try + { + return join(certificate, otherCert); + } + catch (IllegalArgumentException e) + { + // skip over wrong certificate + } + } + + else if (next instanceof PGPSecretKeyRing) + { + + } + + else if (next instanceof PGPSignatureList) + { + // assume there to be primary key (self) signatures + // TODO: Allow consumption of 3rd-party sigs + PGPSignatureList signatures = (PGPSignatureList) next; + + PGPPublicKeyRing publicKeys = certificate.getPGPPublicKeyRing(); + PGPPublicKey primaryKey = publicKeys.getPublicKey(); + for (PGPSignature signature : signatures) + { + primaryKey = PGPPublicKey.addCertification(primaryKey, signature); + } + publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, primaryKey); + return new OpenPGPCertificate(publicKeys, certificate.implementation); + } + } + return null; + } + + public static OpenPGPCertificate join(OpenPGPCertificate certificate, OpenPGPCertificate other) + throws PGPException + { + PGPPublicKeyRing joined = PGPPublicKeyRing.join( + certificate.getPGPPublicKeyRing(), other.getPGPPublicKeyRing()); + return new OpenPGPCertificate(joined, certificate.implementation); + } + + public byte[] getFingerprint() + { + return primaryKey.getPGPPublicKey().getFingerprint(); + } + + public String getPrettyFingerprint() + { + return FingerprintUtil.prettifyFingerprint(getFingerprint()); + } + + public String toAsciiArmoredString() + throws IOException + { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + ArmoredOutputStream.Builder armorBuilder = ArmoredOutputStream.builder() + .clearHeaders(); + // Add fingerprint comment + splitMultilineComment(armorBuilder, getPrettyFingerprint()); + + // Add user-id comments + for (OpenPGPUserId userId : getPrimaryKey().getUserIDs()) + { + ellipsizedComment(armorBuilder, userId.getUserId()); + } + + ArmoredOutputStream aOut = armorBuilder.build(bOut); + BCPGOutputStream pOut = new BCPGOutputStream(aOut, PacketFormat.CURRENT); + + // Make sure we export a TPK + List list = new ArrayList<>(); + for (Iterator it = getPGPKeyRing().getPublicKeys(); it.hasNext(); ) + { + list.add(it.next()); + } + PGPPublicKeyRing publicKeys = new PGPPublicKeyRing(list); + + publicKeys.encode(pOut, true); + pOut.close(); + aOut.close(); + return bOut.toString(); + } + + private void splitMultilineComment(ArmoredOutputStream.Builder armorBuilder, String comment) + { + int availableCommentCharsPerLine = 64 - "Comment: ".length(); // ASCII armor width - header len + + comment = comment.trim(); + + while (comment.length() > availableCommentCharsPerLine) + { + // split comment into multiple lines + armorBuilder.addComment(comment.substring(0, availableCommentCharsPerLine)); + comment = comment.substring(availableCommentCharsPerLine).trim(); + } + + if (!comment.isEmpty()) + { + armorBuilder.addComment(comment); + } + } + + private void ellipsizedComment(ArmoredOutputStream.Builder armorBuilder, String comment) + { + int availableCommentCharsPerLine = 64 - "Comment: ".length(); // ASCII armor width - header len + comment = comment.trim(); + + if (comment.length() > availableCommentCharsPerLine) + { + comment = comment.substring(0, availableCommentCharsPerLine - 1) + '…'; + } + armorBuilder.addComment(comment); + } + + protected List fingerprintComments() + { + // TODO: Implement slicing in ArmoredOutputStream.Builder instead? + String prettyPrinted = FingerprintUtil.prettifyFingerprint(getFingerprint()); + + int availableCommentCharsPerLine = 64 - "Comment: ".length(); // ASCII armor width - header len + List slices = new ArrayList<>(); + + while (prettyPrinted.length() > availableCommentCharsPerLine) + { + slices.add(prettyPrinted.substring(0, availableCommentCharsPerLine)); + prettyPrinted = prettyPrinted.substring(availableCommentCharsPerLine).trim(); + } + slices.add(prettyPrinted); + return slices; + } + + private OpenPGPSignatureChain getSignatureChainFor(OpenPGPCertificateComponent component, + OpenPGPComponentKey origin, + Date evaluationDate) + { + // Check if there are signatures at all for the component + OpenPGPSignatureChains chainsForComponent = getAllSignatureChainsFor(component); + if (component == getPrimaryKey() && chainsForComponent.isEmpty()) + { + // If cert has no direct-key signatures, consider UID bindings instead + // TODO: Only consider current primary user id? + for (OpenPGPIdentityComponent identity : getPrimaryKey().identityComponents) + { + chainsForComponent.addAll(getAllSignatureChainsFor(identity)); + } + } + + // Isolate chains which originate from the passed origin key component + OpenPGPSignatureChains fromOrigin = chainsForComponent.fromOrigin(origin); + if (fromOrigin == null) + { + return null; + } + + // Return chain that currently takes precedence + return fromOrigin.getChainAt(evaluationDate); + } + + private OpenPGPSignatureChains getAllSignatureChainsFor(OpenPGPCertificateComponent component) + { + return componentSignatureChains.get(component.getPublicComponent()); + } + + private void processPrimaryKey(OpenPGPPrimaryKey primaryKey) + { + OpenPGPSignatureChains keySignatureChains = new OpenPGPSignatureChains(primaryKey); + List keySignatures = primaryKey.getKeySignatures(); + + // Key Signatures + for (OpenPGPComponentSignature sig : keySignatures) + { + OpenPGPSignatureChain chain = OpenPGPSignatureChain.direct(sig, sig.issuer, primaryKey); + keySignatureChains.add(chain); + } + componentSignatureChains.put(primaryKey, keySignatureChains); + + // Identities + for (OpenPGPIdentityComponent identity : primaryKey.identityComponents) + { + OpenPGPSignatureChains identityChains = new OpenPGPSignatureChains(identity); + List bindings; + + if (identity instanceof OpenPGPUserId) + { + bindings = primaryKey.getUserIdSignatures((OpenPGPUserId) identity); + } + else + { + bindings = primaryKey.getUserAttributeSignatures((OpenPGPUserAttribute) identity); + } + + for (OpenPGPComponentSignature sig : bindings) + { + OpenPGPSignatureChain chain = OpenPGPSignatureChain.direct(sig, sig.getIssuerComponent(), identity); + identityChains.add(chain); + } + componentSignatureChains.put(identity, identityChains); + } + } + + private void processSubkey(OpenPGPSubkey subkey) + { + List bindingSignatures = subkey.getKeySignatures(); + OpenPGPSignatureChains subkeyChains = new OpenPGPSignatureChains(subkey); + + for (OpenPGPComponentSignature sig : bindingSignatures) + { + OpenPGPComponentKey issuer = subkey.getCertificate().getSigningKeyFor(sig.getSignature()); + if (issuer == null) + { + continue; // external key + } + + OpenPGPSignatureChains issuerChains = getAllSignatureChainsFor(issuer); + if (!issuerChains.chains.isEmpty()) + { + for (OpenPGPSignatureChain issuerChain : issuerChains.chains) + { + subkeyChains.add(issuerChain.plus(sig, subkey)); + } + } + else + { + subkeyChains.add(new OpenPGPSignatureChain( + new OpenPGPSignatureChain.Certification(sig, issuer, subkey))); + } + } + this.componentSignatureChains.put(subkey, subkeyChains); + } + + /** + * Return true, if the passed in component is - at evaluation time - properly bound to the certificate. + * + * @param component OpenPGP certificate component + * @param evaluationTime evaluation time + * @return true if component is bound at evaluation time, false otherwise + */ + private boolean isBound(OpenPGPCertificateComponent component, + Date evaluationTime) + { + return isBoundBy(component, getPrimaryKey(), evaluationTime); + } + + /** + * Return true, if the passed in component is - at evaluation time - properly bound to the certificate with + * a signature chain originating at the passed in root component. + * + * @param component OpenPGP certificate component + * @param root root certificate component + * @param evaluationTime evaluation time + * @return true if component is bound at evaluation time, originating at root, false otherwise + */ + private boolean isBoundBy(OpenPGPCertificateComponent component, + OpenPGPComponentKey root, + Date evaluationTime) + { + try + { + OpenPGPSignatureChain chain = getSignatureChainFor(component, root, evaluationTime); + if (chain == null) + { + // Component is not bound at all + return false; + } + + // Chain needs to be valid (signatures correct) + if (chain.isValid(implementation.pgpContentVerifierBuilderProvider())) + { + // Chain needs to not contain a revocation signature, otherwise the component is considered revoked + return !chain.isRevocation(); + } + + // Signature is not correct + return false; + } + catch (PGPException e) + { + // Signature verification failed (signature broken?) + return false; + } + } + + /** + * Return a {@link List} containing all currently marked, valid encryption keys. + * + * @return encryption keys + */ + public List getEncryptionKeys() + { + return getEncryptionKeys(new Date()); + } + + /** + * Return a list of all keys that are - at evaluation time - valid encryption keys. + * + * @param evaluationTime evaluation time + * @return encryption keys + */ + public List getEncryptionKeys(Date evaluationTime) + { + List encryptionKeys = new ArrayList<>(); + + for (OpenPGPComponentKey key : getKeys()) + { + if (!isBound(key, evaluationTime)) + { + // Key is not bound + continue; + } + + if (!key.isEncryptionKey(evaluationTime)) + { + continue; + } + + encryptionKeys.add(key); + } + + return encryptionKeys; + } + + /** + * Return a {@link List} containing all currently valid marked signing keys. + * + * @return list of signing keys + */ + public List getSigningKeys() + { + return getSigningKeys(new Date()); + } + + /** + * Return a list of all keys that - at evaluation time - are validly marked as signing keys. + * + * @param evaluationTime evaluation time + * @return list of signing keys + */ + public List getSigningKeys(Date evaluationTime) + { + List signingKeys = new ArrayList<>(); + + for (OpenPGPComponentKey key : getKeys()) + { + if (!isBound(key, evaluationTime)) + { + // Key is not bound + continue; + } + + if (!key.isSigningKey(evaluationTime)) + { + continue; + } + + signingKeys.add(key); + } + + return signingKeys; + } + + /** + * Return {@link OpenPGPSignatureChains} that contain preference information. + * + * @return signature chain containing certificate-wide preferences (typically DK signature) + */ + private OpenPGPSignatureChain getPreferenceSignature(Date evaluationTime) + { + OpenPGPSignatureChain directKeyBinding = getPrimaryKey().getSignatureChains() + .fromOrigin(getPrimaryKey()) + .getCertificationAt(evaluationTime); + + if (directKeyBinding != null) + { + return directKeyBinding; + } + + List uidBindings = new ArrayList<>(); + for (OpenPGPUserId userId : getPrimaryKey().getUserIDs()) + { + OpenPGPSignatureChain uidBinding = getAllSignatureChainsFor(userId) + .fromOrigin(getPrimaryKey()) + .getCertificationAt(evaluationTime); + + if (uidBinding != null) + { + uidBindings.add(uidBinding); + } + } + + uidBindings.sort(Comparator.comparing(OpenPGPSignatureChain::getSince).reversed()); + for (OpenPGPSignatureChain binding : uidBindings) + { + PGPSignature sig = binding.getHeadLink().getSignature().getSignature(); + if (sig.getHashedSubPackets().isPrimaryUserID()) + { + return binding; + } + } + + return uidBindings.isEmpty() ? null : uidBindings.get(0); + } + + public List getIdentities() + { + return new ArrayList<>(primaryKey.identityComponents); + } + + /** + * Component on an OpenPGP certificate. + * Components can either be {@link OpenPGPComponentKey keys} or {@link OpenPGPIdentityComponent identities}. + */ + public static abstract class OpenPGPCertificateComponent + { + private final OpenPGPCertificate certificate; + + public OpenPGPCertificateComponent(OpenPGPCertificate certificate) + { + this.certificate = certificate; + } + + /** + * Return this components {@link OpenPGPCertificate}. + * + * @return certificate + */ + public OpenPGPCertificate getCertificate() + { + return certificate; + } + + /** + * Return a detailed String representation of this component. + * + * @return detailed String representation + */ + public abstract String toDetailString(); + + /** + * Return true, if this component is - at evaluation time - properly bound to its certificate. + * + * @param evaluationTime evaluation time + * @return true if bound, false otherwise + */ + public boolean isBoundAt(Date evaluationTime) + { + return getCertificate().isBound(this, evaluationTime); + } + + /** + * Return all {@link OpenPGPSignatureChains} that bind this component. + * + * @return signature chains + */ + public OpenPGPSignatureChains getSignatureChains() + { + return getCertificate().getAllSignatureChainsFor(this); + } + + /** + * Return the public {@link OpenPGPCertificateComponent} that belongs to this component. + * For public components (pubkeys, identities...), that's simply this, while secret components + * return their corresponding public component. + * This is used to properly map secret key and public key components in {@link Map Maps} that use + * {@link OpenPGPCertificateComponent components} as map keys. + * + * @return public certificate component + */ + protected OpenPGPCertificateComponent getPublicComponent() + { + return this; + } + } + + /** + * OpenPGP Signature made over some {@link OpenPGPCertificateComponent} on a {@link OpenPGPCertificate}. + */ + public static class OpenPGPComponentSignature + extends OpenPGPSignature + { + + private final OpenPGPCertificateComponent target; + + /** + * Component signature. + * @param signature signature + * @param issuer key that issued the signature. + * Is nullable (e.g. for 3rd party sigs where the certificate is not available). + * @param target signed certificate component + */ + public OpenPGPComponentSignature(PGPSignature signature, + OpenPGPComponentKey issuer, + OpenPGPCertificateComponent target) + { + super(signature, issuer); + this.target = target; + } + + /** + * Return the {@link OpenPGPComponentKey} that issued this signature. + * + * @return issuer + */ + public OpenPGPComponentKey getIssuerComponent() + { + return getIssuer(); + } + + /** + * Return the {@link OpenPGPCertificateComponent} that this signature was calculated over. + * + * @return target + */ + public OpenPGPCertificateComponent getTargetComponent() + { + return target; + } + + /** + * Return the {@link OpenPGPComponentKey} that this signature is calculated over. + * Contrary to {@link #getTargetComponent()}, which returns the actual target, this method returns the + * {@link OpenPGPComponentKey} "closest" to the target. + * For a subkey-binding signature, this is the target subkey, while for an identity-binding signature + * (binding for a user-id or attribute) the return value is the {@link OpenPGPComponentKey} which + * carries the identity. + * + * @return target key component of the signature + */ + public OpenPGPComponentKey getTargetKeyComponent() + { + if (getTargetComponent() instanceof OpenPGPIdentityComponent) + { + // Identity signatures indirectly authenticate the primary key + return ((OpenPGPIdentityComponent) getTargetComponent()).getPrimaryKey(); + } + if (getTargetComponent() instanceof OpenPGPComponentKey) + { + // Key signatures authenticate the target key + return (OpenPGPComponentKey) getTargetComponent(); + } + throw new IllegalArgumentException("Unknown target type."); + } + + /** + * Verify this signature. + * + * @param contentVerifierBuilderProvider provider for verifiers + * @throws PGPSignatureException if the signature cannot be verified successfully + */ + public void verify(PGPContentVerifierBuilderProvider contentVerifierBuilderProvider) + throws PGPSignatureException + { + if (issuer == null) + { + // No issuer available + throw new MissingIssuerCertException("Issuer certificate unavailable."); + } + + sanitize(issuer); + + // Direct-Key signature + if (target == issuer) + { + verifyKeySignature( + issuer, + issuer, + contentVerifierBuilderProvider); + } + + // Subkey binding signature + else if (target instanceof OpenPGPSubkey) + { + verifyKeySignature( + issuer, + (OpenPGPSubkey) target, + contentVerifierBuilderProvider); + } + + // User-ID binding + else if (target instanceof OpenPGPUserId) + { + verifyUserIdSignature( + issuer, + (OpenPGPUserId) target, + contentVerifierBuilderProvider); + } + + // User-Attribute binding + else if (target instanceof OpenPGPUserAttribute) + { + verifyUserAttributeSignature( + issuer, + (OpenPGPUserAttribute) target, + contentVerifierBuilderProvider); + } + + else + { + throw new PGPSignatureException("Unexpected signature type: " + getType()); + } + } + + public void verifyKeySignature(OpenPGPComponentKey issuer, + OpenPGPComponentKey target, + PGPContentVerifierBuilderProvider contentVerifierBuilderProvider) + throws PGPSignatureException + { + this.isTested = true; + try + { + signature.init(contentVerifierBuilderProvider, issuer.getPGPPublicKey()); + if (issuer == target) + { + // Direct-Key Signature + isCorrect = signature.verifyCertification(target.getPGPPublicKey()); + } + else + { + // Subkey Binding Signature + isCorrect = signature.verifyCertification(issuer.getPGPPublicKey(), target.getPGPPublicKey()); + } + + if (!isCorrect) + { + throw new IncorrectPGPSignatureException("Key Signature is not correct."); + } + } + catch (PGPException e) + { + this.isCorrect = false; + throw new PGPSignatureException("Key Signature could not be verified.", e); + } + } + + public void verifyUserIdSignature(OpenPGPComponentKey issuer, + OpenPGPUserId target, + PGPContentVerifierBuilderProvider contentVerifierBuilderProvider) + throws PGPSignatureException + { + this.isTested = true; + try + { + signature.init(contentVerifierBuilderProvider, issuer.getPGPPublicKey()); + isCorrect = signature.verifyCertification(target.getUserId(), target.getPrimaryKey().getPGPPublicKey()); + if (!isCorrect) + { + throw new IncorrectPGPSignatureException("UserID Signature is not correct."); + } + } + catch (PGPException e) + { + this.isCorrect = false; + throw new PGPSignatureException("UserID Signature could not be verified.", e); + } + } + + public void verifyUserAttributeSignature(OpenPGPComponentKey issuer, + OpenPGPUserAttribute target, + PGPContentVerifierBuilderProvider contentVerifierBuilderProvider) + throws PGPSignatureException + { + this.isTested = true; + try + { + signature.init(contentVerifierBuilderProvider, issuer.getPGPPublicKey()); + isCorrect = signature.verifyCertification(target.getUserAttribute(), target.getPrimaryKey().getPGPPublicKey()); + if (!isCorrect) + { + throw new IncorrectPGPSignatureException("UserAttribute Signature is not correct."); + } + } + catch (PGPException e) + { + this.isCorrect = false; + throw new PGPSignatureException("Could not verify UserAttribute Signature.", e); + } + } + + @Override + protected String getTargetDisplay() + { + return target.toString(); + } + } + + /** + * A component key is either an {@link OpenPGPPrimaryKey}, or an {@link OpenPGPSubkey}. + * + * @see + * OpenPGP for Application Developers - Layers of keys in OpenPGP + */ + public static abstract class OpenPGPComponentKey + extends OpenPGPCertificateComponent + { + protected final PGPPublicKey rawPubkey; + + /** + * Constructor. + * @param rawPubkey public key + * @param certificate certificate + */ + public OpenPGPComponentKey(PGPPublicKey rawPubkey, OpenPGPCertificate certificate) + { + super(certificate); + this.rawPubkey = rawPubkey; + } + + public PGPPublicKey getPGPPublicKey() + { + return rawPubkey; + } + + /** + * Return the {@link KeyIdentifier} of this key. + * + * @return key identifier + */ + public KeyIdentifier getKeyIdentifier() + { + return new KeyIdentifier(rawPubkey); + } + + /** + * Return the creation time of this key. + * + * @return creation time + */ + public Date getCreationTime() + { + return rawPubkey.getCreationTime(); + } + + /** + * Return true, if the key is currently marked as encryption key. + * + * @return true if the key is an encryption key, false otherwise + */ + public boolean isEncryptionKey() + { + return isEncryptionKey(new Date()); + } + + /** + * Return true, if the is - at evaluation time - marked as an encryption key. + * + * @param evaluationTime evaluation time + * @return true if key is an encryption key at evaluation time, false otherwise + */ + public boolean isEncryptionKey(Date evaluationTime) + { + if (!rawPubkey.isEncryptionKey()) + { + // Skip keys that are not encryption-capable by algorithm + return false; + } + + KeyFlags keyFlags = getKeyFlags(evaluationTime); + if (keyFlags == null) + { + return false; + } + + int flags = keyFlags.getFlags(); + return (flags & KeyFlags.ENCRYPT_COMMS) == KeyFlags.ENCRYPT_COMMS || + (flags & KeyFlags.ENCRYPT_STORAGE) == KeyFlags.ENCRYPT_STORAGE; + } + + /** + * Return true, if the key is currently marked as a signing key for message signing. + * + * @return true, if key is currently signing key + */ + public boolean isSigningKey() + { + return isSigningKey(new Date()); + } + + /** + * Return true, if the key is - at evaluation time - marked as signing key for message signing. + * + * @param evaluationTime evaluation time + * @return true if key is signing key at evaluation time + */ + public boolean isSigningKey(Date evaluationTime) + { + // TODO: Replace with https://github.com/bcgit/bc-java/pull/1857/files#diff-36f593d586240aec2546daad96d16b5debd3463202a3d5d82c0b2694572c8426R14-R30 + int alg = rawPubkey.getAlgorithm(); + if (alg != PublicKeyAlgorithmTags.RSA_GENERAL && + alg != PublicKeyAlgorithmTags.RSA_SIGN && + alg != PublicKeyAlgorithmTags.DSA && + alg != PublicKeyAlgorithmTags.ECDSA && + alg != PublicKeyAlgorithmTags.EDDSA_LEGACY && + alg != PublicKeyAlgorithmTags.Ed25519 && + alg != PublicKeyAlgorithmTags.Ed448) + { + // Key is not signing-capable by algorithm + return false; + } + + KeyFlags keyFlags = getKeyFlags(evaluationTime); + if (keyFlags == null) + { + return false; + } + + int flags = keyFlags.getFlags(); + return (flags & KeyFlags.SIGN_DATA) == KeyFlags.SIGN_DATA; + } + + /** + * Return true, if the key is currently marked as certification key that can sign 3rd-party certificates. + * + * @return true, if key is certification key + */ + public boolean isCertificationKey() + { + return isCertificationKey(new Date()); + } + + /** + * Return true, if the key is - at evaluation time - marked as certification key that can sign 3rd-party + * certificates. + * + * @param evaluationTime evaluation time + * @return true if key is certification key at evaluation time + */ + public boolean isCertificationKey(Date evaluationTime) + { + // TODO: Replace with https://github.com/bcgit/bc-java/pull/1857/files#diff-36f593d586240aec2546daad96d16b5debd3463202a3d5d82c0b2694572c8426R14-R30 + int alg = rawPubkey.getAlgorithm(); + if (alg != PublicKeyAlgorithmTags.RSA_GENERAL && + alg != PublicKeyAlgorithmTags.RSA_SIGN && + alg != PublicKeyAlgorithmTags.DSA && + alg != PublicKeyAlgorithmTags.ECDSA && + alg != PublicKeyAlgorithmTags.EDDSA_LEGACY && + alg != PublicKeyAlgorithmTags.Ed25519 && + alg != PublicKeyAlgorithmTags.Ed448) + { + // Key is not signing-capable by algorithm + return false; + } + + KeyFlags keyFlags = getKeyFlags(evaluationTime); + if (keyFlags == null) + { + return false; + } + + int flags = keyFlags.getFlags(); + return (flags & KeyFlags.CERTIFY_OTHER) == KeyFlags.CERTIFY_OTHER; + } + + /** + * Return the {@link KeyFlags} signature subpacket that currently applies to the key. + * @return key flags subpacket + */ + public KeyFlags getKeyFlags() + { + return getKeyFlags(new Date()); + } + + /** + * Return the {@link KeyFlags} signature subpacket that - at evaluation time - applies to the key. + * @param evaluationTime evaluation time + * @return key flags subpacket + */ + public KeyFlags getKeyFlags(Date evaluationTime) + { + SignatureSubpacket subpacket = getApplyingSubpacket( + evaluationTime, SignatureSubpacketTags.KEY_FLAGS); + if (subpacket != null) + { + return (KeyFlags) subpacket; + } + return null; + } + + /** + * Return the {@link Features} signature subpacket that currently applies to the key. + * @return feature signature subpacket + */ + public Features getFeatures() + { + return getFeatures(new Date()); + } + + /** + * Return the {@link Features} signature subpacket that - at evaluation time - applies to the key. + * @param evaluationTime evaluation time + * @return features subpacket + */ + public Features getFeatures(Date evaluationTime) + { + SignatureSubpacket subpacket = getApplyingSubpacket(evaluationTime, SignatureSubpacketTags.FEATURES); + if (subpacket != null) + { + return (Features) subpacket; + } + return null; + } + + /** + * Return the {@link SignatureSubpacket} instance of the given subpacketType, which currently applies to + * the key. Since subpackets from the Direct-Key signature apply to all subkeys of a certificate, + * this method first inspects the signature that immediately applies to this key (e.g. a subkey-binding + * signature), and - if the queried subpacket is found in there, returns that instance. + * Otherwise, indirectly applying signatures (e.g. Direct-Key signatures) are queried. + * That way, preferences from the direct-key signature are considered, but per-key overwrites take precedence. + * + * @see + * OpenPGP for application developers - Attribute Shadowing + * + * @param evaluationTime evaluation time + * @param subpacketType subpacket type that is being searched for + * @return subpacket from directly or indirectly applying signature + */ + protected SignatureSubpacket getApplyingSubpacket(Date evaluationTime, int subpacketType) + { + OpenPGPSignatureChain binding = getSignatureChains().getCertificationAt(evaluationTime); + if (binding == null) + { + // is not bound + return null; + } + + // Check signatures + try + { + if (!binding.isValid()) + { + // Binding is incorrect + return null; + } + } + catch (PGPSignatureException e) + { + // Binding cannot be verified + return null; + } + + // find signature "closest to the key", e.g. subkey binding signature + OpenPGPComponentSignature keySignature = binding.getHeadLink().getSignature(); + + PGPSignatureSubpacketVector hashedSubpackets = keySignature.getSignature().getHashedSubPackets(); + if (hashedSubpackets == null || !hashedSubpackets.hasSubpacket(subpacketType)) + { + // If the subkey binding signature doesn't carry the desired subpacket, + // check direct-key or primary uid sig instead + OpenPGPSignatureChain preferenceBinding = getCertificate().getPreferenceSignature(evaluationTime); + if (preferenceBinding == null) + { + // No direct-key / primary uid sig found -> No subpacket + return null; + } + hashedSubpackets = preferenceBinding.getHeadLink().getSignature().getSignature().getHashedSubPackets(); + } + // else -> attribute from DK sig is shadowed by SB sig + + // Extract subpacket from hashed area + return hashedSubpackets.getSubpacket(subpacketType); + } + + public PreferredAEADCiphersuites getAEADCipherSuitePreferences() + { + return getAEADCipherSuitePreferences(new Date()); + } + + public PreferredAEADCiphersuites getAEADCipherSuitePreferences(Date evaluationTime) + { + SignatureSubpacket subpacket = getApplyingSubpacket(evaluationTime, + SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS); + if (subpacket != null) + { + return (PreferredAEADCiphersuites) subpacket; + } + return null; + } + + public PreferredAlgorithms getSymmetricCipherPreferences() + { + return getSymmetricCipherPreferences(new Date()); + } + + public PreferredAlgorithms getSymmetricCipherPreferences(Date evaluationTime) + { + SignatureSubpacket subpacket = getApplyingSubpacket(evaluationTime, SignatureSubpacketTags.PREFERRED_SYM_ALGS); + if (subpacket != null) + { + return (PreferredAlgorithms) subpacket; + } + return null; + } + + public PreferredAlgorithms getHashAlgorithmPreferences() + { + return getHashAlgorithmPreferences(new Date()); + } + + public PreferredAlgorithms getHashAlgorithmPreferences(Date evaluationTime) + { + SignatureSubpacket subpacket = getApplyingSubpacket(evaluationTime, SignatureSubpacketTags.PREFERRED_HASH_ALGS); + if (subpacket != null) + { + return (PreferredAlgorithms) subpacket; + } + return null; + } + } + + /** + * The primary key of a {@link OpenPGPCertificate}. + */ + public static class OpenPGPPrimaryKey + extends OpenPGPComponentKey + { + @Override + public String toString() + { + return "PrimaryKey[" + Long.toHexString(getKeyIdentifier().getKeyId()).toUpperCase() + "]"; + } + + @Override + public String toDetailString() + { + return "PrimaryKey[" + getKeyIdentifier() + "] (" + UTCUtil.format(getCreationTime()) + ")"; + } + + protected final List identityComponents; + + public OpenPGPPrimaryKey(PGPPublicKey rawPubkey, OpenPGPCertificate certificate) + { + super(rawPubkey, certificate); + this.identityComponents = new ArrayList<>(); + + Iterator userIds = rawPubkey.getUserIDs(); + while (userIds.hasNext()) + { + identityComponents.add(new OpenPGPUserId(userIds.next(), this)); + } + + Iterator userAttributes = rawPubkey.getUserAttributes(); + while (userAttributes.hasNext()) + { + identityComponents.add(new OpenPGPUserAttribute(userAttributes.next(), this)); + } + } + + /** + * Return all {@link OpenPGPUserId OpenPGPUserIds} on this key. + * + * @return user ids + */ + public List getUserIDs() + { + List userIds = new ArrayList<>(); + for (OpenPGPIdentityComponent identity : identityComponents) + { + if (identity instanceof OpenPGPUserId) + { + userIds.add((OpenPGPUserId) identity); + } + } + return userIds; + } + + /** + * Return all {@link OpenPGPUserAttribute OpenPGPUserAttributes} on this key. + * + * @return user attributes + */ + public List getUserAttributes() + { + List userAttributes = new ArrayList<>(); + for (OpenPGPIdentityComponent identity : identityComponents) + { + if (identity instanceof OpenPGPUserAttribute) + { + userAttributes.add((OpenPGPUserAttribute) identity); + } + } + return userAttributes; + } + + protected List getKeySignatures() + { + Iterator iterator = rawPubkey.getSignatures(); + List list = new ArrayList<>(); + while (iterator.hasNext()) + { + PGPSignature sig = iterator.next(); + int type = sig.getSignatureType(); + if (type != PGPSignature.DIRECT_KEY && type != PGPSignature.KEY_REVOCATION) + { + continue; + } + // try to find issuer for self-signature + OpenPGPCertificate.OpenPGPComponentKey issuer = getCertificate() + .getSigningKeyFor(sig); + + list.add(new OpenPGPCertificate.OpenPGPComponentSignature(sig, issuer, this)); + } + return list; + } + + protected List getUserIdSignatures(OpenPGPUserId identity) + { + Iterator iterator = rawPubkey.getSignaturesForID(identity.getUserId()); + List list = new ArrayList<>(); + while (iterator.hasNext()) + { + PGPSignature sig = iterator.next(); + // try to find issuer for self-signature + OpenPGPCertificate.OpenPGPComponentKey issuer = getCertificate() + .getSigningKeyFor(sig); + + list.add(new OpenPGPCertificate.OpenPGPComponentSignature(sig, issuer, identity)); + } + return list; + } + + protected List getUserAttributeSignatures(OpenPGPUserAttribute identity) + { + Iterator iterator = rawPubkey.getSignaturesForUserAttribute(identity.getUserAttribute()); + List list = new ArrayList<>(); + while (iterator.hasNext()) + { + PGPSignature sig = iterator.next(); + // try to find issuer for self-signature + OpenPGPCertificate.OpenPGPComponentKey issuer = getCertificate() + .getSigningKeyFor(sig); + + list.add(new OpenPGPCertificate.OpenPGPComponentSignature(sig, issuer, identity)); + } + return list; + } + } + + /** + * A subkey on a {@link OpenPGPCertificate}. + */ + public static class OpenPGPSubkey + extends OpenPGPComponentKey + { + public OpenPGPSubkey(PGPPublicKey rawPubkey, OpenPGPCertificate certificate) + { + super(rawPubkey, certificate); + } + + @Override + public String toString() + { + return "Subkey[" + Long.toHexString(getKeyIdentifier().getKeyId()).toUpperCase() + "]"; + } + + @Override + public String toDetailString() + { + return "Subkey[" + getKeyIdentifier() + "] (" + UTCUtil.format(getCreationTime()) + ")"; + } + + protected List getKeySignatures() + { + Iterator iterator = rawPubkey.getSignatures(); + List list = new ArrayList<>(); + while (iterator.hasNext()) + { + PGPSignature sig = iterator.next(); + int type = sig.getSignatureType(); + if (type != PGPSignature.SUBKEY_BINDING && type != PGPSignature.SUBKEY_REVOCATION) + { + continue; + } + // try to find issuer for self-signature + OpenPGPCertificate.OpenPGPComponentKey issuer = getCertificate() + .getSigningKeyFor(sig); + + list.add(new OpenPGPCertificate.OpenPGPComponentSignature(sig, issuer, this)); + } + return list; + } + } + + /** + * An identity bound to the {@link OpenPGPPrimaryKey} of a {@link OpenPGPCertificate}. + * An identity may either be a {@link OpenPGPUserId} or (deprecated) {@link OpenPGPUserAttribute}. + */ + public static abstract class OpenPGPIdentityComponent + extends OpenPGPCertificateComponent + { + private final OpenPGPPrimaryKey primaryKey; + + public OpenPGPIdentityComponent(OpenPGPPrimaryKey primaryKey) + { + super(primaryKey.getCertificate()); + this.primaryKey = primaryKey; + } + + public OpenPGPPrimaryKey getPrimaryKey() + { + return primaryKey; + } + + @Override + public String toDetailString() + { + return toString(); + } + } + + /** + * A UserId. + */ + public static class OpenPGPUserId + extends OpenPGPIdentityComponent + { + private final String userId; + + public OpenPGPUserId(String userId, OpenPGPPrimaryKey primaryKey) + { + super(primaryKey); + this.userId = userId; + } + + public String getUserId() + { + return userId; + } + + @Override + public String toString() + { + return "UserID[" + userId + "]"; + } + } + + /** + * A UserAttribute. + * Use of UserAttributes is deprecated in RFC9580. + */ + public static class OpenPGPUserAttribute + extends OpenPGPIdentityComponent + { + + private final PGPUserAttributeSubpacketVector userAttribute; + + public OpenPGPUserAttribute(PGPUserAttributeSubpacketVector userAttribute, OpenPGPPrimaryKey primaryKey) + { + super(primaryKey); + this.userAttribute = userAttribute; + } + + public PGPUserAttributeSubpacketVector getUserAttribute() + { + return userAttribute; + } + + @Override + public String toString() + { + return "UserAttribute" + userAttribute.toString(); + } + } + + /** + * Chain of {@link OpenPGPSignature signatures}. + * Such a chain originates from a certificates primary key and points towards some certificate component that + * is bound to the certificate. + * As for example a subkey can only be bound by a primary key that holds either at least one + * direct-key self-signature or at least one user-id binding signature, multiple signatures may form + * a validity chain. + * An {@link OpenPGPSignatureChain} can either be a certification + * ({@link #isCertification()}), e.g. it represents a positive binding, + * or it can be a revocation ({@link #isRevocation()}) which invalidates a positive binding. + */ + public static class OpenPGPSignatureChain + implements Comparable, Iterable + { + private final List chainLinks = new ArrayList<>(); + + private OpenPGPSignatureChain(Link rootLink) + { + this.chainLinks.add(rootLink); + } + + // copy constructor + private OpenPGPSignatureChain(OpenPGPSignatureChain copy) + { + this.chainLinks.addAll(copy.chainLinks); + } + + /** + * Return an NEW instance of the {@link OpenPGPSignatureChain} with the new link appended. + * @param sig signature + * @param targetComponent signature target + * @return new instance + */ + public OpenPGPSignatureChain plus(OpenPGPComponentSignature sig, + OpenPGPCertificateComponent targetComponent) + { + if (getHeadKey() != sig.getIssuerComponent()) + { + throw new IllegalArgumentException("Chain head is not equal to link issuer."); + } + + OpenPGPSignatureChain chain = new OpenPGPSignatureChain(this); + + chain.chainLinks.add(Link.create(sig, sig.getIssuerComponent(), targetComponent)); + + return chain; + } + + public static OpenPGPSignatureChain direct(OpenPGPComponentSignature sig, + OpenPGPComponentKey issuer, + OpenPGPCertificateComponent targetComponent) + { + return new OpenPGPSignatureChain(Link.create(sig, issuer, targetComponent)); + } + + public Link getRootLink() + { + return chainLinks.get(0); + } + + public OpenPGPComponentKey getRootKey() + { + return getRootLink().issuer; + } + + public Link getHeadLink() + { + return chainLinks.get(chainLinks.size() - 1); + } + + public OpenPGPComponentKey getHeadKey() + { + return getHeadLink().signature.getTargetKeyComponent(); + } + + public boolean isCertification() + { + for (Link link : chainLinks) + { + if (link instanceof Revocation) + { + return false; + } + } + return true; + } + + public boolean isRevocation() + { + for (Link link : chainLinks) + { + if (link instanceof Revocation) + { + return true; + } + } + return false; + } + + public boolean isHardRevocation() + { + for (Link link : chainLinks) + { + if (link.signature.signature.isHardRevocation()) + { + return true; + } + } + return false; + } + + /** + * Return the date since which this signature chain is valid. + * This is the creation time of the most recent link in the chain. + * + * @return most recent signature creation time + */ + public Date getSince() + { + // Find most recent chain link + return chainLinks.stream() + .map(it -> it.signature) + .max(Comparator.comparing(OpenPGPComponentSignature::getCreationTime)) + .map(OpenPGPComponentSignature::getCreationTime) + .orElse(null); + } + + /** + * Return the date until which the chain link is valid. + * This is the earliest expiration time of any signature in the chain. + * + * @return earliest expiration time + */ + public Date getUntil() + { + Date soonestExpiration = null; + for (Link link : chainLinks) + { + Date until = link.until(); + if (until != null) + { + soonestExpiration = (soonestExpiration == null) ? until : + (until.before(soonestExpiration) ? until : soonestExpiration); + } + } + return soonestExpiration; + } + + public boolean isEffectiveAt(Date evaluationDate) + { + if (isHardRevocation()) + { + return true; + } + Date since = getSince(); + Date until = getUntil(); + return !evaluationDate.before(since) && (until == null || evaluationDate.before(until)); + } + + public boolean isValid() + throws PGPSignatureException + { + return isValid(getRootKey().getCertificate().implementation.pgpContentVerifierBuilderProvider()); + } + + public boolean isValid(PGPContentVerifierBuilderProvider contentVerifierBuilderProvider) + throws PGPSignatureException + { + boolean correct = true; + for (Link link : chainLinks) + { + if (!link.signature.isTested) + { + link.verify(contentVerifierBuilderProvider); + } + + if (!link.signature.isCorrect) + { + correct = false; + } + } + return correct; + } + + @Override + public String toString() + { + StringBuilder b = new StringBuilder(); + String until = getUntil() == null ? "EndOfTime" : UTCUtil.format(getUntil()); + b.append("From ").append(UTCUtil.format(getSince())).append(" until ").append(until).append("\n"); + for (Link link : chainLinks) + { + b.append(" ").append(link.toString()).append("\n"); + } + return b.toString(); + } + + @Override + public int compareTo(OpenPGPSignatureChain other) + { + if (isHardRevocation()) + { + return -1; + } + + if (other.isHardRevocation()) + { + return 1; + } + + return -getSince().compareTo(other.getSince()); + } + + @Override + public Iterator iterator() + { + return chainLinks.iterator(); + } + + /** + * Link in a {@link OpenPGPSignatureChain}. + */ + public static abstract class Link + { + protected final OpenPGPComponentSignature signature; + protected final OpenPGPComponentKey issuer; + protected final OpenPGPCertificateComponent target; + + public Link(OpenPGPComponentSignature signature, + OpenPGPComponentKey issuer, + OpenPGPCertificateComponent target) + { + this.signature = signature; + this.issuer = issuer; + this.target = target; + } + + public Date since() + { + return signature.getCreationTime(); + } + + public Date until() + { + return signature.getExpirationTime(); + } + + public boolean verify(PGPContentVerifierBuilderProvider contentVerifierBuilderProvider) + throws PGPSignatureException + { + signature.verify(contentVerifierBuilderProvider); + return true; + } + + @Override + public String toString() + { + return signature.toString(); + } + + public static Link create(OpenPGPComponentSignature signature, + OpenPGPComponentKey issuer, + OpenPGPCertificateComponent target) + { + if (signature.isRevocation()) + { + return new Revocation(signature, issuer, target); + } + else + { + return new Certification(signature, issuer, target); + } + } + + public OpenPGPComponentSignature getSignature() + { + return signature; + } + } + + /** + * "Positive" signature chain link. + */ + public static class Certification + extends Link + { + /** + * Positive certification. + * + * @param signature signature + * @param issuer key that issued the certification. + * Is nullable (e.g. for 3rd-party sigs where the cert is not available) + * @param target signed certificate component + */ + public Certification(OpenPGPComponentSignature signature, + OpenPGPComponentKey issuer, + OpenPGPCertificateComponent target) + { + super(signature, issuer, target); + } + } + + /** + * "Negative" signature chain link. + */ + public static class Revocation + extends Link + { + /** + * Revocation. + * + * @param signature signature + * @param issuer key that issued the revocation. + * Is nullable (e.g. for 3rd-party sigs where the cert is not available) + * @param target revoked certification component + */ + public Revocation(OpenPGPComponentSignature signature, + OpenPGPComponentKey issuer, + OpenPGPCertificateComponent target) + { + super(signature, issuer, target); + } + + @Override + public Date since() + { + if (signature.signature.isHardRevocation()) + { + return new Date(0L); + } + return super.since(); + } + + @Override + public Date until() + { + if (signature.signature.isHardRevocation()) + { + return new Date(Long.MAX_VALUE); + } + return super.until(); + } + } + } + + /** + * Collection of multiple {@link OpenPGPSignatureChain} objects. + */ + public static class OpenPGPSignatureChains implements Iterable + { + private final OpenPGPCertificateComponent targetComponent; + private final Set chains = new TreeSet<>(); + + public OpenPGPSignatureChains(OpenPGPCertificateComponent component) + { + this.targetComponent = component; + } + + /** + * Add a single chain to the collection. + * @param chain chain + */ + public void add(OpenPGPSignatureChain chain) + { + this.chains.add(chain); + } + + public void addAll(OpenPGPSignatureChains otherChains) + { + this.chains.addAll(otherChains.chains); + } + + public boolean isEmpty() + { + return chains.isEmpty(); + } + + /** + * Return a positive certification chain for the component for the given evaluationTime. + * @param evaluationTime time for which validity of the {@link OpenPGPCertificateComponent} is checked. + * @return positive certification chain or null + */ + public OpenPGPSignatureChain getCertificationAt(Date evaluationTime) + { + for (OpenPGPSignatureChain chain : chains) + { + boolean isEffective = chain.isEffectiveAt(evaluationTime); + boolean isCertification = chain.isCertification(); + if (isEffective && isCertification) + { + return chain; + } + } + return null; + } + + public OpenPGPSignatureChains getChainsAt(Date evaluationTime) + { + OpenPGPSignatureChains effectiveChains = new OpenPGPSignatureChains(targetComponent); + for (OpenPGPSignatureChain chain : chains) + { + if (chain.isEffectiveAt(evaluationTime)) + { + effectiveChains.add(chain); + } + } + return effectiveChains; + } + + /** + * Return a negative certification chain for the component for the given evaluationTime. + * @param evaluationTime time for which revocation-ness of the {@link OpenPGPCertificateComponent} is checked. + * @return negative certification chain or null + */ + public OpenPGPSignatureChain getRevocationAt(Date evaluationTime) + { + for (OpenPGPSignatureChain chain : chains) + { + if (!chain.isRevocation()) + { + continue; + } + + if (chain.isEffectiveAt(evaluationTime)) + { + return chain; + } + } + return null; + } + + @Override + public String toString() + { + StringBuilder b = new StringBuilder(targetComponent.toDetailString()) + .append(" is bound with ").append(chains.size()).append(" chains:").append("\n"); + for (OpenPGPSignatureChain chain : chains) + { + b.append(chain.toString()); + } + return b.toString(); + } + + public OpenPGPSignatureChains fromOrigin(OpenPGPComponentKey root) + { + OpenPGPSignatureChains chainsFromRoot = new OpenPGPSignatureChains(root); + for (OpenPGPSignatureChain chain : chains) + { + OpenPGPComponentKey chainRoot = chain.getRootKey(); + if (chainRoot == root) + { + chainsFromRoot.add(chain); + } + } + return chainsFromRoot; + } + + public OpenPGPSignatureChain getChainAt(Date evaluationDate) + { + OpenPGPSignatureChains atDate = getChainsAt(evaluationDate); + Iterator it = atDate.chains.iterator(); + if (it.hasNext()) + { + return it.next(); + } + return null; + } + + @Override + public Iterator iterator() + { + return chains.iterator(); + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPImplementation.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPImplementation.java new file mode 100644 index 0000000000..94e96021b4 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPImplementation.java @@ -0,0 +1,183 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptorBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; + +import java.io.InputStream; + +/** + * Bouncy Castle provides two implementations of OpenPGP operators. + * The

JCA/JCE
implementation makes use of Java Cryptography Architecture and the + * Java Cryptography Extension, while
Bc
uses Bouncy Castles Lightweight Cryptography API. + * The purpose of {@link OpenPGPImplementation} is to define a shared interface for instantiating concrete + * objects of either API. + * It is advised to define the desired implementation by calling {@link #setInstance(OpenPGPImplementation)} and + * acquiring it via {@link #getInstance()}, as swapping out the entire implementation can then be done by + * replacing the instance in one single place. + * This pattern was successfully explored by PGPainless. + */ +public abstract class OpenPGPImplementation +{ + private static OpenPGPImplementation INSTANCE; + + /** + * Replace the {@link OpenPGPImplementation} instance that is returned by {@link #getInstance()}. + * @param implementation instance + */ + public static void setInstance(OpenPGPImplementation implementation) + { + INSTANCE = implementation; + } + + /** + * Return the currently set {@link OpenPGPImplementation} instance. + * The default is {@link BcOpenPGPImplementation}. + * + * @return instance + */ + public static OpenPGPImplementation getInstance() + { + if (INSTANCE == null) + { + setInstance(new BcOpenPGPImplementation()); + } + return INSTANCE; + } + + /** + * Return an instance of {@link PGPObjectFactory} based on the given {@link InputStream}. + * + * @param packetInputStream packet input stream + * @return object factory + */ + public abstract PGPObjectFactory pgpObjectFactory(InputStream packetInputStream); + + /** + * Return an instance of {@link PGPContentVerifierBuilderProvider} which is responsible for providing + * implementations needed for signature verification. + * + * @return content verifier builder provider + */ + public abstract PGPContentVerifierBuilderProvider pgpContentVerifierBuilderProvider(); + + /** + * Return an instance of {@link PBESecretKeyDecryptorBuilderProvider} which is responsible for providing + * implementations needed for secret key unlocking. + * + * @return secret key decryptor builder provider + */ + public abstract PBESecretKeyDecryptorBuilderProvider pbeSecretKeyDecryptorBuilderProvider(); + + /** + * Return an instance of {@link PGPDataEncryptorBuilder} which is responsible for providing implementations + * needed for creating encrypted data packets. + * + * @param symmetricKeyAlgorithm symmetric encryption algorithm + * @return data encryptor builder + */ + public abstract PGPDataEncryptorBuilder pgpDataEncryptorBuilder( + int symmetricKeyAlgorithm); + + /** + * Return an instance of {@link PublicKeyKeyEncryptionMethodGenerator} which is responsible for + * creating public-key-based encryptors for OpenPGP messages. + * Public-key-based encryptors are used when a message is encrypted for a recipients public key. + * + * @param encryptionSubkey subkey for which a message shall be encrypted + * @return public-key key-encryption method generator + */ + public abstract PublicKeyKeyEncryptionMethodGenerator publicKeyKeyEncryptionMethodGenerator( + PGPPublicKey encryptionSubkey); + + /** + * Return an instance of {@link PBEKeyEncryptionMethodGenerator} which is responsible for creating + * symmetric-key-based encryptors for OpenPGP messages, using {@link S2K#SALTED_AND_ITERATED} mode. + * Symmetric-key-based encryptors are used when a message is encrypted using a passphrase. + * + * @param messagePassphrase passphrase to encrypt the message with + * @return pbe key encryption method generator + */ + public abstract PBEKeyEncryptionMethodGenerator pbeKeyEncryptionMethodGenerator( + char[] messagePassphrase); + + /** + * Return an instance of {@link PBEKeyEncryptionMethodGenerator} which is responsible for creating + * symmetric-key-based encryptors for OpenPGP messages, using {@link S2K#ARGON_2} mode. + * Symmetric-key-based encryptors are used when a message is encrypted using a passphrase. + * + * @param messagePassphrase passphrase to encrypt the message with + * @param argon2Params parameters for the Argon2 hash function + * @return pbe key encryption method generator + */ + public abstract PBEKeyEncryptionMethodGenerator pbeKeyEncryptionMethodGenerator( + char[] messagePassphrase, + S2K.Argon2Params argon2Params); + + /** + * Return an instance of {@link PGPContentSignerBuilder}, which is responsible for providing concrete + * implementations needed for signature creation. + * + * @param publicKeyAlgorithm the signing-keys public-key algorithm + * @param hashAlgorithm signature hash algorithm + * @return content signer builder + */ + public abstract PGPContentSignerBuilder pgpContentSignerBuilder( + int publicKeyAlgorithm, + int hashAlgorithm); + + /** + * Return an instance of the {@link PBEDataDecryptorFactory}, which is responsible for providing concrete + * implementations needed to decrypt OpenPGP messages that were encrypted symmetrically with a passphrase. + * + * @param messagePassphrase message passphrase + * @return pbe data decryptor factory + * @throws PGPException if the factory cannot be instantiated + */ + public abstract PBEDataDecryptorFactory pbeDataDecryptorFactory( + char[] messagePassphrase) + throws PGPException; + + /** + * Return an instance of the {@link SessionKeyDataDecryptorFactory}, which is responsible for providing + * concrete implementations needed to decrypt OpenPGP messages using a {@link PGPSessionKey}. + * + * @param sessionKey session key + * @return session-key data decryptor factory + */ + public abstract SessionKeyDataDecryptorFactory sessionKeyDataDecryptorFactory( + PGPSessionKey sessionKey); + + /** + * Return an instance of the {@link PublicKeyDataDecryptorFactory}, which is responsible for providing + * concrete implementations needed to decrypt OpenPGP messages using a {@link PGPPrivateKey}. + * + * @param decryptionKey private decryption key + * @return public-key data decryptor factory + */ + public abstract PublicKeyDataDecryptorFactory publicKeyDataDecryptorFactory( + PGPPrivateKey decryptionKey); + + /** + * Return an instance of the {@link PGPDigestCalculatorProvider}, which is responsible for providing + * concrete {@link org.bouncycastle.openpgp.operator.PGPDigestCalculator} implementations. + * + * @return pgp digest calculator provider + * @throws PGPException if the provider cannot be instantiated + */ + public abstract PGPDigestCalculatorProvider pgpDigestCalculatorProvider() + throws PGPException; +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKey.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKey.java new file mode 100644 index 0000000000..af61448fb0 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKey.java @@ -0,0 +1,303 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.PacketFormat; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.openpgp.KeyIdentifier; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptorBuilderProvider; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An {@link OpenPGPKey} (TSK - transferable secret key) is the pendant to an {@link OpenPGPCertificate}, + * but containing the secret key material in addition to the public components. + * It consists of one or multiple {@link OpenPGPSecretKey} objects. + */ +public class OpenPGPKey + extends OpenPGPCertificate +{ + // This class extends OpenPGPCertificate, but also holds secret key components in a dedicated map. + private final Map secretKeys; + + /** + * Create an {@link OpenPGPKey} instance based on a {@link PGPSecretKeyRing}. + * The {@link OpenPGPImplementation} will be acquired by invoking {@link OpenPGPImplementation#getInstance()}. + * + * @param keyRing secret key ring + */ + public OpenPGPKey(PGPSecretKeyRing keyRing) + { + this(keyRing, OpenPGPImplementation.getInstance()); + } + + /** + * Create an {@link OpenPGPKey} instance based on a {@link PGPSecretKeyRing}. + * + * @param keyRing secret key ring + * @param implementation OpenPGP implementation + */ + public OpenPGPKey(PGPSecretKeyRing keyRing, OpenPGPImplementation implementation) + { + super(keyRing, implementation); + + // Process and map secret keys + this.secretKeys = new HashMap<>(); + for (OpenPGPComponentKey key : getKeys()) + { + KeyIdentifier identifier = key.getKeyIdentifier(); + PGPSecretKey secretKey = keyRing.getSecretKey(identifier); + if (secretKey == null) + { + continue; + } + + secretKeys.put(identifier, new OpenPGPSecretKey(key, secretKey, implementation.pbeSecretKeyDecryptorBuilderProvider())); + } + } + + @Override + public List getComponents() + { + // We go through the list of components returned by OpenPGPCertificate and replace those components + // where we have the secret key available + + // contains only public components + List components = super.getComponents(); + for (int i = components.size() - 1 ; i >= 0; i--) + { + OpenPGPCertificateComponent component = components.get(i); + if (component instanceof OpenPGPComponentKey) + { + OpenPGPSecretKey secretKey = getSecretKey((OpenPGPComponentKey) component); + if (secretKey != null) + { + // swap in secret component + components.remove(i); + components.add(i, secretKey); + } + } + } + return components; + } + + public static OpenPGPKey fromAsciiArmor(String armor) + throws IOException + { + return fromAsciiArmor(armor, OpenPGPImplementation.getInstance()); + } + + public static OpenPGPKey fromAsciiArmor( + String armor, + OpenPGPImplementation implementation) + throws IOException + { + return fromBytes( + armor.getBytes(StandardCharsets.UTF_8), + implementation); + } + + public static OpenPGPKey fromBytes( + byte[] bytes, + OpenPGPImplementation implementation) + throws IOException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(bytes); + InputStream decoderStream = PGPUtil.getDecoderStream(bIn); + BCPGInputStream pIn = BCPGInputStream.wrap(decoderStream); + PGPObjectFactory objectFactory = implementation.pgpObjectFactory(pIn); + + Object object = objectFactory.nextObject(); + if (!(object instanceof PGPSecretKeyRing)) + { + throw new IOException("Not a secret key."); + } + + PGPSecretKeyRing keyRing = (PGPSecretKeyRing) object; + return new OpenPGPKey(keyRing, implementation); + } + + /** + * Return a {@link Map} containing all {@link OpenPGPSecretKey} components (secret subkeys) of the key. + * + * @return secret key components + */ + public Map getSecretKeys() + { + return new HashMap<>(secretKeys); + } + + /** + * Return the {@link OpenPGPSecretKey} identified by the passed {@link KeyIdentifier}. + * + * @param identifier key identifier + * @return corresponding secret key or null + */ + public OpenPGPSecretKey getSecretKey(KeyIdentifier identifier) + { + return secretKeys.get(identifier); + } + + /** + * Return the {@link OpenPGPSecretKey} that corresponds to the passed {@link OpenPGPComponentKey}. + * + * @param key component key + * @return corresponding secret key or null + */ + public OpenPGPSecretKey getSecretKey(OpenPGPComponentKey key) + { + return getSecretKey(key.getKeyIdentifier()); + } + + @Override + public PGPSecretKeyRing getPGPKeyRing() + { + return (PGPSecretKeyRing) super.getPGPKeyRing(); + } + + @Override + public String toAsciiArmoredString() + throws IOException + { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + ArmoredOutputStream.Builder armorBuilder = ArmoredOutputStream.builder() + .clearHeaders(); + + for (String slice : fingerprintComments()) + { + armorBuilder.addComment(slice); + } + + for (OpenPGPUserId userId : getPrimaryKey().getUserIDs()) + { + armorBuilder.addComment(userId.getUserId()); + } + + ArmoredOutputStream aOut = armorBuilder.build(bOut); + BCPGOutputStream pOut = new BCPGOutputStream(aOut, PacketFormat.CURRENT); + + getPGPKeyRing().encode(pOut); + pOut.close(); + aOut.close(); + return bOut.toString(); + } + + /** + * Secret key component of a {@link org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPPrimaryKey} or + * {@link org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPSubkey}. + */ + public static class OpenPGPSecretKey + extends OpenPGPComponentKey + { + private final PGPSecretKey rawSecKey; + private final OpenPGPComponentKey pubKey; + private final PBESecretKeyDecryptorBuilderProvider decryptorBuilderProvider; + + /** + * Constructor. + * + * @param pubKey corresponding public key component + * @param secKey secret key + * @param decryptorBuilderProvider for unlocking private keys + */ + public OpenPGPSecretKey(OpenPGPComponentKey pubKey, + PGPSecretKey secKey, + PBESecretKeyDecryptorBuilderProvider decryptorBuilderProvider) + { + super(pubKey.getPGPPublicKey(), pubKey.getCertificate()); + this.decryptorBuilderProvider = decryptorBuilderProvider; + this.rawSecKey = secKey; + this.pubKey = pubKey; + } + + @Override + protected OpenPGPCertificateComponent getPublicComponent() + { + // return the public key component to properly map this secret key to its public key component when + // the public key component is used as key in a map. + return pubKey; + } + + @Override + public String toDetailString() + { + return "Private" + pubKey.toDetailString(); + } + + /** + * Return the underlying {@link PGPSecretKey}. + * + * @return secret key + */ + public PGPSecretKey getPGPSecretKey() + { + return rawSecKey; + } + + /** + * Return the public {@link OpenPGPComponentKey} corresponding to this {@link OpenPGPSecretKey}. + * + * @return public component key + */ + public OpenPGPComponentKey getPublicKey() + { + return pubKey; + } + + /** + * If true, the secret key is not available in plain and likely needs to be decrypted by providing + * a key passphrase. + */ + public boolean isLocked() + { + return getPGPSecretKey().getS2KUsage() != SecretKeyPacket.USAGE_NONE; + } + + /** + * Access the {@link PGPPrivateKey} by unlocking the potentially locked secret key using the provided + * passphrase. Note: If the key is not locked, it is sufficient to pass null as passphrase. + * + * @param passphrase passphrase or null + * @return unlocked private key + * @throws PGPException if the key cannot be unlocked + */ + public PGPPrivateKey unlock(char[] passphrase) + throws PGPException + { + PBESecretKeyDecryptor decryptor = null; + if (passphrase != null) + { + decryptor = decryptorBuilderProvider.provide().build(passphrase); + } + return getPGPSecretKey().extractPrivateKey(decryptor); + } + + public boolean isPassphraseCorrect(char[] passphrase) + { + try + { + PGPPrivateKey privateKey = unlock(passphrase); + return privateKey != null; + } + catch (PGPException e) + { + return false; + } + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyMaterialPool.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyMaterialPool.java new file mode 100644 index 0000000000..ac32725c8b --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyMaterialPool.java @@ -0,0 +1,206 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.openpgp.KeyIdentifier; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Implementation of the {@link OpenPGPKeyMaterialProvider} which caches items in a {@link HashMap}. + * It allows to provide key or certificates dynamically via a {@link #callback} that can be set using + * {@link #setMissingItemCallback(OpenPGPKeyMaterialProvider)}. + * Results from this callback are automatically cached for later access. This behavior can be adjusted via + * {@link #setCacheResultsFromCallback(boolean)}. + * + * @param {@link OpenPGPCertificate} or {@link OpenPGPKey} + */ +public abstract class OpenPGPKeyMaterialPool + implements OpenPGPKeyMaterialProvider +{ + private final Map pool = new HashMap<>(); + private OpenPGPKeyMaterialProvider callback = null; + private boolean cacheResultsFromCallback = true; + + /** + * Create an empty pool. + */ + public OpenPGPKeyMaterialPool() + { + + } + + /** + * Create a pool from the single provided item. + * @param item item + */ + public OpenPGPKeyMaterialPool(M item) + { + addItem(item); + } + + /** + * Create a pool and initialize its contents with the provided collection of items. + * @param items collection of keys or certificates + */ + public OpenPGPKeyMaterialPool(Collection items) + { + for (M item : items) + { + addItem(item); + } + } + + /** + * Set a callback that gets fired whenever an item is requested, which is not found in the pool. + * + * @param callback callback + * @return this + */ + public OpenPGPKeyMaterialPool setMissingItemCallback(OpenPGPKeyMaterialProvider callback) + { + this.callback = Objects.requireNonNull(callback); + return this; + } + + /** + * Decide, whether the implementation should add {@link OpenPGPCertificate certificates} returned by + * {@link #callback} to the pool of cached certificates. + * + * @param cacheResults if true, cache certificates from callback + * @return this + */ + public OpenPGPKeyMaterialPool setCacheResultsFromCallback(boolean cacheResults) + { + this.cacheResultsFromCallback = cacheResults; + return this; + } + + @Override + public M provide(KeyIdentifier componentKeyIdentifier) + { + M result = pool.get(componentKeyIdentifier); + if (result == null && callback != null) + { + // dynamically request certificate or key from callback + result = callback.provide(componentKeyIdentifier); + if (cacheResultsFromCallback) + { + addItem(result); + } + } + return result; + } + + /** + * Add a certificate to the pool. + * Note: If multiple items share the same subkey material, adding an item might overwrite the reference to + * another item for that subkey. + * + * @param item OpenPGP key or certificate that shall be added into the pool + * @return this + */ + public OpenPGPKeyMaterialPool addItem(M item) + { + if (item != null) + { + for (KeyIdentifier identifier : item.getAllKeyIdentifiers()) + { + pool.put(identifier, item); + } + } + return this; + } + + /** + * Return all items from the pool. + * @return all items + */ + public Collection getAllItems() + { + return pool.values().stream() + .distinct() + .collect(Collectors.toList()); + } + + /** + * Implementation of {@link OpenPGPKeyMaterialPool} tailored to provide {@link OpenPGPKey OpenPGPKeys}. + */ + public static class OpenPGPKeyPool + extends OpenPGPKeyMaterialPool + implements OpenPGPKeyProvider + { + public OpenPGPKeyPool() + { + super(); + } + + public OpenPGPKeyPool(Collection items) + { + super(items); + } + + @Override + public OpenPGPKeyPool setMissingItemCallback(OpenPGPKeyMaterialProvider callback) + { + super.setMissingItemCallback(callback); + return this; + } + + @Override + public OpenPGPKeyPool setCacheResultsFromCallback(boolean cacheResults) + { + super.setCacheResultsFromCallback(cacheResults); + return this; + } + + @Override + public OpenPGPKeyPool addItem(OpenPGPKey item) + { + super.addItem(item); + return this; + } + } + + /** + * Implementation of {@link OpenPGPKeyMaterialPool} tailored to providing + * {@link OpenPGPCertificate OpenPGPCertificates}. + */ + public static class OpenPGPCertificatePool + extends OpenPGPKeyMaterialPool + implements OpenPGPCertificateProvider + { + public OpenPGPCertificatePool() + { + super(); + } + + public OpenPGPCertificatePool(Collection items) + { + super(items); + } + + @Override + public OpenPGPCertificatePool setMissingItemCallback(OpenPGPKeyMaterialProvider callback) + { + super.setMissingItemCallback(callback); + return this; + } + + @Override + public OpenPGPCertificatePool setCacheResultsFromCallback(boolean cacheResults) + { + super.setCacheResultsFromCallback(cacheResults); + return this; + } + + @Override + public OpenPGPCertificatePool addItem(OpenPGPCertificate item) + { + super.addItem(item); + return this; + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyMaterialProvider.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyMaterialProvider.java new file mode 100644 index 0000000000..c842fcaba5 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyMaterialProvider.java @@ -0,0 +1,40 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.openpgp.KeyIdentifier; + +/** + * Interface for providing OpenPGP keys or certificates. + * + * @param either {@link OpenPGPCertificate} or {@link OpenPGPKey} + */ +public interface OpenPGPKeyMaterialProvider +{ + /** + * Provide the requested {@link OpenPGPCertificate} or {@link OpenPGPKey} containing the component key identified + * by the passed in {@link KeyIdentifier}. + * + * @param componentKeyIdentifier identifier of a component key (primary key or subkey) + * @return the OpenPGP certificate or key containing the identified component key + */ + M provide(KeyIdentifier componentKeyIdentifier); + + /** + * Interface for requesting {@link OpenPGPCertificate OpenPGPCertificates} by providing a {@link KeyIdentifier}. + * The {@link KeyIdentifier} can either be that of the certificates primary key, or of a subkey. + */ + interface OpenPGPCertificateProvider + extends OpenPGPKeyMaterialProvider + { + + } + + /** + * Interface for requesting {@link OpenPGPKey OpenPGPKeys} by providing a {@link KeyIdentifier}. + * The {@link KeyIdentifier} can either be that of the keys primary key, or of a subkey. + */ + interface OpenPGPKeyProvider + extends OpenPGPKeyMaterialProvider + { + + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageGenerator.java new file mode 100644 index 0000000000..52e817823f --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageGenerator.java @@ -0,0 +1,797 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.AEADAlgorithmTags; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites; +import org.bouncycastle.bcpg.sig.PreferredAlgorithms; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.openpgp.KeyIdentifier; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPPadding; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Stack; +import java.util.stream.Collectors; + +public class OpenPGPMessageGenerator +{ + public static final int BUFFER_SIZE = 1024; + + private final OpenPGPImplementation implementation; + private final Configuration config = new Configuration(); + + // Literal Data metadata + private Date fileModificationDate = null; + private String filename = null; + private char format = PGPLiteralData.BINARY; + private PGPEncryptedDataGenerator.SessionKeyExtractionCallback sessionKeyExtractionCallback; + + public OpenPGPMessageGenerator() + { + this(OpenPGPImplementation.getInstance()); + } + + public OpenPGPMessageGenerator(OpenPGPImplementation implementation) + { + this.implementation = Objects.requireNonNull(implementation); + } + + /** + * Add a recipients certificate to the set of encryption keys. + * Subkeys will be selected using the default {@link SubkeySelector}, which can be replaced by calling + * {@link Configuration#setEncryptionKeySelector(SubkeySelector)}. + * The recipient will be able to decrypt the message using their corresponding secret key. + * + * @param recipientCertificate recipient certificate (public key) + * @return this + */ + public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate recipientCertificate) + { + return addEncryptionCertificate(recipientCertificate, config.encryptionKeySelector); + } + + /** + * Add a recipients certificate to the set of encryption keys. + * Subkeys will be selected using the provided {@link SubkeySelector}. + * The recipient will be able to decrypt the message using their corresponding secret key. + * + * @param recipientCertificate recipient certificate (public key) + * @param subkeySelector selector for encryption subkeys + * @return this + */ + public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate recipientCertificate, SubkeySelector subkeySelector) + { + config.recipients.add(new Recipient(recipientCertificate, subkeySelector)); + return this; + } + + /** + * Add a message passphrase. + * In addition to optional public key encryption, the message will be decryptable using the given passphrase. + * + * @param passphrase passphrase + * @return this + */ + public OpenPGPMessageGenerator addEncryptionPassphrase(char[] passphrase) + { + config.passphrases.add(passphrase); + return this; + } + + public OpenPGPMessageGenerator addSigningKey(OpenPGPKey signingKey) + { + return addSigningKey(signingKey, key -> null); + } + + /** + * Sign the message using a secret signing key. + * The signing subkey(s) will be selected by the default {@link SubkeySelector} which can be replaced by + * calling {@link Configuration#setSigningKeySelector(SubkeySelector)}. + * + * @param signingKey OpenPGP key + * @param signingKeyDecryptorProvider provider for decryptors to unlock the signing (sub-)keys. + * @return this + */ + public OpenPGPMessageGenerator addSigningKey( + OpenPGPKey signingKey, + SecretKeyPassphraseProvider signingKeyDecryptorProvider) + { + return addSigningKey(signingKey, signingKeyDecryptorProvider, config.signingKeySelector); + } + + /** + * Sign the message using a secret signing key. + * + * @param signingKey OpenPGP key + * @param signingKeyDecryptorProvider provider for decryptors to unlock the signing (sub-)keys. + * @param subkeySelector selector for selecting signing subkey(s) + * @return this + */ + public OpenPGPMessageGenerator addSigningKey( + OpenPGPKey signingKey, + SecretKeyPassphraseProvider signingKeyDecryptorProvider, + SubkeySelector subkeySelector) + { + config.signingKeys.add(new Signer(signingKey, signingKeyDecryptorProvider, subkeySelector)); + return this; + } + + /** + * Specify, whether the output OpenPGP message will be ASCII armored or not. + * + * @param armored boolean + * @return this + */ + public OpenPGPMessageGenerator setArmored(boolean armored) + { + this.config.setArmored(armored); + return this; + } + + public OpenPGPMessageGenerator setFileMetadata(File file) + { + this.filename = file.getName(); + this.fileModificationDate = new Date(file.lastModified()); + this.format = PGPLiteralData.BINARY; + return this; + } + + public OpenPGPMessageGenerator setSessionKeyExtractionCallback( + PGPEncryptedDataGenerator.SessionKeyExtractionCallback callback) + { + this.sessionKeyExtractionCallback = callback; + return this; + } + + /** + * Open an {@link OpenPGPMessageOutputStream} over the given output stream. + * @param out output stream + * @return OpenPGP message output stream + * @throws PGPException if the output stream cannot be created + */ + public OpenPGPMessageOutputStream open(OutputStream out) + throws PGPException, IOException + { + OpenPGPMessageOutputStream.Builder streamBuilder = OpenPGPMessageOutputStream.builder(); + + applyOptionalAsciiArmor(streamBuilder); + applyOptionalEncryption(streamBuilder, sessionKeyExtractionCallback); + applySignatures(streamBuilder); + applyOptionalCompression(streamBuilder); + applyLiteralDataWrap(streamBuilder); + + return streamBuilder.build(out); + } + + /** + * Apply ASCII armor if necessary. + * The output will only be wrapped in ASCII armor, if {@link #setArmored(boolean)} is set + * to true (is true by default). + * The {@link ArmoredOutputStream} will be instantiated using the {@link ArmoredOutputStreamFactory} + * which can be replaced using {@link Configuration#setArmorStreamFactory(ArmoredOutputStreamFactory)}. + * + * @param builder OpenPGP message output stream builder + */ + private void applyOptionalAsciiArmor(OpenPGPMessageOutputStream.Builder builder) + { + if (config.isArmored) + { + builder.armor(config.armorStreamFactory); + } + } + + /** + * Optionally apply message encryption. + * If no recipient certificates and no encryption passphrases were supplied, no encryption + * will be applied. + * Otherwise, encryption mode and algorithms will be negotiated and message encryption will be applied. + * + * @param builder OpenPGP message output stream builder + * @param sessionKeyExtractionCallback callback to extract the session key (nullable) + */ + private void applyOptionalEncryption( + OpenPGPMessageOutputStream.Builder builder, + PGPEncryptedDataGenerator.SessionKeyExtractionCallback sessionKeyExtractionCallback) + { + MessageEncryptionMechanism encryption = config.negotiateEncryption(); + if (!encryption.isEncrypted()) + { + return; // No encryption + } + + PGPDataEncryptorBuilder encBuilder = implementation.pgpDataEncryptorBuilder( + encryption.getSymmetricKeyAlgorithm()); + + // Specify container type for the plaintext + switch (encryption.getMode()) + { + case SEIPDv1: + encBuilder.setWithIntegrityPacket(true); + break; + + case SEIPDv2: + encBuilder.setWithAEAD(encryption.getAeadAlgorithm(), 6); + encBuilder.setUseV6AEAD(); + break; + + case LIBREPGP_OED: + encBuilder.setWithAEAD(encryption.getAeadAlgorithm(), 6); + encBuilder.setUseV5AEAD(); + break; + } + + PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(encBuilder); + // For sake of interoperability and simplicity, we always use a dedicated session key for message encryption + // even if only a single PBE encryption method was added and S2K result could be used as session-key directly. + encGen.setForceSessionKey(true); + encGen.setSessionKeyExtractionCallback(sessionKeyExtractionCallback); + + // Setup asymmetric message encryption + for (Recipient recipient : config.recipients) + { + for (OpenPGPCertificate.OpenPGPComponentKey encryptionSubkey : recipient.encryptionSubkeys()) + { + PublicKeyKeyEncryptionMethodGenerator method = implementation.publicKeyKeyEncryptionMethodGenerator( + encryptionSubkey.getPGPPublicKey()); + encGen.addMethod(method); + } + } + + // Setup symmetric (password-based) message encryption + for (char[] passphrase : config.passphrases) + { + PBEKeyEncryptionMethodGenerator skeskGen; + switch (encryption.getMode()) + { + case SEIPDv1: + case LIBREPGP_OED: + // "v4" and LibrePGP use symmetric-key encrypted session key packets version 4 (SKESKv4) + skeskGen = implementation.pbeKeyEncryptionMethodGenerator(passphrase); + break; + + case SEIPDv2: + // v6 uses symmetric-key encrypted session key packets version 6 (SKESKv6) using AEAD + skeskGen = implementation.pbeKeyEncryptionMethodGenerator(passphrase, S2K.Argon2Params.memoryConstrainedParameters()); + break; + default: continue; + } + + skeskGen.setSecureRandom(CryptoServicesRegistrar.getSecureRandom()); // Prevent NPE + encGen.addMethod(skeskGen); + } + + // Finally apply encryption + builder.encrypt(o -> + { + try + { + return encGen.open(o, new byte[BUFFER_SIZE]); + } + catch (IOException e) + { + throw new PGPException("Could not open encryptor OutputStream", e); + } + }); + + // Optionally, append a padding packet as the last packet inside the SEIPDv2 packet. + if (encryption.getMode() == EncryptedDataPacketType.SEIPDv2 && config.isPadded) + { + builder.padding(o -> new OpenPGPMessageOutputStream.PaddingPacketAppenderOutputStream(o, PGPPadding::new)); + } + } + + /** + * Apply OpenPGP inline-signatures. + * + * @param builder OpenPGP message output stream builder + */ + private void applySignatures(OpenPGPMessageOutputStream.Builder builder) + { + builder.sign(o -> + { + Stack signatureGenerators = new Stack<>(); + for (Signer s : config.signingKeys) + { + for (OpenPGPKey.OpenPGPSecretKey signingSubkey : s.signingSubkeys()) + { + int hashAlgorithm = config.negotiateHashAlgorithm(s.signingKey, signingSubkey); + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + implementation.pgpContentSignerBuilder( + signingSubkey.getPGPSecretKey().getPublicKey().getAlgorithm(), hashAlgorithm), + signingSubkey.getPGPSecretKey().getPublicKey()); + char[] passphrase = signingSubkey.isLocked() ? s.passphraseProvider.providePassphrase(signingSubkey) : null; + PGPPrivateKey privateKey = signingSubkey.unlock(passphrase); + + sigGen.init(PGPSignature.BINARY_DOCUMENT, privateKey); + signatureGenerators.push(sigGen); + } + } + + // One-Pass-Signatures + Iterator sigGens = signatureGenerators.iterator(); + while (sigGens.hasNext()) + { + PGPSignatureGenerator gen = sigGens.next(); + PGPOnePassSignature ops = gen.generateOnePassVersion(sigGens.hasNext()); + ops.encode(o); + } + + return new OpenPGPMessageOutputStream.SignatureGeneratorOutputStream(o, signatureGenerators); + }); + } + + private void applyOptionalCompression(OpenPGPMessageOutputStream.Builder builder) + { + int compressionAlgorithm = config.negotiateCompression(); + if (compressionAlgorithm == CompressionAlgorithmTags.UNCOMPRESSED) + { + return; // Uncompressed + } + + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(compressionAlgorithm); + + builder.compress(o -> + { + try + { + return compGen.open(o, new byte[BUFFER_SIZE]); + } + catch (IOException e) + { + throw new PGPException("Could not apply compression", e); + } + }); + } + + /** + * Setup wrapping of the message plaintext in a literal data packet. + * + * @param builder OpenPGP message output stream + */ + private void applyLiteralDataWrap(OpenPGPMessageOutputStream.Builder builder) + { + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + builder.literalData(o -> + { + try + { + return litGen.open(o, + format, + filename != null ? filename : "", + fileModificationDate != null ? fileModificationDate : PGPLiteralData.NOW, + new byte[BUFFER_SIZE]); + } + catch (IOException e) + { + throw new PGPException("Could not apply literal data wrapping", e); + } + }); + } + + public OpenPGPMessageGenerator setIsPadded(boolean isPadded) + { + config.setPadded(isPadded); + return this; + } + + public Configuration getConfiguration() + { + return config; + } + + public interface ArmoredOutputStreamFactory + extends OpenPGPMessageOutputStream.OutputStreamFactory + { + ArmoredOutputStream get(OutputStream out); + } + + public interface CompressionNegotiator + { + /** + * Negotiate a compression algorithm. + * Returning {@link org.bouncycastle.bcpg.CompressionAlgorithmTags#UNCOMPRESSED} will result in no compression. + * + * @param configuration message generator configuration + * @return negotiated compression algorithm ID + */ + int negotiateCompression(Configuration configuration); + } + + public interface EncryptionNegotiator + { + /** + * Negotiate encryption mode and algorithms. + * + * @param configuration message generator configuration + * @return negotiated encryption mode and algorithms + */ + MessageEncryptionMechanism negotiateEncryption(Configuration configuration); + } + + public interface HashAlgorithmNegotiator + { + int negotiateHashAlgorithm(OpenPGPKey key, OpenPGPCertificate.OpenPGPComponentKey subkey); + } + + public static class Configuration + { + private boolean isArmored = true; + public boolean isPadded = true; + private final List recipients = new ArrayList<>(); + private final List signingKeys = new ArrayList<>(); + private final List passphrases = new ArrayList<>(); + + // Factory for creating ASCII armor + private ArmoredOutputStreamFactory armorStreamFactory = + outputStream -> ArmoredOutputStream.builder() + .clearHeaders() // Hide version + .enableCRC(false) // Disable CRC sum + .build(outputStream); + + private SubkeySelector encryptionKeySelector = OpenPGPCertificate::getEncryptionKeys; + + private SubkeySelector signingKeySelector = OpenPGPCertificate::getSigningKeys; + + // Encryption method negotiator for when only password-based encryption is requested + private EncryptionNegotiator passwordBasedEncryptionNegotiator = configuration -> + MessageEncryptionMechanism.aead(SymmetricKeyAlgorithmTags.AES_256, AEADAlgorithmTags.OCB); + + // Encryption method negotiator for when public-key encryption is requested + private EncryptionNegotiator publicKeyBasedEncryptionNegotiator = configuration -> + { + // Decide, if SEIPDv2 (OpenPGP v6-style AEAD) is supported by all recipients. + boolean seipd2Supported = configuration.recipients + .stream() + // ignore keys that can't encrypt at all + .filter(recipient -> !recipient.certificate.getEncryptionKeys().isEmpty()) + // Make sure all recipients have at least one key that can do SEIPD2 + .allMatch(recipient -> recipient.certificate.getEncryptionKeys() + .stream() + // if some recipient only has keys which DO NOT support SEIPD2 -> downgrade to SEIPD1 + .anyMatch(subkey -> + { + Features features = subkey.getFeatures(); + return features != null && features.supportsFeature(Features.FEATURE_SEIPD_V2); + }) + ); + + if (seipd2Supported) + { + PreferredAEADCiphersuites commonDenominator = configuration.recipients + .stream() + // Ignore certificates that cannot encrypt + .filter(recipient -> !recipient.certificate.getEncryptionKeys().isEmpty()) + // Ignore subkeys on recipients certificates that do not support SEIPDv2 + .map(recipient -> + { + List encKeys = recipient.encryptionSubkeys(); + return encKeys.stream().filter(it -> it.getFeatures().supportsSEIPDv2()); + }) + // go from List> to List + .flatMap(it -> it) + // Extract AEAD preferences per key + .map(OpenPGPCertificate.OpenPGPComponentKey::getAEADCipherSuitePreferences) + // Take the intersection of combinations to find commonly preferred combination + .reduce((current, next) -> + { + List nextPreferences = Arrays.asList(next.getAlgorithms()); + return new PreferredAEADCiphersuites(false, Arrays.stream(current.getAlgorithms()) + .filter(nextPreferences::contains).toArray(PreferredAEADCiphersuites.Combination[]::new)); + }) + // If no common combination was found, fall back to implicitly supported algorithms + .orElse(PreferredAEADCiphersuites.builder(false) + // Default combination + .addCombination(SymmetricKeyAlgorithmTags.AES_128, AEADAlgorithmTags.OCB) + .build() + ); + PreferredAEADCiphersuites.Combination[] combinations = commonDenominator.getAlgorithms(); + // Select best combo from common combinations + // TODO: Make sure this is actually the best + PreferredAEADCiphersuites.Combination best = combinations[0]; + return MessageEncryptionMechanism.aead(best.getSymmetricAlgorithm(), best.getAeadAlgorithm()); + } + else + { + PreferredAlgorithms commonDenominator = configuration.recipients + .stream() + // Ignore certificates that cannot encrypt + .filter(recipient -> !recipient.certificate.getEncryptionKeys().isEmpty()) + .map(Recipient::encryptionSubkeys) + .map(List::stream) + // go from List> to List + .flatMap(it -> it) + // Extract sym. cipher preferences per key + .map(OpenPGPCertificate.OpenPGPComponentKey::getSymmetricCipherPreferences) + // Take the intersection of combinations to find commonly preferred combination + .reduce((current, next) -> + new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_SYM_ALGS, false, + Arrays.stream(current.getPreferences()) + .filter(alg -> Arrays.stream(next.getPreferences()).anyMatch(it -> alg == it)) + .toArray())) + // If no common combination was found, fall back to implicitly supported algorithms + .orElse(new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_SYM_ALGS, false, + new int[] { + SymmetricKeyAlgorithmTags.AES_128 + } // AES128 is "MUST implement" + )); + // TODO: Algorithm selection + int bestCipherPreference = commonDenominator.getPreferences()[0]; + + return MessageEncryptionMechanism.integrityProtected(bestCipherPreference); + } + }; + + // Primary encryption method negotiator + private final EncryptionNegotiator encryptionNegotiator = + configuration -> + { + // No encryption methods provided -> Unencrypted message + if (configuration.recipients.isEmpty() && configuration.passphrases.isEmpty()) + { + return MessageEncryptionMechanism.unencrypted(); + } + + // No public-key encryption requested -> password-based encryption + else if (configuration.recipients.isEmpty()) + { + // delegate negotiation to pbe negotiator + return passwordBasedEncryptionNegotiator.negotiateEncryption(configuration); + } + else + { + // delegate negotiation to pkbe negotiator + return publicKeyBasedEncryptionNegotiator.negotiateEncryption(configuration); + } + }; + + // TODO: Implement properly, taking encryption into account (sign-only should not compress) + private CompressionNegotiator compressionNegotiator = + configuration -> CompressionAlgorithmTags.UNCOMPRESSED; + + private HashAlgorithmNegotiator hashAlgorithmNegotiator = + (key, subkey) -> + { + // TODO: Take into consideration hash preferences of recipients, not the sender + PreferredAlgorithms hashPreferences = subkey.getHashAlgorithmPreferences(); + if (hashPreferences == null) + { + return HashAlgorithmTags.SHA512; + } + return hashPreferences.getPreferences()[0]; + }; + + /** + * Replace the default {@link EncryptionNegotiator} that gets to decide, which {@link MessageEncryptionMechanism} mode + * to use if only password-based encryption is used. + * + * @param pbeNegotiator custom PBE negotiator. + * @return this + */ + public Configuration setPasswordBasedEncryptionNegotiator(EncryptionNegotiator pbeNegotiator) + { + this.passwordBasedEncryptionNegotiator = Objects.requireNonNull(pbeNegotiator); + return this; + } + + /** + * Replace the default {@link EncryptionNegotiator} that decides, which {@link MessageEncryptionMechanism} + * mode to use if public-key encryption is used. + * + * @param pkbeNegotiator custom encryption negotiator that gets to decide if PK-based encryption is used + * @return this + */ + public Configuration setPublicKeyBasedEncryptionNegotiator(EncryptionNegotiator pkbeNegotiator) + { + this.publicKeyBasedEncryptionNegotiator = Objects.requireNonNull(pkbeNegotiator); + return this; + } + + /** + * Replace the default encryption key selector with a custom implementation. + * The encryption key selector is responsible for selecting one or more encryption subkeys from a + * recipient certificate. + * + * @param encryptionKeySelector selector for encryption (sub-)keys + * @return this + */ + public Configuration setEncryptionKeySelector(SubkeySelector encryptionKeySelector) + { + this.encryptionKeySelector = Objects.requireNonNull(encryptionKeySelector); + return this; + } + + /** + * Replace the default signing key selector with a custom implementation. + * The signing key selector is responsible for selecting one or more signing subkeys from a signing key. + * + * @param signingKeySelector selector for signing (sub-)keys + * @return this + */ + public Configuration setSigningKeySelector(SubkeySelector signingKeySelector) + { + this.signingKeySelector = Objects.requireNonNull(signingKeySelector); + return this; + } + + /** + * Replace the default {@link CompressionNegotiator} with a custom implementation. + * The {@link CompressionNegotiator} is used to negotiate, whether and how to compress the literal data packet. + * + * @param compressionNegotiator negotiator + * @return this + */ + public Configuration setCompressionNegotiator(CompressionNegotiator compressionNegotiator) + { + this.compressionNegotiator = Objects.requireNonNull(compressionNegotiator); + return this; + } + + /** + * Replace the default {@link HashAlgorithmNegotiator} with a custom implementation. + * + * @param hashAlgorithmNegotiator custom hash algorithm negotiator + * @return this + */ + public Configuration setHashAlgorithmNegotiator(HashAlgorithmNegotiator hashAlgorithmNegotiator) + { + this.hashAlgorithmNegotiator = Objects.requireNonNull(hashAlgorithmNegotiator); + return this; + } + + /** + * Replace the {@link ArmoredOutputStreamFactory} with a custom implementation. + * + * @param factory factory for {@link ArmoredOutputStream} instances + * @return this + */ + public Configuration setArmorStreamFactory(ArmoredOutputStreamFactory factory) + { + this.armorStreamFactory = Objects.requireNonNull(factory); + return this; + } + + public Configuration setArmored(boolean isArmored) + { + this.isArmored = isArmored; + return this; + } + + public Configuration setPadded(boolean isPadded) + { + this.isPadded = isPadded; + return this; + } + + public int negotiateCompression() + { + return compressionNegotiator.negotiateCompression(this); + } + + public int negotiateHashAlgorithm(OpenPGPKey signingKey, OpenPGPKey.OpenPGPSecretKey signingSubkey) + { + return hashAlgorithmNegotiator.negotiateHashAlgorithm(signingKey, signingSubkey); + } + + public MessageEncryptionMechanism negotiateEncryption() + { + return encryptionNegotiator.negotiateEncryption(this); + } + } + + /** + * Tuple representing a recipients OpenPGP certificate. + */ + static class Recipient + { + private final OpenPGPCertificate certificate; + private final SubkeySelector subkeySelector; + + /** + * Create a {@link Recipient}. + * + * @param certificate OpenPGP certificate (public key) + * @param subkeySelector selector to select encryption-capable subkeys from the certificate + */ + public Recipient(PGPPublicKeyRing certificate, SubkeySelector subkeySelector, OpenPGPImplementation implementation) + { + this(new OpenPGPCertificate(certificate, implementation), subkeySelector); + } + + public Recipient(OpenPGPCertificate certificate, SubkeySelector subkeySelector) + { + this.certificate = certificate; + this.subkeySelector = subkeySelector; + } + + /** + * Return a set of {@link PGPPublicKey subkeys} which will be used for message encryption. + * + * @return encryption capable subkeys for this recipient + */ + public List encryptionSubkeys() + { + return subkeySelector.select(certificate) + .stream() + .distinct() + .collect(Collectors.toList()); + } + } + + /** + * Tuple representing an OpenPGP key used for signing. + */ + static class Signer + { + private final OpenPGPKey signingKey; + private final SecretKeyPassphraseProvider passphraseProvider; + private final SubkeySelector subkeySelector; + + public Signer(OpenPGPKey signingKey, + SecretKeyPassphraseProvider passphraseProvider, + SubkeySelector subkeySelector) + { + this.signingKey = signingKey; + this.passphraseProvider = passphraseProvider; + this.subkeySelector = subkeySelector; + } + + public List signingSubkeys() + { + return subkeySelector.select(signingKey) + .stream() + .map(signingKey::getSecretKey) + .distinct() + .collect(Collectors.toList()); + } + } + + /** + * Interface for selecting a subset of keys from a {@link PGPKeyRing}. + * This is useful e.g. for selecting a signing key from an OpenPGP key, or a for selecting all + * encryption capable subkeys of a certificate. + */ + public interface SubkeySelector + { + /** + * Given a {@link PGPKeyRing}, select a subset of the key rings (sub-)keys and return their + * {@link KeyIdentifier KeyIdentifiers}. + * + * @param certificate OpenPGP key or certificate + * @return non-null list of identifiers + */ + List select(OpenPGPCertificate certificate); + } + + public interface SecretKeyPassphraseProvider + { + char[] providePassphrase(OpenPGPKey.OpenPGPSecretKey key); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageInputStream.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageInputStream.java new file mode 100644 index 0000000000..49f0a208bf --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageInputStream.java @@ -0,0 +1,743 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.AEADEncDataPacket; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; +import org.bouncycastle.openpgp.KeyIdentifier; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPMarker; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPadding; +import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An {@link InputStream} that processes an OpenPGP message. + * Its contents are the plaintext from the messages LiteralData packet. + * You can get information about the message (signatures, encryption method, message metadata) + * by reading ALL data from the stream, closing it with {@link #close()} and then retrieving a {@link Result} object + * by calling {@link #getResult()}. + */ +public class OpenPGPMessageInputStream + extends InputStream +{ + public static int MAX_RECURSION = 16; + + private final PGPObjectFactory objectFactory; + private final OpenPGPImplementation implementation; + + private final OpenPGPMessageProcessor processor; + + private final Result.Builder resultBuilder; + private final Layer layer; // the packet layer processed by this input stream + + private InputStream in; + + OpenPGPMessageInputStream(PGPObjectFactory objectFactory, + OpenPGPMessageProcessor processor) + { + this(objectFactory, processor, Result.builder()); + } + + private OpenPGPMessageInputStream(PGPObjectFactory objectFactory, + OpenPGPMessageProcessor processor, + Result.Builder resultBuilder) + { + this.objectFactory = objectFactory; + this.processor = processor; + this.implementation = processor.getImplementation(); + this.resultBuilder = resultBuilder; + try + { + this.layer = resultBuilder.openLayer(); + } + catch (PGPException e) + { + // cannot happen + throw new AssertionError(e); + } + } + + void process() + throws IOException, PGPException + { + Object next; + while ((next = objectFactory.nextObject()) != null) + { + // prefixed packets + + if (next instanceof PGPSignatureList) + { + // prefixed-signed message (SIG MSG) + PGPSignatureList prefixedSigs = (PGPSignatureList) next; + resultBuilder.prefixedSignatures(prefixedSigs); + } + else if (next instanceof PGPOnePassSignatureList) + { + // one-pass-signed message (OPS MSG SIG) + PGPOnePassSignatureList pgpOnePassSignatures = (PGPOnePassSignatureList) next; + resultBuilder.onePassSignatures(pgpOnePassSignatures); + } + else if (next instanceof PGPMarker) + { + // prefixed marker packet (ignore) + } + + else + { + // Init signatures of this layer + resultBuilder.initSignatures(processor); + + if (next instanceof PGPLiteralData) + { + // Literal Data \o/ + PGPLiteralData literalData = (PGPLiteralData) next; + resultBuilder.literalData( + literalData.getFileName(), + (char) literalData.getFormat(), + literalData.getModificationTime()); + + in = literalData.getDataStream(); + return; + } + else if (next instanceof PGPCompressedData) + { + // Compressed Data + PGPCompressedData compressedData = (PGPCompressedData) next; + resultBuilder.compressed(compressedData.getAlgorithm()); + + InputStream decompressed = compressedData.getDataStream(); + InputStream decodeIn = BCPGInputStream.wrap(decompressed); + PGPObjectFactory decFac = implementation.pgpObjectFactory(decodeIn); + OpenPGPMessageInputStream nestedIn = new OpenPGPMessageInputStream(decFac, processor, resultBuilder); + in = nestedIn; + nestedIn.process(); + return; + } + else if (next instanceof PGPEncryptedDataList) + { + // Encrypted Data + PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) next; + OpenPGPMessageProcessor.Decrypted decrypted = processor.decrypt(encryptedDataList); + InputStream decryptedIn = decrypted.inputStream; + resultBuilder.encrypted(decrypted); + InputStream decodeIn = BCPGInputStream.wrap(decryptedIn); + PGPObjectFactory decFac = implementation.pgpObjectFactory(decodeIn); + OpenPGPMessageInputStream nestedIn = new OpenPGPMessageInputStream(decFac, processor, resultBuilder); + in = nestedIn; + nestedIn.process(); + return; + } + else + { + processor.onException(new PGPException("Unexpected packet encountered: " + + next.getClass().getName())); + } + } + } + } + + @Override + public void close() + throws IOException + { + in.close(); + + Object next; + while ((next = objectFactory.nextObject()) != null) + { + if (next instanceof PGPSignatureList) + { + // one-pass-signed message (OPS MSG SIG) + PGPSignatureList signatures = (PGPSignatureList) next; + resultBuilder.last().onePassSignatures.addSignatures(signatures); + } + else if (next instanceof PGPPadding) + { + // padded message + } + else if (next instanceof PGPMarker) + { + // postfixed marker packet (ignore) + } + else + { + // unknown/unexpected packet + processor.onException(new PGPException("Unexpected trailing packet encountered: " + + next.getClass().getName())); + } + } + + resultBuilder.verifySignatures(processor); + resultBuilder.closeLayer(); + } + + @Override + public int read() + throws IOException + { + int i = in.read(); + if (i >= 0) + { + layer.onePassSignatures.update(i); + layer.prefixedSignatures.update(i); + } + return i; + } + + @Override + public int read(byte[] b) + throws IOException + { + int i = in.read(b); + if (i >= 0) + { + layer.onePassSignatures.update(b, 0, i); + layer.prefixedSignatures.update(b, 0, i); + } + return i; + } + + @Override + public int read(byte[] b, int off, int len) + throws IOException + { + int i = in.read(b, off, len); + if (i >= 0) + { + layer.onePassSignatures.update(b, off, len); + layer.prefixedSignatures.update(b, off, len); + } + return i; + } + + public Result getResult() + { + return resultBuilder.build(); + } + + public static class Result + { + private final List documentSignatures = new ArrayList<>(); + private OpenPGPCertificate.OpenPGPComponentKey decryptionKey; + private char[] decryptionPassphrase; + private PGPSessionKey sessionKey; + private MessageEncryptionMechanism encryptionMethod = MessageEncryptionMechanism.unencrypted(); + private int compressionAlgorithm = 0; + private String filename; + private char fileFormat; + private Date fileModificationTime; + + private Result(List layers) + { + for (Layer l : layers) + { + if (l.signatures != null) + documentSignatures.addAll(l.signatures); + + if (l.nested instanceof EncryptedData) + { + EncryptedData encryptedData = (EncryptedData) l.nested; + encryptionMethod = encryptedData.encryption; + sessionKey = encryptedData.sessionKey; + decryptionKey = encryptedData.decryptionKey; + decryptionPassphrase = encryptedData.decryptionPassphrase; + } + else if (l.nested instanceof CompressedData) + { + CompressedData compressedData = (CompressedData) l.nested; + compressionAlgorithm = compressedData.compressionAlgorithm; + } + else if (l.nested instanceof LiteralData) + { + LiteralData literalData = (LiteralData) l.nested; + filename = literalData.filename; + fileFormat = literalData.encoding; + fileModificationTime = literalData.modificationTime; + } + } + } + + static Builder builder() + { + return new Builder(); + } + + public MessageEncryptionMechanism getEncryptionMethod() + { + return encryptionMethod; + } + + public OpenPGPCertificate.OpenPGPComponentKey getDecryptionKey() + { + return decryptionKey; + } + + public char[] getDecryptionPassphrase() + { + return decryptionPassphrase; + } + + public PGPSessionKey getSessionKey() + { + return sessionKey; + } + + public int getCompressionAlgorithm() + { + return compressionAlgorithm; + } + + public String getFilename() + { + return filename; + } + + public char getFileFormat() + { + return fileFormat; + } + + public Date getFileModificationTime() + { + return fileModificationTime; + } + + public List getSignatures() + { + return new ArrayList<>(documentSignatures); + } + + static class Builder + { + private final List layers = new ArrayList<>(); + + private Builder() + { + + } + + Layer last() + { + return layers.get(layers.size() - 1); + } + + /** + * Enter a nested OpenPGP packet layer. + * + * @return the new layer + * @throws PGPException if the parser exceeded the maximum nesting depth ({@link #MAX_RECURSION}). + */ + Layer openLayer() + throws PGPException + { + if (layers.size() >= MAX_RECURSION) + { + throw new PGPException("Exceeded maximum packet nesting depth."); + } + Layer layer = new Layer(); + layers.add(layer); + return layer; + } + + /** + * Close a nested OpenPGP packet layer. + */ + void closeLayer() + { + for (int i = layers.size() - 1; i >= 0; i--) + { + Layer l = layers.get(i); + if (l.isOpen()) + { + l.close(); + return; + } + } + } + + /** + * Set the nested packet type of the current layer to {@link CompressedData}. + * + * @param compressionAlgorithm compression algorithm ID + */ + void compressed(int compressionAlgorithm) + { + last().setNested(new CompressedData(compressionAlgorithm)); + } + + /** + * Add One-Pass-Signature packets on the current layer. + * + * @param pgpOnePassSignatures one pass signature packets + */ + void onePassSignatures(PGPOnePassSignatureList pgpOnePassSignatures) + { + last().onePassSignatures.addOnePassSignatures(pgpOnePassSignatures); + } + + /** + * Build the {@link Result}. + * + * @return result + */ + Result build() + { + return new Result(layers); + } + + /** + * Add prefixed signatures on the current layer. + * + * @param prefixedSigs prefixed signatures + */ + void prefixedSignatures(PGPSignatureList prefixedSigs) + { + last().prefixedSignatures.addAll(prefixedSigs); + } + + /** + * Initialize any signatures on the current layer, prefixed and one-pass-signatures. + * + * @param processor message processor + */ + void initSignatures(OpenPGPMessageProcessor processor) + { + last().onePassSignatures.init(processor); + last().prefixedSignatures.init(processor); + } + + /** + * Verify all signatures on the current layer, prefixed and one-pass-signatures. + * + * @param processor message processor + */ + void verifySignatures(OpenPGPMessageProcessor processor) + { + Layer last = last(); + if (last.signatures != null) + { + return; + } + + last.signatures = new ArrayList<>(); + last.signatures.addAll(last.onePassSignatures.verify(processor)); + last.signatures.addAll(last.prefixedSignatures.verify(processor)); + } + + /** + * Set literal data metadata on the current layer. + * + * @param fileName filename + * @param format data format + * @param modificationTime modification time + */ + void literalData(String fileName, char format, Date modificationTime) + { + last().setNested(new LiteralData(fileName, format, modificationTime)); + } + + /** + * Set metadata from an encrypted data packet on the current layer. + * + * @param decrypted decryption result + */ + void encrypted(OpenPGPMessageProcessor.Decrypted decrypted) + { + last().setNested(new EncryptedData(decrypted)); + } + } + } + + static class Layer + { + private final OnePassSignatures onePassSignatures = new OnePassSignatures(); + private final PrefixedSignatures prefixedSignatures = new PrefixedSignatures(); + + private List signatures = null; + + private Nested nested; + private boolean open = true; + + void setNested(Nested nested) + { + this.nested = nested; + } + + void close() + { + this.open = false; + } + + boolean isOpen() + { + return open; + } + } + + static class Nested + { + + } + + static class CompressedData + extends Nested + { + private final int compressionAlgorithm; + + public CompressedData(int algorithm) + { + this.compressionAlgorithm = algorithm; + } + } + + static class LiteralData + extends Nested + { + private final String filename; + private final char encoding; + private final Date modificationTime; + + LiteralData(String filename, char encoding, Date modificationTime) + { + this.filename = filename; + this.encoding = encoding; + this.modificationTime = modificationTime; + } + } + + static class EncryptedData + extends Nested + { + private final OpenPGPCertificate.OpenPGPComponentKey decryptionKey; + private final char[] decryptionPassphrase; + private final PGPSessionKey sessionKey; + private final MessageEncryptionMechanism encryption; + + EncryptedData(OpenPGPMessageProcessor.Decrypted decrypted) + { + this.decryptionKey = decrypted.decryptionKey; + this.decryptionPassphrase = decrypted.decryptionPassphrase; + this.sessionKey = decrypted.sessionKey; + if (decrypted.dataPacket instanceof SymmetricEncIntegrityPacket) + { + SymmetricEncIntegrityPacket seipd = (SymmetricEncIntegrityPacket) decrypted.dataPacket; + if (seipd.getVersion() == SymmetricEncIntegrityPacket.VERSION_2) + { + encryption = MessageEncryptionMechanism.aead( + seipd.getCipherAlgorithm(), seipd.getAeadAlgorithm()); + } + else + { + encryption = MessageEncryptionMechanism.integrityProtected(sessionKey.getAlgorithm()); + } + } + else if (decrypted.dataPacket instanceof AEADEncDataPacket) + { + encryption = MessageEncryptionMechanism.librePgp(sessionKey.getAlgorithm()); + } + else + { + throw new RuntimeException("Unexpected encrypted data packet type: " + decrypted.dataPacket.getClass().getName()); + } + } + } + + static class OnePassSignatures + { + private final List onePassSignatures = new ArrayList<>(); + private final List signatures = new ArrayList<>(); + private final Map issuers = new HashMap<>(); + + OnePassSignatures() + { + + } + + void addOnePassSignatures(PGPOnePassSignatureList onePassSignatures) + { + for (PGPOnePassSignature ops : onePassSignatures) + { + this.onePassSignatures.add(ops); + } + } + + void addSignatures(PGPSignatureList signatures) + { + for (PGPSignature signature : signatures) + { + this.signatures.add(signature); + } + } + + void init(OpenPGPMessageProcessor processor) + { + + for (PGPOnePassSignature ops : onePassSignatures) + { + KeyIdentifier identifier = ops.getKeyIdentifier(); + OpenPGPCertificate cert = processor.provideCertificate(identifier); + if (cert == null) + { + continue; + } + + try + { + OpenPGPCertificate.OpenPGPComponentKey key = cert.getKey(identifier); + issuers.put(ops, key); + ops.init(processor.getImplementation().pgpContentVerifierBuilderProvider(), + key.getPGPPublicKey()); + } + catch (PGPException e) + { + processor.onException(e); + } + } + } + + void update(int i) + { + for (PGPOnePassSignature onePassSignature : onePassSignatures) + { + onePassSignature.update((byte) i); + } + } + + void update(byte[] b, int off, int len) + { + for (PGPOnePassSignature onePassSignature : onePassSignatures) + { + onePassSignature.update(b, off, len); + } + } + + List verify( + OpenPGPMessageProcessor processor) + { + List dataSignatures = new ArrayList<>(); + int num = onePassSignatures.size(); + for (int i = 0; i < signatures.size(); i++) + { + PGPSignature signature = signatures.get(i); + PGPOnePassSignature ops = onePassSignatures.get(num - i - 1); + OpenPGPCertificate.OpenPGPComponentKey key = issuers.get(ops); + if (key == null) + { + continue; + } + + OpenPGPSignature.OpenPGPDocumentSignature dataSignature = + new OpenPGPSignature.OpenPGPDocumentSignature(signature, key); + try + { + dataSignature.verify(ops); + } + catch (PGPException e) + { + processor.onException(e); + } + dataSignatures.add(dataSignature); + } + return dataSignatures; + } + } + + static class PrefixedSignatures + { + private final List prefixedSignatures = new ArrayList<>(); + private final List dataSignatures = new ArrayList<>(); + + PrefixedSignatures() + { + + } + + void addAll(PGPSignatureList signatures) + { + for (PGPSignature signature : signatures) + { + this.prefixedSignatures.add(signature); + } + } + + void init(OpenPGPMessageProcessor processor) + { + for (PGPSignature sig : prefixedSignatures) + { + KeyIdentifier identifier = OpenPGPSignature.getMostExpressiveIdentifier(sig.getKeyIdentifiers()); + if (identifier == null) + { + dataSignatures.add(new OpenPGPSignature.OpenPGPDocumentSignature(sig, null)); + continue; + } + OpenPGPCertificate cert = processor.provideCertificate(identifier); + if (cert == null) + { + dataSignatures.add(new OpenPGPSignature.OpenPGPDocumentSignature(sig, null)); + continue; + } + + OpenPGPCertificate.OpenPGPComponentKey key = cert.getKey(identifier); + OpenPGPSignature.OpenPGPDocumentSignature signature = new OpenPGPSignature.OpenPGPDocumentSignature(sig, key); + dataSignatures.add(signature); + try + { + signature.signature.init( + processor.getImplementation().pgpContentVerifierBuilderProvider(), + cert.getKey(identifier).getPGPPublicKey()); + } + catch (PGPException e) + { + processor.onException(e); + } + } + } + + void update(int i) + { + for(PGPSignature signature : prefixedSignatures) + { + signature.update((byte) i); + } + } + + void update(byte[] buf, int off, int len) + { + for (PGPSignature signature : prefixedSignatures) + { + signature.update(buf, off, len); + } + } + + List verify(OpenPGPMessageProcessor processor) + { + for (OpenPGPSignature.OpenPGPDocumentSignature sig : dataSignatures) + { + try + { + sig.verify(); + } + catch (PGPException e) + { + processor.onException(e); + } + } + return dataSignatures; + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageOutputStream.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageOutputStream.java new file mode 100644 index 0000000000..d52db76900 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageOutputStream.java @@ -0,0 +1,470 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.PacketFormat; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPadding; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.util.io.TeeOutputStream; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Stack; + +/** + * Implementation of an {@link OutputStream} tailored to creating OpenPGP messages. + * Since not all OpenPGP-related OutputStreams forward {@link #close()} calls, we need to keep track of nested streams + * and close them in order. + * This stream can create OpenPGP messages following the following EBNF (which is a subset of the EBNF defined in RFC9580): + *
    + *
  • OpenPGP Message := ASCII-Armor(Optionally Encrypted Message) | Optionally Encrypted Message
  • + *
  • Literal Message := LiteralDataPacket
  • + *
  • Optionally Compressed Message := Literal Message | + * CompressedDataPacket(Literal Message)
  • + *
  • Optionally Signed Message := Optionally Compressed Message | + * OnePassSignaturePacket + Optionally Signed Message + SignaturePacket
  • + *
  • Optionally Padded Message := Optionally Signed Message | Optionally Signed Message + PaddingPacket
  • + *
  • Encrypted Message := SEIPDv1(Optionally Padded Message) | + * SEIPDv2(Optionally Padded Message) | + * OED(Optionally Padded Message)
  • + *
  • Optionally Encrypted Message := Optionally Padded Message | Encrypted Message
  • + *
+ * Therefore, this stream is capable of creating a wide variety of OpenPGP message, from simply + * LiteralDataPacket-wrapped plaintext over signed messages to encrypted, signed and padded messages. + * The following considerations lead to why this particular subset was chosen: + *
    + *
  • An encrypted message is not distinguishable from randomness, so it makes no sense to compress it
  • + *
  • Since signatures also consist of data which is not distinguishable from randomness, + * it makes no sense to compress them either
  • + *
  • Padding packets are used to prevent traffic analysis. + * Since they contain random data, we do not compress them. + * If a message is encrypted, we want to encrypt the padding packet to prevent an intermediate from stripping it
  • + *
  • Since (v4) signatures leak some metadata about the message plaintext (the hash and the issuer), + * for encrypted messages we always place signatures inside the encryption container (sign-then-encrypt)
  • + *
  • Prefix-signed messages (where a SignaturePacket is prefixed to an OpenPGP message) are + * inferior to One-Pass-Signed messages, so this stream cannot produce those.
  • + *
  • Messages using the Cleartext-Signature Framework are "different enough" to deserve their own + * message generator / stream.
  • + *
+ */ +public class OpenPGPMessageOutputStream + extends OutputStream +{ + // sink for the OpenPGP message + private final OutputStream baseOut; // non-null + + private final OutputStream armorOut; // nullable + private final OutputStream encodeOut; // non-null + private final OutputStream encryptOut; // nullable + private final OutputStream paddingOut; // nullable + private final OutputStream signOut; // nullable + private final OutputStream compressOut; // nullable + private final OutputStream literalOut; // non-null + + // pipe plaintext data into this stream + private final OutputStream plaintextOut; // non-null + + OpenPGPMessageOutputStream(OutputStream baseOut, Builder builder) + throws PGPException, IOException + { + this.baseOut = baseOut; + OutputStream innermostOut = baseOut; + + // ASCII ARMOR + if (builder.armorFactory != null) + { + armorOut = builder.armorFactory.get(innermostOut); + innermostOut = armorOut; + } + else + { + armorOut = null; + } + + // BCPG (decide packet length encoding format) + encodeOut = new BCPGOutputStream(innermostOut, PacketFormat.CURRENT); + innermostOut = encodeOut; + + // ENCRYPT + if (builder.encryptionStreamFactory != null) + { + encryptOut = builder.encryptionStreamFactory.get(innermostOut); + innermostOut = encryptOut; + } + else + { + encryptOut = null; + } + + // PADDING + if (builder.paddingStreamFactory != null) + { + paddingOut = builder.paddingStreamFactory.get(innermostOut); + innermostOut = paddingOut; + } + else + { + paddingOut = null; + } + + // SIGN + if (builder.signatureStreamFactory != null) + { + signOut = builder.signatureStreamFactory.get(innermostOut); + // signOut does not forward write() calls down, so we do *not* set innermostOut to it + } + else + { + signOut = null; + } + + // COMPRESS + if (builder.compressionStreamFactory != null) + { + compressOut = builder.compressionStreamFactory.get(innermostOut); + innermostOut = compressOut; + } + else + { + compressOut = null; + } + + // LITERAL DATA + if (builder.literalDataStreamFactory == null) + { + throw new PGPException("Missing instructions for LiteralData encoding."); + } + literalOut = builder.literalDataStreamFactory.get(innermostOut); + + if (signOut != null) + { + this.plaintextOut = new TeeOutputStream(literalOut, signOut); + } + else + { + this.plaintextOut = literalOut; + } + } + + @Override + public void write(int i) + throws IOException + { + plaintextOut.write(i); + } + + @Override + public void write(byte[] b) + throws IOException + { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) + throws IOException + { + plaintextOut.write(b, off, len); + } + + @Override + public void flush() + throws IOException + { + literalOut.flush(); + if (compressOut != null) + { + compressOut.flush(); + } + if (signOut != null) + { + signOut.flush(); + } + if (paddingOut != null) + { + paddingOut.flush(); + } + if (encryptOut != null) + { + encryptOut.flush(); + } + encodeOut.flush(); + if (armorOut != null) + { + armorOut.flush(); + } + baseOut.flush(); + } + + @Override + public void close() + throws IOException + { + literalOut.close(); + if (compressOut != null) + { + compressOut.close(); + } + if (signOut != null) + { + signOut.close(); + } + if (paddingOut != null) + { + paddingOut.close(); + } + if (encryptOut != null) + { + encryptOut.close(); + } + encodeOut.close(); + if (armorOut != null) + { + armorOut.close(); + } + baseOut.close(); + } + + /** + * Factory class for wrapping output streams. + */ + public interface OutputStreamFactory + { + /** + * Wrap the given base stream with another {@link OutputStream} and return the result. + * @param base base output stream + * @return wrapped output stream + * @throws PGPException if the wrapping stream cannot be instantiated + */ + OutputStream get(OutputStream base) throws PGPException, IOException; + } + + static Builder builder() + { + return new Builder(); + } + + /** + * Builder class for {@link OpenPGPMessageOutputStream} instances. + */ + static class Builder + { + private OpenPGPMessageGenerator.ArmoredOutputStreamFactory armorFactory; + private OutputStreamFactory paddingStreamFactory; + private OutputStreamFactory encryptionStreamFactory; + private OutputStreamFactory signatureStreamFactory; + private OutputStreamFactory compressionStreamFactory; + private OutputStreamFactory literalDataStreamFactory; + + /** + * Specify a factory for ASCII armoring the message. + * + * @param factory armor stream factory + * @return this + */ + public Builder armor(OpenPGPMessageGenerator.ArmoredOutputStreamFactory factory) + { + this.armorFactory = factory; + return this; + } + + /** + * Specify a factory for encrypting the message. + * + * @param factory encryption stream factory + * @return this + */ + public Builder encrypt(OutputStreamFactory factory) + { + this.encryptionStreamFactory = factory; + return this; + } + + /** + * Specify a factory for padding the message. + * + * @param factory padding stream factory + * @return this + */ + public Builder padding(OutputStreamFactory factory) + { + this.paddingStreamFactory = factory; + return this; + } + + /** + * Specify a factory for signing the message. + * + * @param factory signing stream factory + * @return this + */ + public Builder sign(OutputStreamFactory factory) + { + this.signatureStreamFactory = factory; + return this; + } + + /** + * Specify a factory for compressing the message. + * ' + * @param factory compression stream factory + * @return this + */ + public Builder compress(OutputStreamFactory factory) + { + this.compressionStreamFactory = factory; + return this; + } + + /** + * Specify a factory for literal-data-wrapping the message. + * + * @param factory literal data stream factory + * @return this + */ + public Builder literalData(OutputStreamFactory factory) + { + this.literalDataStreamFactory = factory; + return this; + } + + /** + * Construct the {@link OpenPGPMessageOutputStream} over the base output stream. + * + * @param baseOut base output stream + * @return OpenPGP message output stream + * @throws PGPException if a stream cannot be created + * @throws IOException if a signature cannot be generated + */ + public OpenPGPMessageOutputStream build(OutputStream baseOut) + throws PGPException, IOException + { + return new OpenPGPMessageOutputStream(baseOut, this); + } + } + + /** + * OutputStream which updates {@link PGPSignatureGenerator} instances with data that is written to it. + * Note: Data written to this stream will *NOT* be forwarded to the underlying {@link OutputStream}. + * Once {@link #close()} is called, it will generate {@link PGPSignature} objects from the generators and write + * those to the underlying {@link OutputStream}. + */ + static class SignatureGeneratorOutputStream + extends OutputStream + { + + private final OutputStream out; + private final Stack signatureGenerators; + + public SignatureGeneratorOutputStream(OutputStream out, Stack signatureGenerators) + { + this.out = out; + this.signatureGenerators = signatureGenerators; + } + + @Override + public void write(int i) + throws IOException + { + for (PGPSignatureGenerator sigGen : signatureGenerators) + { + sigGen.update((byte) i); + } + } + + @Override + public void write(byte[] b) + throws IOException + { + for (PGPSignatureGenerator sigGen : signatureGenerators) + { + sigGen.update(b); + } + } + + @Override + public void write(byte[] b, int off, int len) + throws IOException + { + for (PGPSignatureGenerator sigGen : signatureGenerators) + { + sigGen.update(b, off, len); + } + } + + @Override + public void close() + throws IOException + { + while (!signatureGenerators.isEmpty()) + { + PGPSignatureGenerator gen = signatureGenerators.pop(); + PGPSignature sig = null; + try + { + sig = gen.generate(); + } + catch (PGPException e) + { + throw new RuntimeException(e); + } + sig.encode(out); + } + } + } + + /** + * OutputStream which appends a {@link org.bouncycastle.bcpg.PaddingPacket} to the data + * once {@link #close()} is called. + */ + static class PaddingPacketAppenderOutputStream + extends OutputStream + { + private final OutputStream out; + private final PaddingPacketFactory packetFactory; + + public PaddingPacketAppenderOutputStream(OutputStream out, PaddingPacketFactory packetFactory) + { + this.out = out; + this.packetFactory = packetFactory; + } + + @Override + public void write(byte[] b) + throws IOException + { + out.write(b); + } + + @Override + public void write(byte[] b, int off, int len) + throws IOException + { + out.write(b, off, len); + } + + @Override + public void write(int i) + throws IOException + { + out.write(i); + } + + @Override + public void close() + throws IOException + { + packetFactory.providePaddingPacket().encode(out); + out.close(); + } + } + + /** + * Factory interface for creating PGPPadding objects. + */ + public interface PaddingPacketFactory + { + PGPPadding providePaddingPacket(); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageProcessor.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageProcessor.java new file mode 100644 index 0000000000..b5f0288f13 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageProcessor.java @@ -0,0 +1,500 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.InputStreamPacket; +import org.bouncycastle.openpgp.KeyIdentifier; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.util.Arrays; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class OpenPGPMessageProcessor +{ + private final OpenPGPImplementation implementation; + private final Configuration configuration; + + /** + * Create a new {@link OpenPGPMessageProcessor} using the default {@link OpenPGPImplementation}. + */ + public OpenPGPMessageProcessor() + { + this(OpenPGPImplementation.getInstance()); + } + + /** + * Create a new {@link OpenPGPMessageProcessor} using the given {@link OpenPGPImplementation}. + * + * @param implementation openpgp implementation + */ + public OpenPGPMessageProcessor(OpenPGPImplementation implementation) + { + this.implementation = implementation; + this.configuration = new Configuration(); + } + + /** + * Add an {@link OpenPGPCertificate} for signature verification. + * If the message contains any signatures, the provided certificate will be considered as a candidate to verify + * the signature. + * + * @param issuerCertificate OpenPGP certificate + * @return this + */ + public OpenPGPMessageProcessor addVerificationCertificate(OpenPGPCertificate issuerCertificate) + { + configuration.certificatePool.addItem(issuerCertificate); + return this; + } + + /** + * Add an {@link OpenPGPKey} as potential decryption key. + * If the message is encrypted for an {@link OpenPGPKey}, this key can be tried to decrypt the message. + * Keys added via this method will also be available for message decryption if the message was encrypted + * to an anonymous recipient (wildcard key-id / fingerprint). + * + * @param key OpenPGP key + * @return this + */ + public OpenPGPMessageProcessor addDecryptionKey(OpenPGPKey key) + { + configuration.keyPool.addItem(key); + return this; + } + + /** + * Add an {@link OpenPGPKey} as potential decryption key, along with a {@link KeyPassphraseProvider} dedicated + * to this key. + * If the message is encrypted for an {@link OpenPGPKey}, this key can be tried to decrypt the message. + * Keys added via this method will also be available for message decryption if the message was encrypted + * to an anonymous recipient (wildcard key-id / fingerprint). + * + * @param key OpenPGP key + * @return this + */ + public OpenPGPMessageProcessor addDecryptionKey(OpenPGPKey key, char[] passphrase) + { + configuration.keyPool.addItem(key); + configuration.keyPassphraseProvider.addPassphrase(key, passphrase); + return this; + } + + /** + * Add a passphrase for secret key decryption. + * If the corresponding {@link OpenPGPKey} which key this passphrase is for is known in advance, + * it is highly advised to call {@link #addDecryptionKey(OpenPGPKey, char[])} instead, due to performance reasons. + * + * @param passphrase key-passphrase + * @return this + */ + public OpenPGPMessageProcessor addDecryptionKeyPassphrase(char[] passphrase) + { + configuration.keyPassphraseProvider.addPassphrase(passphrase); + return this; + } + + /** + * Set a provider for dynamically requesting missing passphrases used to unlock encrypted + * {@link OpenPGPKey OpenPGPKeys}. + * This provider is called, if a key cannot be unlocked using any passphrase provided via + * {@link #addDecryptionKey(OpenPGPKey, char[])}. + * + * @param keyPassphraseProvider key passphrase provider + * @return this + */ + public OpenPGPMessageProcessor setMissingOpenPGPKeyPassphraseProvider( + KeyPassphraseProvider keyPassphraseProvider) + { + this.configuration.keyPassphraseProvider.setMissingPassphraseCallback(keyPassphraseProvider); + return this; + } + + /** + * Set a {@link OpenPGPKeyMaterialProvider.OpenPGPCertificateProvider} to allow dynamic requesting certificates + * for signature verification. + * This provider is called if the requested {@link OpenPGPCertificate} has not yet been added explicitly + * via {@link #addVerificationCertificate(OpenPGPCertificate)}. + * This allows lazily requesting verification certificates at runtime. + * + * @param certificateProvider provider for OpenPGP certificates + * @return this + */ + public OpenPGPMessageProcessor setMissingOpenPGPCertificateProvider( + OpenPGPKeyMaterialProvider.OpenPGPCertificateProvider certificateProvider) + { + configuration.certificatePool.setMissingItemCallback(certificateProvider); + return this; + } + + /** + * Set a provider for {@link OpenPGPKey OpenPGPKeys}, which can be used to decrypt encrypted messages. + * This provider is called if an {@link OpenPGPKey} required to decrypt the message has not yet been + * explicitly added via {@link #addDecryptionKey(OpenPGPKey)}. + * This allows lazily requesting decryption keys at runtime. + * + * @param keyProvider provider for OpenPGP keys + * @return this + */ + public OpenPGPMessageProcessor setMissingOpenPGPKeyProvider( + OpenPGPKeyMaterialProvider.OpenPGPKeyProvider keyProvider) + { + configuration.keyPool.setMissingItemCallback(keyProvider); + return this; + } + + /** + * Set a passphrase to decrypt a symmetrically encrypted OpenPGP message. + * + * @param messagePassphrase passphrase for message decryption + * @return this + */ + public OpenPGPMessageProcessor addMessagePassphrase(char[] messagePassphrase) + { + this.configuration.addMessagePassphrase(messagePassphrase); + return this; + } + + /** + * Set a {@link MissingPassphraseCallback} which will be invoked if the message is encrypted using a passphrase, + * but no working passphrase was provided. + * + * @param callback callback + * @return this + */ + public OpenPGPMessageProcessor setMissingMessagePassphraseCallback( + MissingPassphraseCallback callback) + { + this.configuration.missingMessagePassphraseCallback = callback; + return this; + } + + /** + * Set a {@link PGPSessionKey} with which an encrypted OpenPGP message can be decrypted without the need for + * using a private key or passphrase. + * Typically, this method can be used, if the {@link PGPSessionKey} of a message is already known (e.g. because + * the message has already been decrypted before). + * The benefit of this is, that public-key operations can be costly. + * + * @param sessionKey session key + * @return this + */ + public OpenPGPMessageProcessor setSessionKey(PGPSessionKey sessionKey) + { + configuration.sessionKey = sessionKey; + return this; + } + + /** + * Process an OpenPGP message. + * + * @param messageIn input stream of the OpenPGP message + * @return plaintext input stream + * @throws IOException + * @throws PGPException + */ + public OpenPGPMessageInputStream process(InputStream messageIn) + throws IOException, PGPException + { + // Remove potential ASCII armoring + InputStream packetInputStream = PGPUtil.getDecoderStream(messageIn); + + PGPObjectFactory objectFactory = implementation.pgpObjectFactory(packetInputStream); + OpenPGPMessageInputStream in = new OpenPGPMessageInputStream(objectFactory, this); + in.process(); + return in; + } + + /** + * Bundle together metadata about the decryption result. + * That includes the encrypted data packet itself, the passphrase or (sub-)key that was used to decrypt the + * session-key, the session-key itself and lastly the resulting decrypted packet input stream. + */ + static class Decrypted + { + final InputStream inputStream; + final PGPSessionKey sessionKey; + final InputStreamPacket dataPacket; + OpenPGPCertificate.OpenPGPComponentKey decryptionKey; + char[] decryptionPassphrase; + + public Decrypted(InputStreamPacket encryptedData, + PGPSessionKey decryptedSessionKey, + InputStream decryptedIn) + { + this.dataPacket = encryptedData; + this.sessionKey = decryptedSessionKey; + this.inputStream = decryptedIn; + } + } + + /** + * Decrypt an encrypted data packet by trying passphrases and/or decryption keys. + * + * @param encDataList encrypted data + * @return decrypted data + * @throws PGPException in case of an error + */ + Decrypted decrypt(PGPEncryptedDataList encDataList) + throws PGPException + { + // Since decryption using session key is the most "deliberate" and "specific", we'll try that first + if (configuration.sessionKey != null) + { + // decrypt with provided session key + SessionKeyDataDecryptorFactory decryptorFactory = + implementation.sessionKeyDataDecryptorFactory(configuration.sessionKey); + InputStream decryptedIn = encDataList.extractSessionKeyEncryptedData() + .getDataStream(decryptorFactory); + + return new Decrypted(encDataList.getEncryptedData(), configuration.sessionKey, decryptedIn); + } + + List skesks = skesks(encDataList); + List pkesks = pkesks(encDataList); + + PGPException exception = null; + + // If the user explicitly provided a message passphrase, we'll try that next + if (!skesks.isEmpty() && !configuration.messagePassphrases.isEmpty()) + { + for (PGPPBEEncryptedData skesk : skesks) + { + for (char[] passphrase : configuration.messagePassphrases) + { + try + { + // Extract message session key with passphrase + PBEDataDecryptorFactory passphraseDecryptorFactory = + implementation.pbeDataDecryptorFactory(passphrase); + PGPSessionKey decryptedSessionKey = skesk.getSessionKey(passphraseDecryptorFactory); + + // Decrypt the message with the decrypted session key + SessionKeyDataDecryptorFactory skDecryptorFactory = + implementation.sessionKeyDataDecryptorFactory(decryptedSessionKey); + InputStream decryptedIn = encDataList.extractSessionKeyEncryptedData() + .getDataStream(skDecryptorFactory); + + Decrypted decrypted = new Decrypted(encDataList.getEncryptedData(), decryptedSessionKey, decryptedIn); + decrypted.decryptionPassphrase = passphrase; + + return decrypted; + } + catch (PGPException e) + { + onException(e); + // cache first exception, then continue to try next skesk if present + exception = exception != null ? exception : e; + } + } + } + } + + // Then we'll try decryption using secret key(s) + for (PGPPublicKeyEncryptedData pkesk : pkesks) + { + KeyIdentifier identifier = pkesk.getKeyIdentifier(); + OpenPGPKey key = configuration.keyPool.provide(identifier); + if (key == null) + { + continue; + } + + OpenPGPKey.OpenPGPSecretKey decryptionKey = key.getSecretKeys().get(identifier); + if (decryptionKey == null) + { + continue; + } + + try + { + if (!decryptionKey.isEncryptionKey()) + { + throw new PGPException("Key is not an encryption key and can therefore not decrypt."); + } + + char[] keyPassphrase = configuration.keyPassphraseProvider.getKeyPassword(decryptionKey); + PGPPrivateKey privateKey = decryptionKey.unlock(keyPassphrase); + + // Decrypt the message session key using the private key + PublicKeyDataDecryptorFactory pkDecryptorFactory = + implementation.publicKeyDataDecryptorFactory(privateKey); + PGPSessionKey decryptedSessionKey = pkesk.getSessionKey(pkDecryptorFactory); + + // Decrypt the message using the decrypted session key + SessionKeyDataDecryptorFactory skDecryptorFactory = + implementation.sessionKeyDataDecryptorFactory(decryptedSessionKey); + InputStream decryptedIn = encDataList.extractSessionKeyEncryptedData() + .getDataStream(skDecryptorFactory); + Decrypted decrypted = new Decrypted(encDataList.getEncryptedData(), decryptedSessionKey, decryptedIn); + decrypted.decryptionKey = decryptionKey; + return decrypted; + } + catch (PGPException e) + { + onException(e); + } + } + + // And lastly, we'll prompt the user dynamically for a message passphrase + if (!skesks.isEmpty() && configuration.missingMessagePassphraseCallback != null) + { + char[] passphrase; + while ((passphrase = configuration.missingMessagePassphraseCallback.getPassphrase()) != null) + { + for (PGPPBEEncryptedData skesk : skesks) + { + try + { + // Decrypt the message session key using a passphrase + PBEDataDecryptorFactory passphraseDecryptorFactory = implementation.pbeDataDecryptorFactory(passphrase); + PGPSessionKey decryptedSessionKey = skesk.getSessionKey(passphraseDecryptorFactory); + + // Decrypt the data using the decrypted session key + SessionKeyDataDecryptorFactory skDecryptorFactory = implementation.sessionKeyDataDecryptorFactory(decryptedSessionKey); + InputStream decryptedIn = encDataList.extractSessionKeyEncryptedData().getDataStream(skDecryptorFactory); + Decrypted decrypted = new Decrypted(encDataList.getEncryptedData(), decryptedSessionKey, decryptedIn); + decrypted.decryptionPassphrase = passphrase; + return decrypted; + } + catch (PGPException e) + { + onException(e); + // cache first exception, then continue to try next skesk if present + exception = exception != null ? exception : e; + } + } + } + + if (exception != null) + { + throw exception; + } + } + + throw new PGPException("No working decryption method found."); + } + + /** + * Return all symmetric-key-encrypted-session-key (SKESK) packets leading the encrypted data packet. + * + * @param encDataList encrypted data list + * @return list of skesk packets (might be empty) + */ + private List skesks(PGPEncryptedDataList encDataList) + { + List list = new ArrayList<>(); + for (PGPEncryptedData encData : encDataList) + { + if (encData instanceof PGPPBEEncryptedData) + { + list.add((PGPPBEEncryptedData) encData); + } + } + return list; + } + + /** + * Return all public-key-encrypted-session-key (PKESK) packets leading the encrypted data packet. + * + * @param encDataList encrypted data list + * @return list of pkesk packets (might be empty) + */ + private List pkesks(PGPEncryptedDataList encDataList) + { + List list = new ArrayList<>(); + for (PGPEncryptedData encData : encDataList) + { + if (encData instanceof PGPPublicKeyEncryptedData) + { + list.add((PGPPublicKeyEncryptedData) encData); + } + } + return list; + } + + OpenPGPCertificate provideCertificate(KeyIdentifier identifier) + { + return configuration.certificatePool.provide(identifier); + } + + OpenPGPImplementation getImplementation() + { + return implementation; + } + + /** + * Method that can be called if a {@link PGPException} is thrown. + * If the user provided a {@link PGPExceptionCallback} ({@link Configuration#exceptionCallback} is not null), + * the exception will be passed along to that callback. + * Otherwise, nothing happens. + * + * @param e exception + */ + void onException(PGPException e) + { + if (configuration.exceptionCallback != null) + { + configuration.exceptionCallback.onException(e); + } + } + + public static class Configuration + { + private final OpenPGPKeyMaterialPool.OpenPGPCertificatePool certificatePool; + private final OpenPGPKeyMaterialPool.OpenPGPKeyPool keyPool; + private final KeyPassphraseProvider.DefaultKeyPassphraseProvider keyPassphraseProvider; + public final List messagePassphrases = new ArrayList<>(); + private MissingPassphraseCallback missingMessagePassphraseCallback; + private PGPExceptionCallback exceptionCallback = null; + private PGPSessionKey sessionKey; + + public Configuration() + { + this.certificatePool = new OpenPGPKeyMaterialPool.OpenPGPCertificatePool(); + this.keyPool = new OpenPGPKeyMaterialPool.OpenPGPKeyPool(); + this.keyPassphraseProvider = new KeyPassphraseProvider.DefaultKeyPassphraseProvider(); + } + + /** + * Add a passphrase that will be tried when a symmetric-key-encrypted-session-key packet is found + * during the decryption process. + * + * @param messagePassphrase passphrase to decrypt the message with + * @return this + */ + public Configuration addMessagePassphrase(char[] messagePassphrase) + { + boolean found = false; + for (char[] existing : messagePassphrases) + { + found |= Arrays.areEqual(existing, messagePassphrase); + } + + if (!found) + { + messagePassphrases.add(messagePassphrase); + } + return this; + } + } + + /** + * Callback to handle {@link PGPException PGPExceptions}. + */ + public interface PGPExceptionCallback + { + void onException(PGPException e); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPNotationRegistry.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPNotationRegistry.java new file mode 100644 index 0000000000..fa9f0406af --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPNotationRegistry.java @@ -0,0 +1,19 @@ +package org.bouncycastle.openpgp.api; + +import java.util.HashSet; +import java.util.Set; + +public class OpenPGPNotationRegistry +{ + private final Set knownNotations = new HashSet<>(); + + public boolean isNotationKnown(String notationName) + { + return knownNotations.contains(notationName); + } + + public void addKnownNotation(String notationName) + { + this.knownNotations.add(notationName); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPSignature.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPSignature.java new file mode 100644 index 0000000000..c6c1cbb19f --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPSignature.java @@ -0,0 +1,535 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.openpgp.KeyIdentifier; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.bouncycastle.openpgp.api.exception.MalformedPGPSignatureException; +import org.bouncycastle.openpgp.api.util.UTCUtil; +import org.bouncycastle.util.encoders.Hex; + +import java.util.Date; +import java.util.List; + +/** + * An OpenPGP signature. + * This is a wrapper around {@link PGPSignature} which tracks the verification state of the signature. + */ +public abstract class OpenPGPSignature +{ + protected final PGPSignature signature; + protected final OpenPGPCertificate.OpenPGPComponentKey issuer; + protected boolean isTested = false; + protected boolean isCorrect = false; + + /** + * Create an {@link OpenPGPSignature}. + * + * @param signature signature + * @param issuer issuer subkey + */ + public OpenPGPSignature(PGPSignature signature, OpenPGPCertificate.OpenPGPComponentKey issuer) + { + this.signature = signature; + this.issuer = issuer; + } + + /** + * Return the {@link PGPSignature}. + * + * @return signature + */ + public PGPSignature getSignature() + { + return signature; + } + + /** + * Return the {@link OpenPGPCertificate.OpenPGPComponentKey} subkey that issued this signature. + * This method might return null, if the issuer certificate is not available. + * + * @return issuer subkey or null + */ + public OpenPGPCertificate.OpenPGPComponentKey getIssuer() + { + return issuer; + } + + /** + * Return the {@link OpenPGPCertificate} that contains the subkey that issued this signature. + * This method might return null if the issuer certificate is not available + * + * @return issuer certificate or null + */ + public OpenPGPCertificate getIssuerCertificate() + { + return issuer != null ? issuer.getCertificate() : null; + } + + /** + * Return a {@link List} of possible {@link KeyIdentifier} candidates. + * + * @return key identifier candidates + */ + public List getKeyIdentifiers() + { + return signature.getKeyIdentifiers(); + } + + /** + * Return the most expressive {@link KeyIdentifier} from available candidates. + * + * @return most expressive key identifier + */ + public KeyIdentifier getKeyIdentifier() + { + List identifiers = getKeyIdentifiers(); + return getMostExpressiveIdentifier(identifiers); + } + + /** + * Return the most expressive issuer {@link KeyIdentifier}. + * Due to historic reasons, signatures MAY contain more than one issuer packet, which might contain inconsistent + * information (issuer key-ids / issuer fingerprints). + * Throw wildcards (anonymous issuers) into the mix, and it becomes apparent, that there needs to be a way to + * select the "best" issuer identifier. + * If there are more than one issuer packet, this method returns the most expressive (prefer fingerprints over + * key-ids, prefer non-wildcard over wildcard) and returns that. + * + * @param identifiers list of available identifiers + * @return the best identifier + */ + public static KeyIdentifier getMostExpressiveIdentifier(List identifiers) + { + if (identifiers.isEmpty()) + { + // none + return null; + } + if (identifiers.size() == 1) + { + // single + return identifiers.get(0); + } + + // Find most expressive identifier + for (KeyIdentifier identifier : identifiers) + { + // non-wildcard and has fingerprint + if (!identifier.isWildcard() && identifier.getFingerprint() != null) + { + return identifier; + } + } + + // Find non-wildcard identifier + for (KeyIdentifier identifier : identifiers) + { + // non-wildcard (and no fingerprint) + if (!identifier.isWildcard()) + { + return identifier; + } + } + // else return first identifier + return identifiers.get(0); + } + + /** + * Return true, if this signature has been tested and is correct. + * + * @return true if the signature is tested and is correct, false otherwise + */ + public boolean isTestedCorrect() + { + return isTested && isCorrect; + } + + /** + * Return the creation time of the signature. + * + * @return signature creation time + */ + public Date getCreationTime() + { + return signature.getCreationTime(); + } + + /** + * Return the expiration time of the signature. + * If no expiration time was included (or if the signature was explicitly marked as non-expiring), + * return null, otherwise return the time of expiration. + * The signature is no longer valid, once the expiration time is exceeded. + * + * @return expiration time + */ + public Date getExpirationTime() + { + PGPSignatureSubpacketVector hashed = signature.getHashedSubPackets(); + if (hashed == null) + { + // v3 sigs have no expiration + return null; + } + long exp = hashed.getSignatureExpirationTime(); + if (exp < 0) + { + throw new RuntimeException("Negative expiration time"); + } + + if (exp == 0L) + { + // Explicit or implicit no expiration + return null; + } + + return new Date(getCreationTime().getTime() + 1000 * exp); + } + + /** + * Return true, if the signature is not a hard revocation, and if the evaluation time falls into the period + * between signature creation time and expiration or revocation. + * + * @param evaluationTime time for which you want to determine effectiveness of the signature + * @return true if the signature is effective at the given evaluation time + */ + public boolean isEffectiveAt(Date evaluationTime) + { + if (isHardRevocation()) + { + // hard revocation is valid at all times + return true; + } + + // creation <= eval < expiration + Date creation = getCreationTime(); + Date expiration = getExpirationTime(); + return !evaluationTime.before(creation) && (expiration == null || evaluationTime.before(expiration)); + } + + /** + * Return true, if this signature is a hard revocation. + * Contrary to soft revocations (the key / signature / user-id was gracefully retired), a hard revocation + * has a serious reason, like key compromise, or no reason at all. + * Hard revocations invalidate the key / signature / user-id retroactively, while soft revocations only + * invalidate from the time of revocation signature creation onwards. + * + * @return true if the signature is a hard revocation + */ + public boolean isHardRevocation() + { + return signature.isHardRevocation(); + } + + /** + * Return true, if this signature is a certification. + * Certification signatures are used to bind user-ids to a key. + * + * @return true if the signature is a certification + */ + public boolean isCertification() + { + return signature.isCertification(); + } + + + /** + * Check certain requirements for OpenPGP signatures. + * + * @param issuer signature issuer + * @throws MalformedPGPSignatureException if the signature is malformed + */ + void sanitize(OpenPGPCertificate.OpenPGPComponentKey issuer) + throws MalformedPGPSignatureException + { + PGPSignatureSubpacketVector hashed = signature.getHashedSubPackets(); + if (hashed == null) + { + throw new MalformedPGPSignatureException("Missing hashed signature subpacket area."); + } + PGPSignatureSubpacketVector unhashed = signature.getUnhashedSubPackets(); + + if (hashed.getSignatureCreationTime() == null) + { + // Signatures MUST have hashed creation time subpacket + throw new MalformedPGPSignatureException("Signature does not have a hashed SignatureCreationTime subpacket."); + } + + if (hashed.getSignatureCreationTime().before(issuer.getCreationTime())) + { + throw new MalformedPGPSignatureException("Signature predates issuer key creation time."); + } + + for (NotationData notation : hashed.getNotationDataOccurrences()) + { + if (notation.isCritical()) + { + throw new MalformedPGPSignatureException("Critical unknown NotationData encountered: " + notation.getNotationName()); + } + } + + for (SignatureSubpacket unknownSubpacket : hashed.toArray()) + { + // SignatureSubpacketInputStream returns unknown subpackets as SignatureSubpacket + if (unknownSubpacket.isCritical() && + unknownSubpacket.getClass().equals(SignatureSubpacket.class)) + { + throw new MalformedPGPSignatureException("Critical hashed unknown SignatureSubpacket encountered: " + + unknownSubpacket.getType()); + } + } + + switch (signature.getVersion()) + { + case SignaturePacket.VERSION_4: + if (hashed.getIssuerFingerprint() == null && + unhashed.getIssuerFingerprint() == null && + hashed.getSubpacket(SignatureSubpacketTags.ISSUER_KEY_ID) == null && + unhashed.getSubpacket(SignatureSubpacketTags.ISSUER_KEY_ID) == null) + { + throw new MalformedPGPSignatureException("Missing IssuerKeyID and IssuerFingerprint subpacket."); + } + break; + + case SignaturePacket.VERSION_5: + // TODO: Implement + break; + + case SignaturePacket.VERSION_6: + if (hashed.getSubpacket(SignatureSubpacketTags.ISSUER_KEY_ID) != null) + { + throw new MalformedPGPSignatureException("v6 signature MUST NOT contain IssuerKeyID subpacket."); + } + if (hashed.getIssuerFingerprint() == null && unhashed.getIssuerFingerprint() == null) + { + throw new MalformedPGPSignatureException("v6 signature MUST contain IssuerFingerprint subpacket."); + } + break; + + default: + } + } + + /** + * Return true, if this signature is a revocation, false otherwise. + * @return true if signature is revocation + */ + public boolean isRevocation() + { + return PGPSignature.isRevocation(signature.getSignatureType()); + } + + @Override + public String toString() + { + String issuerInfo = getIssuerDisplay(); + String period = UTCUtil.format(getCreationTime()) + + (getExpirationTime() == null ? "" : ">" + UTCUtil.format(getExpirationTime())); + String validity = isTested ? (isCorrect ? "✓" : "✗") : "❓"; + // -DM Hex.toHexString + return getType() + (signature.isHardRevocation() ? "(hard)" : "") + " " + Hex.toHexString(signature.getDigestPrefix()) + + " " + issuerInfo + " -> " + getTargetDisplay() + " (" + period + ") " + validity; + } + + protected String getIssuerDisplay() + { + if (issuer != null) + { + return issuer.toString(); + } + + KeyIdentifier issuerIdentifier = getKeyIdentifier(); + if (issuerIdentifier == null) + { + return "External[unknown]"; + } + + if (issuerIdentifier.isWildcard()) + { + return "Anonymous"; + } + return "External[" + Long.toHexString(issuerIdentifier.getKeyId()).toUpperCase() + "]"; + } + + protected abstract String getTargetDisplay(); + + protected String getType() + { + switch (signature.getSignatureType()) + { + case PGPSignature.BINARY_DOCUMENT: + return "BINARY_DOCUMENT"; + case PGPSignature.CANONICAL_TEXT_DOCUMENT: + return "CANONICAL_TEXT_DOCUMENT"; + case PGPSignature.STAND_ALONE: + return "STANDALONE"; + case PGPSignature.DEFAULT_CERTIFICATION: + return "DEFAULT_CERTIFICATION"; + case PGPSignature.NO_CERTIFICATION: + return "NO_CERTIFICATION"; + case PGPSignature.CASUAL_CERTIFICATION: + return "CASUAL_CERTIFICATION"; + case PGPSignature.POSITIVE_CERTIFICATION: + return "POSITIVE_CERTIFICATION"; + case PGPSignature.SUBKEY_BINDING: + return "SUBKEY_BINDING"; + case PGPSignature.PRIMARYKEY_BINDING: + return "PRIMARYKEY_BINDING"; + case PGPSignature.DIRECT_KEY: + return "DIRECT_KEY"; + case PGPSignature.KEY_REVOCATION: + return "KEY_REVOCATION"; + case PGPSignature.SUBKEY_REVOCATION: + return "SUBKEY_REVOCATION"; + case PGPSignature.CERTIFICATION_REVOCATION: + return "CERTIFICATION_REVOCATION"; + case PGPSignature.TIMESTAMP: + return "TIMESTAMP"; + case PGPSignature.THIRD_PARTY_CONFIRMATION: + return "THIRD_PARTY_CONFIRMATION"; + default: + return "UNKNOWN (" + signature.getSignatureType() + ")"; + } + } + + /** + * An {@link OpenPGPSignature} made over a binary or textual document (e.g. a message). + * Also known as a Data Signature. + * An {@link OpenPGPDocumentSignature} CANNOT live on a {@link OpenPGPCertificate}. + */ + public static class OpenPGPDocumentSignature + extends OpenPGPSignature + { + protected final OpenPGPDocumentSignature attestedSignature; + + /** + * Create a document signature of level 0 (signature is made directly over the document). + * + * @param signature signature + * @param issuer public issuer-signing-key-component (or null if not available) + */ + public OpenPGPDocumentSignature(PGPSignature signature, OpenPGPCertificate.OpenPGPComponentKey issuer) + { + super(signature, issuer); + this.attestedSignature = null; + } + + @Override + protected String getTargetDisplay() + { + return ""; + } + + /** + * Create a document signature of level greater than 0 (signature is made as an attestation over + * other signature(s) + document). + * If the attested signature is itself an attestation, it will recursively contain its attested signature. + * + * @param signature attestation signature + * @param issuer public issuer signing-key-component (or null if not available) + * @param attestedSignature the attested signature + */ + public OpenPGPDocumentSignature(PGPSignature signature, + OpenPGPCertificate.OpenPGPComponentKey issuer, + OpenPGPDocumentSignature attestedSignature) + { + super(signature, issuer); + this.attestedSignature = attestedSignature; + } + + /** + * Return the signature attestation level of this signature. + * If this signature was created directly over a document, this method returns 0. + * A level greater than 0 indicates that the signature is an attestation over at least one other signature. + * + * @return signature attestation level + */ + public int getSignatureLevel() + { + if (attestedSignature == null) + { + return 0; // signature over data + } + else + { + return 1 + attestedSignature.getSignatureLevel(); + } + } + + /** + * Return the attested signature (or null if this is not an attestation signature). + * + * @return attested signature or null + */ + public OpenPGPDocumentSignature getAttestedSignature() + { + return attestedSignature; + } + + /** + * Verify the correctness of an inline signature by evaluating the corresponding {@link PGPOnePassSignature}. + * + * @param ops one-pass-signature packet + * @return true if the signature is correct, false otherwise + * @throws PGPException if the signature cannot be verified + */ + public boolean verify(PGPOnePassSignature ops) + throws PGPException + { + isTested = true; + isCorrect = ops.verify(signature); + return isCorrect; + } + + /** + * Verify the correctness of a prefixed-signature. + * + * @return true if the signature is correct, false otherwise + * @throws PGPException if the signature cannot be verified + */ + public boolean verify() + throws PGPException + { + isTested = true; + isCorrect = signature.verify(); + return isCorrect; + } + + /** + * Return true, if the signature is valid at this moment. + * A valid signature is effective, correct and was issued by a valid signing key. + * + * @return true if the signature is valid now. + */ + public boolean isValid() + { + return isValidAt(getCreationTime()); + } + + /** + * Return true, if th signature is valid at the given date. + * A valid signature is effective, correct and was issued by a valid signing key. + * + * @param date evaluation time + * @return true if the signature is valid at the given date + * @throws IllegalStateException if the signature has not yet been tested using a
verify()
method. + */ + public boolean isValidAt(Date date) + { + if (!isTested) + { + throw new IllegalStateException("Signature has not yet been verified."); + } + if (!isTestedCorrect()) + { + return false; + } + return issuer.isSigningKey(date); + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/RetainingInputStream.java b/pg/src/main/java/org/bouncycastle/openpgp/api/RetainingInputStream.java new file mode 100644 index 0000000000..675b6932f4 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/RetainingInputStream.java @@ -0,0 +1,107 @@ +package org.bouncycastle.openpgp.api; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Implementation of {@link InputStream} that withholds a number of bytes from the end of the original + * message until the message has been processed entirely. + * Furthermore, upon reaching the end of the underlying data stream, the underlying data stream is + * automatically closed. + * This is done in order to minimize the risk of emitting unauthenticated plaintext, while at the same + * time being somewhat resource-efficient. + * The number of bytes to withhold can be configured ({@link #CIRCULAR_BUFFER_SIZE} by default). + */ +public class RetainingInputStream + extends InputStream +{ + private static final int CIRCULAR_BUFFER_SIZE = 1024 * 1024 * 32; // 32 MiB + + private final byte[] circularBuffer; + private int lastWrittenPos = 0; + private int bufReadPos = 0; + private final I in; + private boolean closed = false; + + public RetainingInputStream(I in) + { + this(in, CIRCULAR_BUFFER_SIZE); + } + + public RetainingInputStream(I in, int bufferSize) + { + if (bufferSize <= 0) + { + throw new IllegalArgumentException("Buffer size cannot be null nor negative."); + } + this.circularBuffer = new byte[bufferSize]; + this.in = in; + } + + public I getInputStream() + { + return in; + } + + private void fill() + throws IOException + { + if (closed) + { + return; + } + + // readerPos - 1 % buf.len + int lastAvailPos = (circularBuffer.length + bufReadPos - 1) % circularBuffer.length; + int read; + if (lastWrittenPos < lastAvailPos) + { + read = in.read(circularBuffer, lastWrittenPos, lastAvailPos - lastWrittenPos); + } + else + { + read = in.read(circularBuffer, lastWrittenPos, circularBuffer.length - lastWrittenPos); + if (read >= 0) + { + lastWrittenPos += read; + } + read = in.read(circularBuffer, 0, lastAvailPos); + } + + if (read >= 0) + { + lastWrittenPos += read; + } + else + { + close(); + } + + lastWrittenPos %= circularBuffer.length; + } + + @Override + public void close() + throws IOException + { + if (!closed) + { + closed = true; + in.close(); + } + } + + @Override + public int read() + throws IOException + { + fill(); + if (bufReadPos == lastWrittenPos) + { + return -1; + } + int i = circularBuffer[bufReadPos++]; + bufReadPos %= circularBuffer.length; + return i; + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/exception/IncorrectPGPSignatureException.java b/pg/src/main/java/org/bouncycastle/openpgp/api/exception/IncorrectPGPSignatureException.java new file mode 100644 index 0000000000..8d7bd5c601 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/exception/IncorrectPGPSignatureException.java @@ -0,0 +1,15 @@ +package org.bouncycastle.openpgp.api.exception; + +import org.bouncycastle.openpgp.PGPSignatureException; + +/** + * An OpenPGP signature is not correct. + */ +public class IncorrectPGPSignatureException + extends PGPSignatureException +{ + public IncorrectPGPSignatureException(String message) + { + super(message); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/exception/MalformedPGPSignatureException.java b/pg/src/main/java/org/bouncycastle/openpgp/api/exception/MalformedPGPSignatureException.java new file mode 100644 index 0000000000..f83695359d --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/exception/MalformedPGPSignatureException.java @@ -0,0 +1,16 @@ +package org.bouncycastle.openpgp.api.exception; + +import org.bouncycastle.openpgp.PGPSignatureException; + +/** + * An OpenPGP Signature is malformed (missing required subpackets, etc.). + */ +public class MalformedPGPSignatureException + extends PGPSignatureException +{ + + public MalformedPGPSignatureException(String message) + { + super(message); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/exception/MissingIssuerCertException.java b/pg/src/main/java/org/bouncycastle/openpgp/api/exception/MissingIssuerCertException.java new file mode 100644 index 0000000000..ded366a9ef --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/exception/MissingIssuerCertException.java @@ -0,0 +1,15 @@ +package org.bouncycastle.openpgp.api.exception; + +import org.bouncycastle.openpgp.PGPSignatureException; + +/** + * The OpenPGP certificate (public key) required to verify a signature is not available. + */ +public class MissingIssuerCertException + extends PGPSignatureException +{ + public MissingIssuerCertException(String message) + { + super(message); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/util/DebugPrinter.java b/pg/src/main/java/org/bouncycastle/openpgp/api/util/DebugPrinter.java new file mode 100644 index 0000000000..7ef3b9fc94 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/util/DebugPrinter.java @@ -0,0 +1,203 @@ +package org.bouncycastle.openpgp.api.util; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; + +import java.io.IOException; +import java.util.Date; + +public class DebugPrinter +{ + + private static final String hardRevokedPrimaryKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xsBNBFpJegABCACzr1V+GxVkrtfDjihYK+HtyEIcO52uw7O2kd7JbduYp4RK17jy\n" + + "75N3EnsgmiIkSxXCWr+rTtonNs1zCJeUa/gwnNfs7mVgjL2rMOZU/KZ4MP0yOYU5\n" + + "u5FjNPWz8hpFQ9GKqfdj0Op61h1pCQO45IjUQ3dCDj9Rfn44zHMB1ZrbmIH9nTR1\n" + + "YIGHWmdm0LItb2WxIkwzWBAJ5acTlsmLyZZEQ1+8NDqktyzwFoQqTJvLU4StY2k6\n" + + "h18ZKZdPyrdLoEyOuWkvjxmbhDk1Gt5KiS/yy7mrzIPLr0dmJe4vc8WLV+bXoyNE\n" + + "x3H8o9CFcYehLfyqsy40lg92d6Kp96ww8dZ5ABEBAAHCwN8EIAEKAJMFglwqrYAJ\n" + + "EAitUcrkcPAGRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y\n" + + "ZzUC0OZfTpIdwlwf0ObCTwna1jQBSX993ccnmOrNte5LIx0CS2V5IG1hdGVyaWFs\n" + + "IGhhcyBiZWVuIGNvbXByb21pc2VkFiEE4yy22oICkbfnbbGoCK1RyuRw8AYAAJA5\n" + + "CACTlymVijD9/t/SUBh3QihI9xjk+l2dGcFN64qkYEoplAJKedpO3z9niE9ejByF\n" + + "4tqn5BklxUGaRjq3Sgy0EQAi/nkgSq0cQX/aG2UoIs+OYbqzSktZAXIPUiQI5Ir5\n" + + "OYyALBJo03TxHHMOIBrLERVJiDGGoFNY58jQ7kUD6/XtRvpXNuQnfpRH4sAX+VQo\n" + + "fC5WojyWsiIv1aXwOJOA1IXSCHmK7lFuWVyZ6f/SGYpMnIROE1hzaRAVaaMhjcw1\n" + + "2gr5fKi/3Sd2agzwLbLfqvvYD9BI4yKkysTMp6t2ZbwcpvlWp/8Yu1Zrmf5moLJY\n" + + "6BveLKJdm/Th6Tik4dDP/WvCwsDEBB8BCgB4BYJeC+EACRAIrVHK5HDwBkcUAAAA\n" + + "AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeHLGXtWodbY9gI8X3Q\n" + + "zLB9sL0hMGY2/+9yAip5uwckkAIVCgKbAwIeARYhBOMsttqCApG3522xqAitUcrk\n" + + "cPAGAABmBQgAoipwm9jQWWvyY9WiXuEdq8T2Y9hEV1nt2ySjTyk+ytK1Q5E8NSUY\n" + + "k3wrLgGNpWPbCiXYUGZfms15uuL703OoRBkUP/l7LA5RNgyJ/At+Bw3OPeWZ68hz\n" + + "QfA3eZdR3Y6sXxiGOhwTyVHcdHXncD+NjorIPbeSrAvM5Xf/jCEYM5Kfg4NC1yVZ\n" + + "w7sFhD6KNjeloQK+UXi718QC1+YbfS295T9AwEmbwCsvQTv8EQq9veCfHYPwqMAH\n" + + "5aMn9CqPiY8o2p5mZ92nMuQhpFTdpnPjxVHpBmQw8uaKGJIFzvwpgKbkzb2m3Lfg\n" + + "OyFVXVljOUlm/dCb2lfUlo4up0KYVZu0rcLAxAQfAQoAeAWCWkl6AAkQCK1RyuRw\n" + + "8AZHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn1WXYy2Gc\n" + + "Q19ob8t2hq7BOItGrywzM393vZFR5mg+jwICFQoCmwMCHgEWIQTjLLbaggKRt+dt\n" + + "sagIrVHK5HDwBgAAUGMIAK3yEcaveZ6VCgDj17NuZ2Zb8vsUG65rE8R4QGFvHhhX\n" + + "M/NkMLpqKq0fFX66I8TPngmXUyPOZzOZM852A1NvnDIbGVZuflYRmct3t0B+CfxN\n" + + "9Q+7daKQr4+YNXkSeC4MsAfnGBnGQWKf20E/UlGLoWR9jlwkdOKkm6VVAiAKZ4QR\n" + + "8SjbTpaowJB3mjnVv/F3j7G3767VTixmIK2V32Ozast/ls23ZvFL1TxVx/rhxM04\n" + + "Mr2G5yQWJIzkZgqlCrPOtDy/HpHoPrC+Dx0kY9VFH8HEA+eatJt1bXsNioiFIuMC\n" + + "ouS3Hg7aa46DubrVP9WHxAIjTHkkB1yqvN3aWs7461LNEmp1bGlldEBleGFtcGxl\n" + + "Lm9yZ8LAxAQTAQoAeAWCWkl6AAkQCK1RyuRw8AZHFAAAAAAAHgAgc2FsdEBub3Rh\n" + + "dGlvbnMuc2VxdW9pYS1wZ3Aub3JnOkYsewniH1sJ2kI5N2wa5AImO40vTfrIbkXR\n" + + "2dICirICFQoCmwMCHgEWIQTjLLbaggKRt+dtsagIrVHK5HDwBgAAn/UIALMbXwG8\n" + + "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\n" + + "=x/EN\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + private static final String v6SecretKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB\n" + + "exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ\n" + + "BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh\n" + + "RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe\n" + + "7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/\n" + + "LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG\n" + + "GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE\n" + + "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr\n" + + "k0mXubZvyl4GBg==\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static void main(String[] args) + throws IOException + { + + OpenPGPCertificate certificate = OpenPGPCertificate.fromAsciiArmor(v6SecretKey); + // -DM System.out.println + System.out.println(toString(certificate, new Date())); + } + + public static String toString(OpenPGPCertificate certificate, Date evaluationTime) + { + StringBuilder sb = new StringBuilder(); + for (OpenPGPCertificate.OpenPGPCertificateComponent component : certificate.getComponents()) + { + if (component.isBoundAt(evaluationTime)) + { + green(sb, component.toDetailString()).append("\n"); + } + else + { + red(sb, component.toDetailString()).append("\n"); + } + + OpenPGPCertificate.OpenPGPSignatureChains chains = component.getSignatureChains(); + for (OpenPGPCertificate.OpenPGPSignatureChain chain : chains) + { + boolean revocation = chain.isRevocation(); + boolean isHardRevocation = chain.isHardRevocation(); + String indent = ""; + for (OpenPGPCertificate.OpenPGPSignatureChain.Link link : chain) + { + indent = indent + " "; + sb.append(indent); + try + { + link.verify(new BcPGPContentVerifierBuilderProvider()); + if (revocation) + { + if (isHardRevocation) + { + red(sb, link.toString()).append("\n"); + } + else + { + yellow(sb, link.toString()).append("\n"); + } + } + else + { + green(sb, link.toString()).append("\n"); + } + } + catch (PGPException e) + { + red(sb, link.toString()).append("\n"); + } + } + } + } + + return sb.toString(); + } + + private static StringBuilder red(StringBuilder sb, String text) + { + return sb.append("\033[31m").append(text).append("\033[0m"); + } + + private static StringBuilder redBg(StringBuilder sb, String text) + { + return sb.append("\033[41m").append(text).append("\033[0m"); + } + + private static StringBuilder green(StringBuilder sb, String text) + { + return sb.append("\033[32m").append(text).append("\033[0m"); + } + + private static StringBuilder greenBg(StringBuilder sb, String text) + { + return sb.append("\033[42m").append(text).append("\033[0m"); + } + + private static StringBuilder yellow(StringBuilder sb, String text) + { + return sb.append("\033[33m").append(text).append("\033[0m"); + } + + private static StringBuilder yellowBg(StringBuilder sb, String text) + { + return sb.append("\033[43m").append(text).append("\033[0m"); + } + +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/util/UTCUtil.java b/pg/src/main/java/org/bouncycastle/openpgp/api/util/UTCUtil.java new file mode 100644 index 0000000000..d2013cc6e5 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/util/UTCUtil.java @@ -0,0 +1,48 @@ +package org.bouncycastle.openpgp.api.util; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +public class UTCUtil +{ + private static SimpleDateFormat utc() + { + // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every invocation. + // See https://stackoverflow.com/a/6840856/11150851 + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format; + } + + /** + * Format a {@link Date} as UTC timestamp. + * + * @param timestamp date + * @return formatted timestamp + */ + public static String format(Date timestamp) + { + return utc().format(timestamp); + } + + /** + * Parse a UTC timestamp. + * The timestamp needs to be provided in the form 'yyyy-MM-dd HH:mm:ss z'. + * + * @param utcTimestamp timestamp + * @return date + */ + public static Date parse(String utcTimestamp) + { + try + { + return utc().parse(utcTimestamp); + } + catch (ParseException e) + { + throw new IllegalArgumentException("Malformed UTC timestamp: " + utcTimestamp, e); + } + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/OpenPGPTestKeys.java b/pg/src/test/java/org/bouncycastle/openpgp/OpenPGPTestKeys.java new file mode 100644 index 0000000000..28c19bd875 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/OpenPGPTestKeys.java @@ -0,0 +1,453 @@ +package org.bouncycastle.openpgp; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class OpenPGPTestKeys +{ + /** + * Alice's Ed25519 OpenPGP key. + * + * @see + * Alice's OpenPGP Secret Key Material + */ + public static final String ALICE_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Alice's OpenPGP Transferable Secret Key\n" + + "Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n" + + "\n" + + "lFgEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U\n" + + "b7O1u10AAP9XBeW6lzGOLx7zHH9AsUDUTb2pggYGMzd0P3ulJ2AfvQ4RtCZBbGlj\n" + + "ZSBMb3ZlbGFjZSA8YWxpY2VAb3BlbnBncC5leGFtcGxlPoiQBBMWCAA4AhsDBQsJ\n" + + "CAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE64W7X6M6deFelE5j8jFVDE9H444FAl2l\n" + + "nzoACgkQ8jFVDE9H447pKwD6A5xwUqIDprBzrHfahrImaYEZzncqb25vkLV2arYf\n" + + "a78A/R3AwtLQvjxwLDuzk4dUtUwvUYibL2sAHwj2kGaHnfICnF0EXEcE6RIKKwYB\n" + + "BAGXVQEFAQEHQEL/BiGtq0k84Km1wqQw2DIikVYrQrMttN8d7BPfnr4iAwEIBwAA\n" + + "/3/xFPG6U17rhTuq+07gmEvaFYKfxRB6sgAYiW6TMTpQEK6IeAQYFggAIBYhBOuF\n" + + "u1+jOnXhXpROY/IxVQxPR+OOBQJcRwTpAhsMAAoJEPIxVQxPR+OOWdABAMUdSzpM\n" + + "hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb\n" + + "Pnn+We1aTBhaGa86AQ==\n" + + "=n8OM\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + /** + * Alice's Ed25519 OpenPGP v4 certificate. + * + * @see + * Alice's OpenPGP Certificate + */ + public static final String ALICE_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: Alice's OpenPGP certificate\n" + + "Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n" + + "\n" + + "mDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U\n" + + "b7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE\n" + + "ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy\n" + + "MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO\n" + + "dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4\n" + + "OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s\n" + + "E9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb\n" + + "DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn\n" + + "0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE=\n" + + "=iIGO\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + /** + * Alice's Ed25519 OpenPGP v4 revocation certificate. + * + * @see + * Alice's Revocation Certificate + */ + public static final String ALICE_REVOCATION_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: Alice's revocation certificate\n" + + "Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n" + + "\n" + + "iHgEIBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXaWkOwIdAAAKCRDyMVUM\n" + + "T0fjjoBlAQDA9ukZFKRFGCooVcVoDVmxTaHLUXlIg9TPh2f7zzI9KgD/SLNXUOaH\n" + + "O6TozOS7C9lwIHwwdHdAxgf5BzuhLT9iuAM=\n" + + "=Tm8h\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + /** + * Bob's RSA-3072 OpenPGP v4 Secret Key Material. + * + * @see + * Bob's OpenPGP Secret Key Material + */ + public static final String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: D1A6 6E1A 23B1 82C9 980F 788C FBFC C82A 015E 7330\n" + + "Comment: Bob Babbage \n" + + "\n" + + "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\n" + + "=FAzO\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + /** + * Bob's RSA-3072 OpenPGP v4 Certificate. + * @see + * Bob's OpenPGP Certificate + */ + public static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: D1A6 6E1A 23B1 82C9 980F 788C FBFC C82A 015E 7330\n" + + "Comment: Bob Babbage \n" + + "\n" + + "xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w\n" + + "bGU+wsEOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx\n" + + "gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz\n" + + "XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO\n" + + "ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g\n" + + "9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF\n" + + "DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c\n" + + "ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1\n" + + "6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ\n" + + "ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo\n" + + "zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsDNBF2lnPIBDADW\n" + + "ML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamXnn9sSXvI\n" + + "DEINOQ6A9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMXSO4uImA+\n" + + "Uzula/6k1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6rrd5y2AO\n" + + "baifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA0YwIMgIT\n" + + "86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/wGlQ01rh\n" + + "827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+paLNDdVPL6\n" + + "vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV8rUnR76U\n" + + "qVC7KidNepdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwzj8sxH48A\n" + + "EQEAAcLA9gQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzyAhsMAAoJ\n" + + "EPv8yCoBXnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+41IL4rVcS\n" + + "KhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZQanYmtSx\n" + + "cVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zpf3u0k14i\n" + + "tcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn3OWjCPHV\n" + + "dTrdZ2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK2b0vk/+w\n" + + "qMJxfpa1lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFAExaEK6Vy\n" + + "jP7SXGLwvfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWif9RSK4xj\n" + + "zRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj5KjhX2PV\n" + + "NEJd3XZRzaXZE2aAMQ==\n" + + "=F9yX\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + /** + * Bob's RSA-3072 Revocation Certificate. + * @see + * Bob's Revocation Certificate + */ + public static final String BOB_REVOCATION_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: Bob's revocation certificate\n" + + "Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n" + + "\n" + + "iQG2BCABCgAgFiEE0aZuGiOxgsmYD3iM+/zIKgFeczAFAl2lnQQCHQAACgkQ+/zI\n" + + "KgFeczAIHAv/RrlGlPFKsW0BShC8sVtPfbT1N9lUqyrsgBhrUryM/i+rBtkbnSjp\n" + + "28R5araupt0og1g2L5VsCRM+ql0jf0zrZXOorKfAO70HCP3X+MlEquvztMUZGJRZ\n" + + "7TSMgIY1MeFgLmOw9pDKf3tSoouBOpPe5eVfXviEDDo2zOfdntjPyCMlxHgAcjZo\n" + + "XqMaurV+nKWoIx0zbdpNLsRy4JZcmnOSFdPw37R8U2miPi2qNyVwcyCxQy0LjN7Y\n" + + "AWadrs9vE0DrneSVP2OpBhl7g+Dj2uXJQRPVXcq6w9g5Fir6DnlhekTLsa78T5cD\n" + + "n8q7aRusMlALPAOosENOgINgsVcjuILkPN1eD+zGAgHgdiKaep1+P3pbo5n0CLki\n" + + "UCAsLnCEo8eBV9DCb/n1FlI5yhQhgQyMYlp/49H0JSc3IY9KHhv6f0zIaRWs0JuD\n" + + "ajcXTJ9AyB+SA6GBb9Q+XsNXjZ1gj75ekUD1sQ3ezTvVfovgP5bD+vPvILhSImKB\n" + + "aU6V3zld/x/1\n" + + "=mMwU\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + /** + * Carol's OpenPGP v4 key. + */ + public static final String CAROL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "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\n" + + "1FkOSekLi8WNMdUx3XMyvP8nJ65P2Q==\n" + + "=Xj8h\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + /** + * Carol's OpenPGP v4 certificate. + */ + public static final String CAROL_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "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\n" + + "=pa/S\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + /** + * Minimal OpenPGP v6 key. + * @see + * Sample Version 6 Secret Key + */ + public static final String V6_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB\n" + + "exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ\n" + + "BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh\n" + + "RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe\n" + + "7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/\n" + + "LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG\n" + + "GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE\n" + + "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr\n" + + "k0mXubZvyl4GBg==\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + /** + * Locked, minimal OpenPGP v6 key. + * @see + * Sample Locked Version 6 Secret Key + */ + public static final String V6_KEY_LOCKED = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC\n" + + "FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS\n" + + "3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC\n" + + "Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW\n" + + "cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin\n" + + "7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/\n" + + "0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0\n" + + "gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf\n" + + "9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR\n" + + "v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr\n" + + "DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki\n" + + "Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt\n" + + "ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + /** + * Passphrase to unlock {@link #V6_KEY_LOCKED} with. + */ + public static final String V6_KEY_LOCKED_PASSPHRASE = "correct horse battery staple"; + /** + * Sample Version 6 Certificate. + * @see + * Sample Version 6 Certificate + */ + public static final String V6_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf\n" + + "GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy\n" + + "KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw\n" + + "gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE\n" + + "QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn\n" + + "+eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh\n" + + "BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8\n" + + "j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805\n" + + "I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg==\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + public static PGPPublicKeyRing readPGPPublicKeyRing(String armor) + throws IOException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(armor.getBytes()); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + PGPPublicKeyRing publicKeys = (PGPPublicKeyRing) objFac.nextObject(); + pIn.close(); + aIn.close(); + return publicKeys; + } + + public static PGPSecretKeyRing readPGPSecretKeyRing(String armor) + throws IOException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(armor.getBytes()); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); + pIn.close(); + aIn.close(); + return secretKeys; + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java new file mode 100644 index 0000000000..56b38916a7 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPCertificateTest.java @@ -0,0 +1,846 @@ +package org.bouncycastle.openpgp.api.test; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.bcpg.test.AbstractPacketTest; +import org.bouncycastle.openpgp.KeyIdentifier; +import org.bouncycastle.openpgp.OpenPGPTestKeys; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.util.UTCUtil; +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class OpenPGPCertificateTest + extends AbstractPacketTest +{ + @Override + public String getName() + { + return "OpenPGPCertificateTest"; + } + + @Override + public void performTest() + throws Exception + { + testOpenPGPv6Key(); + + testBaseCasePrimaryKeySigns(); + testBaseCaseSubkeySigns(); + testPKSignsPKRevokedNoSubpacket(); + testSKSignsPKRevokedNoSubpacket(); + testPKSignsPKRevocationSuperseded(); + } + + private void testOpenPGPv6Key() + throws IOException + { + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY); + + isTrue("Test key has no identities", key.getIdentities().isEmpty()); + + OpenPGPCertificate.OpenPGPPrimaryKey primaryKey = key.getPrimaryKey(); + isEquals("Primary key identifier mismatch", + new KeyIdentifier("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9"), + primaryKey.getKeyIdentifier()); + OpenPGPKey.OpenPGPSecretKey secretPrimaryKey = key.getSecretKey(primaryKey); + isTrue("Secret Primary key MUST have reference to its public component", + primaryKey == secretPrimaryKey.getPublicKey()); + isTrue("Primary key is expected to be signing key", primaryKey.isSigningKey()); + isTrue("Primary secret key is expected to be signing key", secretPrimaryKey.isSigningKey()); + isTrue("Primary secret key is expected to be certification key", secretPrimaryKey.isCertificationKey()); + isTrue("Primary key is expected to be certification key", primaryKey.isCertificationKey()); + + List signingKeys = key.getSigningKeys(); + isEquals("Expected exactly 1 signing key", 1, signingKeys.size()); + OpenPGPCertificate.OpenPGPPrimaryKey signingKey = (OpenPGPCertificate.OpenPGPPrimaryKey) signingKeys.get(0); + isEquals("Signing key is expected to be the same as primary key", primaryKey, signingKey); + + Features signingKeyFeatures = signingKey.getFeatures(); + // Features are extracted from direct-key signature + isEquals("Signing key features mismatch. Expect features to be extracted from DK signature.", + Features.FEATURE_MODIFICATION_DETECTION | Features.FEATURE_SEIPD_V2, + signingKeyFeatures.getFeatures()); + + List encryptionKeys = key.getEncryptionKeys(); + isEquals("Expected exactly 1 encryption key", 1, encryptionKeys.size()); + OpenPGPCertificate.OpenPGPSubkey encryptionKey = (OpenPGPCertificate.OpenPGPSubkey) encryptionKeys.get(0); + isTrue("Subkey MUST be encryption key", encryptionKey.isEncryptionKey()); + isEquals("Encryption subkey identifier mismatch", + new KeyIdentifier("12C83F1E706F6308FE151A417743A1F033790E93E9978488D1DB378DA9930885"), + encryptionKey.getKeyIdentifier()); + + KeyFlags encryptionKeyFlags = encryptionKey.getKeyFlags(); + // Key Flags are extracted from subkey-binding signature + isEquals("Encryption key flag mismatch. Expected key flags to be extracted from SB sig.", + KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, + encryptionKeyFlags.getFlags()); + + Features encryptionKeyFeatures = encryptionKey.getFeatures(); + // Features are extracted from direct-key signature + isEquals("Encryption key features mismatch. Expected features to be extracted from DK sig.", + Features.FEATURE_MODIFICATION_DETECTION | Features.FEATURE_SEIPD_V2, + encryptionKeyFeatures.getFeatures()); + } + + private void testBaseCasePrimaryKeySigns() + throws IOException + { + // https://sequoia-pgp.gitlab.io/openpgp-interoperability-test-suite/results.html#Key_revocation_test__primary_key_signs_and_is_not_revoked__base_case_ + String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "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\n" + + "/3vqxdhYHw==\n" + + "=Ric2\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + TestSignature t0 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJYaEaACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmeoPMfalw2oS7uyOKnOXJSN8Gx7pr/BMlo3Xn8nTgx6\n" + + "ORYhBOMsttqCApG3522xqAitUcrkcPAGAABXbAf/WfWaQYNuATAKwxYrJx4fd5kt\n" + + "0M6sn1q7wK1MIxursG2+FuKafV25O9+pde8Nog77OEgegwk+HokOVFpVXfOzHQjs\n" + + "8dwWTtTQlX5NIBNvtqS7cvCKhjsqaHKgmzsenMjCEbpDZ3C5CoqcYicykqEU/Ia0\n" + + "ZGC4lzRByrgNy/w+/iLN748S707bzBLVc/sE73k9N5pANAlE+cA/sHI1Gp2WxJR9\n" + + "t2Fk4x6/85PEnF1RHI16p/wSEeuRaBpyw9QGZBbVDVt5wvgttxZjteGGSwBM3WI/\n" + + "gPfC0LW+JQ2W+dwY0PN/7yuARVRhXpKiBI4xqp7x3OanQX6quU77g3B8nXAt3A==\n" + + "=StqT\n" + + "-----END PGP SIGNATURE-----\n", false, "Sig predates primary key"); + TestSignature t1 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJa564ACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfM0EN4Ei0bQv6UO9BRq2wtUfV948cRynRMBb8TSGCG\n" + + "tBYhBOMsttqCApG3522xqAitUcrkcPAGAAAlNwf+L0KQK9i/xmYKOMV2EX13QUoZ\n" + + "vvb/pHGZaCQ9JtvEF2l2DT0DqByZ+tOv5Y4isU+un7CraoyvyajAwR0Yqk937B6C\n" + + "HQHKMkmIl+5R4/xqSoWYmOidbrgilojPMBEhB3INQ8/THjjFijtLzitVhnWBd7+u\n" + + "s0kcqnWnOdx2By4aDe+UEiyCfSE02e/0tIsM71RqiU91zH6dl6+q8nml7PsYuTFV\n" + + "V09oQTbBuuvUe+YgN/uvyKVIsA64lQ+YhqEeIA8Quek7fHhW+du9OIhSPsbYodyx\n" + + "VWMTXwSWKGNvZNAkpmgUYqFjS2Cx5ZUWblZLjrNKBwnnmt50qvUN7+o2pjlnfA==\n" + + "=UuXb\n" + + "-----END PGP SIGNATURE-----\n", true); + TestSignature t2 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJdP4iACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfFzYGoiuSjN+gz1IDD4ZvRXGuPTHks0/pIiGY90mrZ\n" + + "WxYhBOMsttqCApG3522xqAitUcrkcPAGAABGPAf/ck7tJAFoPIDd9fTPZANpNGoW\n" + + "Fq6VuNfy/nLjz2gkHFX/lLAxQ0N3McIdRA++Ik/omb0lis3R2DVNgwqNm2OF34HE\n" + + "qxmPmrQHBgk2q0fDH4NCE0XnYQjQT65V99IfiaQu+oS3Mq8MuYsDYvRVvRKMwt49\n" + + "fcDnvFtAtCqEETdv6wV5cUZmdQ3L9NU9bApJ0jk+EHVdpfTUIbOYYGnsIe/4Aa0d\n" + + "jgzu4Em79ynosOn//953XJ7OO8LCDi1EKt+nFuZARUlt/Jwwull6zzp7HUPw6HPt\n" + + "Upp7os8TIPC4STwoSeEKaxEkrbMGFnDcoDajnKKRt5+MkB24Oq7PHvnzgnPpVg==\n" + + "=Ljv7\n" + + "-----END PGP SIGNATURE-----\n", true); + TestSignature t3 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJmhTYiCRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfbjQf/zfoJQT0hhna4RDjOESBLgGaCbc5HLeo751F4\n" + + "NxYhBOMsttqCApG3522xqAitUcrkcPAGAABqBQgAkkNmYf6yLPvox+ZayrLtMb9D\n" + + "ghgt0nau72DSazsJ6SAq2QqIdr0RRhRa2gCETkp4PpeoDWmIvoVj35ZnfyeO/jqy\n" + + "HECvRwO0WPA5FXQM6uG7s40vDTRFjlJMpPyHWnn2igcR64iDxBGmc40xi9CcmJP9\n" + + "tmA26+1Nzj1LcfNvknKZ2UIOmnXiZY0QssIdyqsmJrdFpXs4UCLUzdXkfFLoxksU\n" + + "mk4B6hig2IKMj5mnbWy/JQSXtjjI+HHmtzgWfXs7d9iQ61CklbtCOiPeWxvoqlGG\n" + + "oK1wV1olcSar/RPKTlMmQpAg9dztQgrNs1oF7EF3i9kwNP7I5JzekPiOLH6oMw==\n" + + "=5KMU\n" + + "-----END PGP SIGNATURE-----\n", true); + + signatureValidityTest(cert, t0, t1, t2, t3); + } + + private void testBaseCaseSubkeySigns() + throws IOException + { + // https://sequoia-pgp.gitlab.io/openpgp-interoperability-test-suite/results.html#Key_revocation_test__subkey_signs__primary_key_is_not_revoked__base_case_ + String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "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\n" + + "/3vqxdhYHw==\n" + + "=Ric2\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + TestSignature t0 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJYaEaACRAQ/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmdVa4OG6WfRoRlj5+Zb6avhJUIZFvcIFiLuvrJp8Hio\n" + + "iBYhBM6mENCE+lHyEeHEbBD8vP8OjqeRAAAbaQgAjhBh0dLO0Sqiqkb2M3KWc25V\n" + + "hJlcP3isFROJ0jikmXxkG9W04AvlA78tSxEP2n8a0CbxH/hT4g8mFb/qM5FKZcKf\n" + + "HQxjbjUxBmVHa3EfMkwT7u1mVRmoWtJ59oVsKoqRb/kZ14i6VZ9NzfK8MRlL0e24\n" + + "oNjkksZQ8ImjwwtvxSinxhezA6BtWi+dDnXAnG5Vva+6N/GRNPAAd8kFTPrlEqEz\n" + + "uRbpq76r4taPjRjzMNcwZJoRVHSahWhDcXxNTalVUwt0DZFAskZ3gI+0VgU11bK1\n" + + "QmIw2iR4itQY5f10HFNcl7uHLKnul0YyuvA5509HwCuEpdYUV/OxtlpVRaJ+yg==\n" + + "=Rc6K\n" + + "-----END PGP SIGNATURE-----\n", false, "Signature predates primary key"); + TestSignature t1 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJa564ACRAQ/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfcG7Iqn3OOKVjeJ61MlgERt08kcxh0x+BZFD7a8K7V\n" + + "VBYhBM6mENCE+lHyEeHEbBD8vP8OjqeRAACBIwf9EoS24IFeT3cPFf/nWxLFkbZK\n" + + "fiy9WzyK4wlpO3VTyWPbXi6zpC4I5Rbp2jDk/c7Q3DnOZqFDv6TriTwuLYTJGPxr\n" + + "U3dtDsFcKp4FcbgFyCDKIuLB+3kLaNpMXqdttEkY3Wd5m33XrBB7M0l5xZCk56Jm\n" + + "H5L1sGNNNkCzG6P44qu69o5fkWxbYuX22fyhdeyxucJHMztqiMQYDwT7eSA92A1v\n" + + "5OwA5D/k7GeyYFBFisxRijkdVtxstC9zkagC19VnZo7MRekA9gXj7kIna4XYRhfb\n" + + "uQnN47HXdiWQytwypLvZ8JEJpRruyMAaHjX5OBXh0SK11xYWb6wB93+QfOahtg==\n" + + "=UlUZ\n" + + "-----END PGP SIGNATURE-----\n", false, "Subkey is not bound at this time"); + TestSignature t2 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJdP4iACRAQ/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmcgkZw3ZSg8CZCKqJw2r4VqCpTuUhz6N0zX43d+1xop\n" + + "2hYhBM6mENCE+lHyEeHEbBD8vP8OjqeRAADnqAgAq+m6dDZpNOBaXH9nwv8/+HgR\n" + + "MvRjnuLoa6zB5tcUhGPPVS0gg1PW0wfxlo1GPmgW3QDlV1zvcfYAZmV9uEC61wn/\n" + + "+FkqN0Tceo487UvkWARE/mmRj5L8OgUTfqm1eebFQlMu/MeG9YOg+tXBy7XS7hy3\n" + + "UdntIbtsv5oRTcybTnn5oiU2OFDlFC6sBNzOQt7wpyB1TKp2BdcsAv1RwmyCCCK4\n" + + "bnmrpYH6woWMyVEVeMYfOHAx9vHD+od8Vf/v5L1M2N0nHzRWjjkobTVUr+xt/CyW\n" + + "nq8SoazKYu3ETpZLeWX6Bciuv9+pzUCeClOSmBB1MFyyrTgbkOacHgrYnLvvtQ==\n" + + "=WCKA\n" + + "-----END PGP SIGNATURE-----\n", true); + TestSignature t3 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJmhTYiCRAQ/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmdi3dCpJ4nZincNH5owv8+fJ5YpXljqtegtoBEnbbHP\n" + + "thYhBM6mENCE+lHyEeHEbBD8vP8OjqeRAAD0cQf/e8RHocRESJPbosqUuvC3ELnD\n" + + "oSsJomDMUDfSfgpS5EhkOyJhvcrHkCbsHH2xlUEQ+zjJWY/dwM3FUkoj+p3kb/JC\n" + + "Rn5cqQYlME+uJzjdHMyQCSOI1SvYwKCLCGPARDbCpeINrV++Oy29e6cv6/IcPlgo\n" + + "k/0A7XuNq0YNxC7oopCj5ye3yVUvUmSCG2iV4oiWW5GhhPRzMeW7MFQmS0NUkAI8\n" + + "hzJ8juTG4xP8SXnHCMakasZhJmtpMDd2BDZ7CrhWiWUQGrtd0eYkuyodreqVMGIF\n" + + "BN80YgTNFW2MrblhDRRmxAqWzD9FedBwwSdgYbtkDwjsSq0S1jQV6aPndJqiLw==\n" + + "=CIl0\n" + + "-----END PGP SIGNATURE-----\n", true); + + signatureValidityTest(cert, t0, t1, t2, t3); + } + + private void testPKSignsPKRevokedNoSubpacket() + throws IOException + { + String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "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\n" + + "6sXYWB8=\n" + + "=13Sf\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + TestSignature t0 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJYaEaACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmeoPMfalw2oS7uyOKnOXJSN8Gx7pr/BMlo3Xn8nTgx6\n" + + "ORYhBOMsttqCApG3522xqAitUcrkcPAGAABXbAf/WfWaQYNuATAKwxYrJx4fd5kt\n" + + "0M6sn1q7wK1MIxursG2+FuKafV25O9+pde8Nog77OEgegwk+HokOVFpVXfOzHQjs\n" + + "8dwWTtTQlX5NIBNvtqS7cvCKhjsqaHKgmzsenMjCEbpDZ3C5CoqcYicykqEU/Ia0\n" + + "ZGC4lzRByrgNy/w+/iLN748S707bzBLVc/sE73k9N5pANAlE+cA/sHI1Gp2WxJR9\n" + + "t2Fk4x6/85PEnF1RHI16p/wSEeuRaBpyw9QGZBbVDVt5wvgttxZjteGGSwBM3WI/\n" + + "gPfC0LW+JQ2W+dwY0PN/7yuARVRhXpKiBI4xqp7x3OanQX6quU77g3B8nXAt3A==\n" + + "=StqT\n" + + "-----END PGP SIGNATURE-----\n", false, "Signature predates primary key"); + TestSignature t1 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJa564ACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfM0EN4Ei0bQv6UO9BRq2wtUfV948cRynRMBb8TSGCG\n" + + "tBYhBOMsttqCApG3522xqAitUcrkcPAGAAAlNwf+L0KQK9i/xmYKOMV2EX13QUoZ\n" + + "vvb/pHGZaCQ9JtvEF2l2DT0DqByZ+tOv5Y4isU+un7CraoyvyajAwR0Yqk937B6C\n" + + "HQHKMkmIl+5R4/xqSoWYmOidbrgilojPMBEhB3INQ8/THjjFijtLzitVhnWBd7+u\n" + + "s0kcqnWnOdx2By4aDe+UEiyCfSE02e/0tIsM71RqiU91zH6dl6+q8nml7PsYuTFV\n" + + "V09oQTbBuuvUe+YgN/uvyKVIsA64lQ+YhqEeIA8Quek7fHhW+du9OIhSPsbYodyx\n" + + "VWMTXwSWKGNvZNAkpmgUYqFjS2Cx5ZUWblZLjrNKBwnnmt50qvUN7+o2pjlnfA==\n" + + "=UuXb\n" + + "-----END PGP SIGNATURE-----\n", false, "Hard revocations invalidate key at all times"); + TestSignature t2 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJdP4iACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfFzYGoiuSjN+gz1IDD4ZvRXGuPTHks0/pIiGY90mrZ\n" + + "WxYhBOMsttqCApG3522xqAitUcrkcPAGAABGPAf/ck7tJAFoPIDd9fTPZANpNGoW\n" + + "Fq6VuNfy/nLjz2gkHFX/lLAxQ0N3McIdRA++Ik/omb0lis3R2DVNgwqNm2OF34HE\n" + + "qxmPmrQHBgk2q0fDH4NCE0XnYQjQT65V99IfiaQu+oS3Mq8MuYsDYvRVvRKMwt49\n" + + "fcDnvFtAtCqEETdv6wV5cUZmdQ3L9NU9bApJ0jk+EHVdpfTUIbOYYGnsIe/4Aa0d\n" + + "jgzu4Em79ynosOn//953XJ7OO8LCDi1EKt+nFuZARUlt/Jwwull6zzp7HUPw6HPt\n" + + "Upp7os8TIPC4STwoSeEKaxEkrbMGFnDcoDajnKKRt5+MkB24Oq7PHvnzgnPpVg==\n" + + "=Ljv7\n" + + "-----END PGP SIGNATURE-----\n", false, "Hard revocations invalidate key at all times"); + TestSignature t3 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJmhTYiCRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfbjQf/zfoJQT0hhna4RDjOESBLgGaCbc5HLeo751F4\n" + + "NxYhBOMsttqCApG3522xqAitUcrkcPAGAABqBQgAkkNmYf6yLPvox+ZayrLtMb9D\n" + + "ghgt0nau72DSazsJ6SAq2QqIdr0RRhRa2gCETkp4PpeoDWmIvoVj35ZnfyeO/jqy\n" + + "HECvRwO0WPA5FXQM6uG7s40vDTRFjlJMpPyHWnn2igcR64iDxBGmc40xi9CcmJP9\n" + + "tmA26+1Nzj1LcfNvknKZ2UIOmnXiZY0QssIdyqsmJrdFpXs4UCLUzdXkfFLoxksU\n" + + "mk4B6hig2IKMj5mnbWy/JQSXtjjI+HHmtzgWfXs7d9iQ61CklbtCOiPeWxvoqlGG\n" + + "oK1wV1olcSar/RPKTlMmQpAg9dztQgrNs1oF7EF3i9kwNP7I5JzekPiOLH6oMw==\n" + + "=5KMU\n" + + "-----END PGP SIGNATURE-----\n", false, "Hard revocations invalidate key at all times"); + + signatureValidityTest(cert, t0, t1, t2, t3); + } + + private void testSKSignsPKRevokedNoSubpacket() + throws IOException + { + // https://sequoia-pgp.gitlab.io/openpgp-interoperability-test-suite/results.html#Key_revocation_test__subkey_signs__primary_key_is_revoked__revoked__no_subpacket + String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "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\n" + + "6sXYWB8=\n" + + "=13Sf\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + TestSignature t0 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJYaEaACRAQ/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmdVa4OG6WfRoRlj5+Zb6avhJUIZFvcIFiLuvrJp8Hio\n" + + "iBYhBM6mENCE+lHyEeHEbBD8vP8OjqeRAAAbaQgAjhBh0dLO0Sqiqkb2M3KWc25V\n" + + "hJlcP3isFROJ0jikmXxkG9W04AvlA78tSxEP2n8a0CbxH/hT4g8mFb/qM5FKZcKf\n" + + "HQxjbjUxBmVHa3EfMkwT7u1mVRmoWtJ59oVsKoqRb/kZ14i6VZ9NzfK8MRlL0e24\n" + + "oNjkksZQ8ImjwwtvxSinxhezA6BtWi+dDnXAnG5Vva+6N/GRNPAAd8kFTPrlEqEz\n" + + "uRbpq76r4taPjRjzMNcwZJoRVHSahWhDcXxNTalVUwt0DZFAskZ3gI+0VgU11bK1\n" + + "QmIw2iR4itQY5f10HFNcl7uHLKnul0YyuvA5509HwCuEpdYUV/OxtlpVRaJ+yg==\n" + + "=Rc6K\n" + + "-----END PGP SIGNATURE-----\n", false, "Signature predates primary key"); + TestSignature t1 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJa564ACRAQ/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfcG7Iqn3OOKVjeJ61MlgERt08kcxh0x+BZFD7a8K7V\n" + + "VBYhBM6mENCE+lHyEeHEbBD8vP8OjqeRAACBIwf9EoS24IFeT3cPFf/nWxLFkbZK\n" + + "fiy9WzyK4wlpO3VTyWPbXi6zpC4I5Rbp2jDk/c7Q3DnOZqFDv6TriTwuLYTJGPxr\n" + + "U3dtDsFcKp4FcbgFyCDKIuLB+3kLaNpMXqdttEkY3Wd5m33XrBB7M0l5xZCk56Jm\n" + + "H5L1sGNNNkCzG6P44qu69o5fkWxbYuX22fyhdeyxucJHMztqiMQYDwT7eSA92A1v\n" + + "5OwA5D/k7GeyYFBFisxRijkdVtxstC9zkagC19VnZo7MRekA9gXj7kIna4XYRhfb\n" + + "uQnN47HXdiWQytwypLvZ8JEJpRruyMAaHjX5OBXh0SK11xYWb6wB93+QfOahtg==\n" + + "=UlUZ\n" + + "-----END PGP SIGNATURE-----\n", false, "Hard revocations invalidate key at all times"); + TestSignature t2 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJdP4iACRAQ/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmcgkZw3ZSg8CZCKqJw2r4VqCpTuUhz6N0zX43d+1xop\n" + + "2hYhBM6mENCE+lHyEeHEbBD8vP8OjqeRAADnqAgAq+m6dDZpNOBaXH9nwv8/+HgR\n" + + "MvRjnuLoa6zB5tcUhGPPVS0gg1PW0wfxlo1GPmgW3QDlV1zvcfYAZmV9uEC61wn/\n" + + "+FkqN0Tceo487UvkWARE/mmRj5L8OgUTfqm1eebFQlMu/MeG9YOg+tXBy7XS7hy3\n" + + "UdntIbtsv5oRTcybTnn5oiU2OFDlFC6sBNzOQt7wpyB1TKp2BdcsAv1RwmyCCCK4\n" + + "bnmrpYH6woWMyVEVeMYfOHAx9vHD+od8Vf/v5L1M2N0nHzRWjjkobTVUr+xt/CyW\n" + + "nq8SoazKYu3ETpZLeWX6Bciuv9+pzUCeClOSmBB1MFyyrTgbkOacHgrYnLvvtQ==\n" + + "=WCKA\n" + + "-----END PGP SIGNATURE-----\n", false, "Hard revocations invalidate key at all times"); + TestSignature t3 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJmhTYiCRAQ/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmdi3dCpJ4nZincNH5owv8+fJ5YpXljqtegtoBEnbbHP\n" + + "thYhBM6mENCE+lHyEeHEbBD8vP8OjqeRAAD0cQf/e8RHocRESJPbosqUuvC3ELnD\n" + + "oSsJomDMUDfSfgpS5EhkOyJhvcrHkCbsHH2xlUEQ+zjJWY/dwM3FUkoj+p3kb/JC\n" + + "Rn5cqQYlME+uJzjdHMyQCSOI1SvYwKCLCGPARDbCpeINrV++Oy29e6cv6/IcPlgo\n" + + "k/0A7XuNq0YNxC7oopCj5ye3yVUvUmSCG2iV4oiWW5GhhPRzMeW7MFQmS0NUkAI8\n" + + "hzJ8juTG4xP8SXnHCMakasZhJmtpMDd2BDZ7CrhWiWUQGrtd0eYkuyodreqVMGIF\n" + + "BN80YgTNFW2MrblhDRRmxAqWzD9FedBwwSdgYbtkDwjsSq0S1jQV6aPndJqiLw==\n" + + "=CIl0\n" + + "-----END PGP SIGNATURE-----\n", false, "Hard revocations invalidate key at all times"); + + signatureValidityTest(cert, t0, t1, t2, t3); + } + + private void testPKSignsPKRevocationSuperseded() + throws IOException + { + // https://sequoia-pgp.gitlab.io/openpgp-interoperability-test-suite/results.html#Key_revocation_test__primary_key_signs_and_is_revoked__revoked__superseded + String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "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\n" + + "2fSYyKjPbyaRqh72MlIlUXwqq1ih2TJc3vwF8aNVDrcb9DnBabdt2M1vI3PUaeG3\n" + + "1BmakC/XZCNCrbbJkyd/vdMLqw7prLrp0auVNNhLYxOK9usXbClNxluo4i/lSFVo\n" + + "5B9ai+ne1kKKiplzqy2qqhdeplomcwGHbB1CkZ04DmCMbSSFAGxYqUC/bBm0bolC\n" + + "ebw/KIz9sEojNKt6mvsFN67/hMYeJS0HVlwwc6i8iKSzC2D53iywhtvkdiKECXQe\n" + + "XDf9zNXAn1wpK01SLJ0iig7cDFrtoqkfPYzbNfC0bt34fNx9iz3w9aEH8c7ATQRa\n" + + "SsuAAQgAu5yau9psltmWiUn7fsRSqbQInO0iWnu4DK9IXB3ghNYMcii3JJEjHzgI\n" + + "xGf3GiJEjzubyRQaX5J/p7yB1fOH8z7FYUuax1saGf9c1/b02N9gyXNlHam31hNa\n" + + "aL3ffFczI95p7MNrTtroTt5oZqsc+i+oKLZn7X0YAI4tEYwhSnUQYB/F7YqkkI4e\n" + + "V+7CxZPA8pBhXiAOK/zn416PsZ6JS5wsM65yCtOHcAAIBnKDnC+bQi+f1WZesSoc\n" + + "y/rXx3QEQmodDu3ojhS+VxcYGeZCUcFF0FyZBIkGjHIVQLyOfjP3FRJ4qFXMz9/Y\n" + + "IVoM4Y6guTERMTEj/KDG4BP7RfJHTQARAQABwsI8BBgBCgHwBYJeC+EACRAIrVHK\n" + + "5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfcAa1Z\n" + + "PWTtg60w3Oo4dt4Fa8cKFYbZYsqDSHV5pwEfMwKbAsC8oAQZAQoAbwWCXgvhAAkQ\n" + + "EPy8/w6Op5FHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn\n" + + "L6I2+VyN5T1FoVgj3cdnMLYCpcB5i/FRSCVKybuLzrgWIQTOphDQhPpR8hHhxGwQ\n" + + "/Lz/Do6nkQAArk8H/AhjM9lqbffFL6RRR4HTjelspy4A3nyTicCljrDuXDUh23Gf\n" + + "LvajTR5h16ZBqAF7cpb9rrlz1C1WcS5JLVxzXAe7f+KOfXu+eyLhpTzZ8VT3pK3h\n" + + "HGaYwlVlXrBZP0JXgL8hm6hDSXZQZtcpsnQ1uIHC9ONxUB4liNFhTqQCQYdQJFiF\n" + + "s1umUbo/C4KdzlDI08bM3CqEKat9vUFuGG68mDg0CrRZEWt946L5i8kZmBUkSShI\n" + + "m2k5e2qE/muYeM6qKQNsxlx3VIf5eUhtxCi9fg7SjvHkdUSFstYcxAdaohWCFCEs\n" + + "DJI12hzcKQazSjvtKF4BNBKgX/wLsbVQnYLd9ggWIQTjLLbaggKRt+dtsagIrVHK\n" + + "5HDwBgAANjMH/1MY7DJyxkiTjc/jzmnVxqtHOZDCSmUqk0eh/6BHs+ostWqkGC6+\n" + + "7dfxDnptwcqandYey4KF2ajt4nOwu0xQw/NEF3i81h7IiewY7G+YT69DUd+DvVUQ\n" + + "emfKNYVOrMqoH7QU5o4YojdJiDeIp2d/JyJrqyof78JFAHnNZgHC2T2zo9E54dnO\n" + + "TY9VNUNCOUct5Rby0GXjTIURO0f485eGuZxVWdLRllDYOiCrQHPSHhrxHVXVMbYJ\n" + + "oroPy+IyaJanVoAWgyipBmmIDV8aINM2RLMsGkuPTRtITI2ZlGOQN7xgy4LqWzjP\n" + + "nrzMXfwBEDx/nrwdG6zEGMK8AkVkMT5uJJvCwjwEGAEKAfAFglro/4AJEAitUcrk\n" + + "cPAGRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ/Q0Z6WD\n" + + "H2+8/F1xEEuiApsjnn2lGNZ2DeIaklJzdqQOApsCwLygBBkBCgBvBYJa6P+ACRAQ\n" + + "/Lz/Do6nkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfr\n" + + "VATyX3tgcM2z41fqYquxVhJRavN6+w2SU4xEG++SqBYhBM6mENCE+lHyEeHEbBD8\n" + + "vP8OjqeRAABGVggAsB8M2KI5cxXKKgVHL1dEfzg9halVavktfcT6ZVC/+aDp94tv\n" + + "BCL16Guhq4ccN7DATrWx430/GecY6E77qvhDzmCclSbdLbiZmsrVX9kCmTfrJzFQ\n" + + "64KfvIS5GgbL21+ZJ+pKW2HOMBGn6sgAPmTqM5UsDCpsEKDt5CJcJr3sTc8D9NhE\n" + + "nc0dKsQ91+n9ms3W5tyyE6r9pyM6ThBCMhbQkR7hE9XWAQeO1ILSFGnie0aFcTU0\n" + + "Oo0wL1MaiSyA/8XpKq23xfx1kNS9hQkdq0aWehNoTJdCt1Nq1cWABy2rQR0x+qhG\n" + + "WowfsAjnBautxvet28t2kPCAIMniYpWc89BwfhYhBOMsttqCApG3522xqAitUcrk\n" + + "cPAGAACq1gf/Q7H9Re5SWk+UOn/NQPRedf544YJ/YdQnve/hSaPGL33cUzf4yxzF\n" + + "ILnK19Ird5f8/mTT1pg99L3ixE3N5031JJKwFpCB69Rsysg88ZLDL2VLc3xdsAQd\n" + + "UbVaCqeRHKwtMtpBvbAFvF9plwam0SSXHHr/JkYm5ufXN6I8ib/nwr1bFbf/Se0W\n" + + "uk9RG4ne9JUBCrGxakyVd+OgLLhvzOmJa7fDC0uUZhTKFbjMxLhaas4HFYiRbfz2\n" + + "T0xz9gyDytDWsEFM+XoKHlEH8Fx/U2B5/8N0Q+pIFoEuOmBO+5EPvPIlxNByHgia\n" + + "NIuKt1Mu+UAb2Spl6D5zbDfX/3vqxdhYHw==\n" + + "=9epL\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + TestSignature t0 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJYaEaACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmeoPMfalw2oS7uyOKnOXJSN8Gx7pr/BMlo3Xn8nTgx6\n" + + "ORYhBOMsttqCApG3522xqAitUcrkcPAGAABXbAf/WfWaQYNuATAKwxYrJx4fd5kt\n" + + "0M6sn1q7wK1MIxursG2+FuKafV25O9+pde8Nog77OEgegwk+HokOVFpVXfOzHQjs\n" + + "8dwWTtTQlX5NIBNvtqS7cvCKhjsqaHKgmzsenMjCEbpDZ3C5CoqcYicykqEU/Ia0\n" + + "ZGC4lzRByrgNy/w+/iLN748S707bzBLVc/sE73k9N5pANAlE+cA/sHI1Gp2WxJR9\n" + + "t2Fk4x6/85PEnF1RHI16p/wSEeuRaBpyw9QGZBbVDVt5wvgttxZjteGGSwBM3WI/\n" + + "gPfC0LW+JQ2W+dwY0PN/7yuARVRhXpKiBI4xqp7x3OanQX6quU77g3B8nXAt3A==\n" + + "=StqT\n" + + "-----END PGP SIGNATURE-----\n", false, "Signature predates primary key"); + TestSignature t1 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJa564ACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfM0EN4Ei0bQv6UO9BRq2wtUfV948cRynRMBb8TSGCG\n" + + "tBYhBOMsttqCApG3522xqAitUcrkcPAGAAAlNwf+L0KQK9i/xmYKOMV2EX13QUoZ\n" + + "vvb/pHGZaCQ9JtvEF2l2DT0DqByZ+tOv5Y4isU+un7CraoyvyajAwR0Yqk937B6C\n" + + "HQHKMkmIl+5R4/xqSoWYmOidbrgilojPMBEhB3INQ8/THjjFijtLzitVhnWBd7+u\n" + + "s0kcqnWnOdx2By4aDe+UEiyCfSE02e/0tIsM71RqiU91zH6dl6+q8nml7PsYuTFV\n" + + "V09oQTbBuuvUe+YgN/uvyKVIsA64lQ+YhqEeIA8Quek7fHhW+du9OIhSPsbYodyx\n" + + "VWMTXwSWKGNvZNAkpmgUYqFjS2Cx5ZUWblZLjrNKBwnnmt50qvUN7+o2pjlnfA==\n" + + "=UuXb\n" + + "-----END PGP SIGNATURE-----\n", true); + TestSignature t2 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJdP4iACRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfFzYGoiuSjN+gz1IDD4ZvRXGuPTHks0/pIiGY90mrZ\n" + + "WxYhBOMsttqCApG3522xqAitUcrkcPAGAABGPAf/ck7tJAFoPIDd9fTPZANpNGoW\n" + + "Fq6VuNfy/nLjz2gkHFX/lLAxQ0N3McIdRA++Ik/omb0lis3R2DVNgwqNm2OF34HE\n" + + "qxmPmrQHBgk2q0fDH4NCE0XnYQjQT65V99IfiaQu+oS3Mq8MuYsDYvRVvRKMwt49\n" + + "fcDnvFtAtCqEETdv6wV5cUZmdQ3L9NU9bApJ0jk+EHVdpfTUIbOYYGnsIe/4Aa0d\n" + + "jgzu4Em79ynosOn//953XJ7OO8LCDi1EKt+nFuZARUlt/Jwwull6zzp7HUPw6HPt\n" + + "Upp7os8TIPC4STwoSeEKaxEkrbMGFnDcoDajnKKRt5+MkB24Oq7PHvnzgnPpVg==\n" + + "=Ljv7\n" + + "-----END PGP SIGNATURE-----\n", false, "Key is revoked at this time"); + TestSignature t3 = new TestSignature("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wsC7BAABCgBvBYJmhTYiCRAIrVHK5HDwBkcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + + "cy5zZXF1b2lhLXBncC5vcmfbjQf/zfoJQT0hhna4RDjOESBLgGaCbc5HLeo751F4\n" + + "NxYhBOMsttqCApG3522xqAitUcrkcPAGAABqBQgAkkNmYf6yLPvox+ZayrLtMb9D\n" + + "ghgt0nau72DSazsJ6SAq2QqIdr0RRhRa2gCETkp4PpeoDWmIvoVj35ZnfyeO/jqy\n" + + "HECvRwO0WPA5FXQM6uG7s40vDTRFjlJMpPyHWnn2igcR64iDxBGmc40xi9CcmJP9\n" + + "tmA26+1Nzj1LcfNvknKZ2UIOmnXiZY0QssIdyqsmJrdFpXs4UCLUzdXkfFLoxksU\n" + + "mk4B6hig2IKMj5mnbWy/JQSXtjjI+HHmtzgWfXs7d9iQ61CklbtCOiPeWxvoqlGG\n" + + "oK1wV1olcSar/RPKTlMmQpAg9dztQgrNs1oF7EF3i9kwNP7I5JzekPiOLH6oMw==\n" + + "=5KMU\n" + + "-----END PGP SIGNATURE-----\n", true); + + signatureValidityTest(CERT, t0, t1, t2, t3); + } + + private void signatureValidityTest(String cert, TestSignature... testSignatures) + throws IOException + { + OpenPGPCertificate certificate = OpenPGPCertificate.fromAsciiArmor(cert); + + for (TestSignature test : testSignatures) + { + PGPSignature signature = test.getSignature(); + OpenPGPCertificate.OpenPGPComponentKey signingKey = certificate.getSigningKeyFor(signature); + + boolean valid = signingKey.isBoundAt(signature.getCreationTime()); + if (valid != test.isExpectValid()) + { + StringBuilder sb = new StringBuilder("Key validity mismatch. Expected " + signingKey.toString() + + (test.isExpectValid() ? (" to be valid at ") : (" to be invalid at ")) + UTCUtil.format(signature.getCreationTime())); + if (test.getMsg() != null) + { + sb.append(" because:\n").append(test.getMsg()); + } + sb.append("\n").append(signingKey.getSignatureChains()); + fail(sb.toString()); + } + } + } + + public static class TestSignature + { + private final PGPSignature signature; + private final boolean expectValid; + private final String msg; + + public TestSignature(String armoredSignature, boolean expectValid) + throws IOException + { + this(armoredSignature, expectValid, null); + } + + public TestSignature(String armoredSignature, boolean expectValid, String msg) + throws IOException + { + this.signature = parseSignature(armoredSignature); + this.expectValid = expectValid; + this.msg = msg; + } + + private static PGPSignature parseSignature(String armoredSignature) + throws IOException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(armoredSignature.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + + PGPSignatureList sigs = (PGPSignatureList) objFac.nextObject(); + + pIn.close(); + aIn.close(); + bIn.close(); + + return sigs.get(0); + } + + public PGPSignature getSignature() + { + return signature; + } + + public boolean isExpectValid() + { + return expectValid; + } + + public String getMsg() + { + return msg; + } + } + + public static void main(String[] args) + { + runTest(new OpenPGPCertificateTest()); + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageGeneratorTest.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageGeneratorTest.java new file mode 100644 index 0000000000..826788a7f3 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageGeneratorTest.java @@ -0,0 +1,180 @@ +package org.bouncycastle.openpgp.api.test; + +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.test.AbstractPacketTest; +import org.bouncycastle.openpgp.OpenPGPTestKeys; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPMessageGenerator; +import org.bouncycastle.openpgp.api.OpenPGPMessageOutputStream; +import org.bouncycastle.util.encoders.Hex; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public class OpenPGPMessageGeneratorTest + extends AbstractPacketTest +{ + @Override + public String getName() + { + return "OpenPGPMessageGeneratorTest"; + } + + @Override + public void performTest() + throws Exception + { + armoredLiteralDataPacket(); + unarmoredLiteralDataPacket(); + + armoredCompressedLiteralDataPacket(); + unarmoredCompressedLiteralDataPacket(); + + seipd1EncryptedMessage(); + seipd2EncryptedMessage(); + + seipd2EncryptedSignedMessage(); + } + + private void armoredLiteralDataPacket() + throws PGPException, IOException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.setIsPadded(false); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream msgOut = gen.open(bOut); + + // Only write a LiteralData packet with "Hello, World!" as content + msgOut.write("Hello, World!".getBytes(StandardCharsets.UTF_8)); + + msgOut.close(); + + isEquals( + "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "yxNiAAAAAABIZWxsbywgV29ybGQh\n" + + "-----END PGP MESSAGE-----\n", + bOut.toString()); + } + + private void unarmoredLiteralDataPacket() + throws PGPException, IOException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.setArmored(false); // disable ASCII armor + gen.setIsPadded(false); // disable padding + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream msgOut = gen.open(bOut); + + // Only write a LiteralData packet with "Hello, World!" as content + msgOut.write("Hello, World!".getBytes(StandardCharsets.UTF_8)); + + msgOut.close(); + + isEncodingEqual(Hex.decode("cb1362000000000048656c6c6f2c20576f726c6421"), bOut.toByteArray()); + } + + private void armoredCompressedLiteralDataPacket() + throws PGPException, IOException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.setIsPadded(false); + OpenPGPMessageGenerator.Configuration configuration = gen.getConfiguration(); + configuration.setCompressionNegotiator(conf -> CompressionAlgorithmTags.ZIP); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream msgOut = gen.open(bOut); + + // Only write a LiteralData packet with "Hello, World!" as content + msgOut.write("Hello, World!".getBytes(StandardCharsets.UTF_8)); + + msgOut.close(); + + isEquals("-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "yBUBOy2cxAACHqk5Ofk6CuH5RTkpigA=\n" + + "-----END PGP MESSAGE-----\n", + bOut.toString()); + } + + private void unarmoredCompressedLiteralDataPacket() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.setArmored(false); // no armor + gen.setIsPadded(false); + OpenPGPMessageGenerator.Configuration configuration = gen.getConfiguration(); + configuration.setCompressionNegotiator(conf -> CompressionAlgorithmTags.ZIP); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream msgOut = gen.open(bOut); + + // Only write a LiteralData packet with "Hello, World!" as content + msgOut.write("Hello, World!".getBytes(StandardCharsets.UTF_8)); + + msgOut.close(); + + isEncodingEqual(Hex.decode("c815013b2d9cc400021ea93939f93a0ae1f94539298a00"), bOut.toByteArray()); + } + + private void seipd2EncryptedMessage() + throws IOException, PGPException + { + OpenPGPCertificate cert = OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.V6_CERT); + + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.addEncryptionCertificate(cert); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream encOut = gen.open(bOut); + encOut.write("Hello World!\n".getBytes(StandardCharsets.UTF_8)); + encOut.close(); + + System.out.println(bOut); + } + + private void seipd1EncryptedMessage() + throws IOException, PGPException + { + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.BOB_KEY); + + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.addEncryptionCertificate(key); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream encOut = gen.open(bOut); + encOut.write("Hello World!\n".getBytes(StandardCharsets.UTF_8)); + encOut.close(); + + System.out.println(bOut); + } + + private void seipd2EncryptedSignedMessage() + throws IOException, PGPException + { + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY); + + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator() + .setIsPadded(true) + .setArmored(true) + .addSigningKey(key) + .addEncryptionCertificate(key); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream encOut = gen.open(bOut); + encOut.write("Hello, World!\n".getBytes()); + encOut.close(); + + System.out.println(bOut); + } + + public static void main(String[] args) + { + runTest(new OpenPGPMessageGeneratorTest()); + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageProcessorTest.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageProcessorTest.java new file mode 100644 index 0000000000..57234132d8 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageProcessorTest.java @@ -0,0 +1,664 @@ +package org.bouncycastle.openpgp.api.test; + +import org.bouncycastle.bcpg.AEADAlgorithmTags; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.bcpg.test.AbstractPacketTest; +import org.bouncycastle.openpgp.OpenPGPTestKeys; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPMessageInputStream; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPMessageGenerator; +import org.bouncycastle.openpgp.api.OpenPGPMessageOutputStream; +import org.bouncycastle.openpgp.api.OpenPGPMessageProcessor; +import org.bouncycastle.openpgp.api.OpenPGPSignature; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.io.Streams; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class OpenPGPMessageProcessorTest + extends AbstractPacketTest +{ + private static final byte[] PLAINTEXT = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); + + private PGPSessionKey encryptionSessionKey; + + @Override + public String getName() + { + return "OpenPGPMessageProcessorTest"; + } + + @Override + public void performTest() + throws Exception + { + roundtripUnarmoredPlaintextMessage(); + roundtripArmoredPlaintextMessage(); + roundTripCompressedMessage(); + roundTripCompressedSymEncMessageMessage(); + + roundTripSymEncMessageWithMultiplePassphrases(); + + roundTripV4KeyEncryptedMessageAlice(); + roundTripV4KeyEncryptedMessageBob(); + roundTripV4KeyEncryptedMessageCarol(); + + roundTripV6KeyEncryptedMessage(); + encryptWithV4V6KeyDecryptWithV4(); + encryptWithV4V6KeyDecryptWithV6(); + + encryptDecryptWithLockedKey(); + encryptDecryptWithMissingKey(); + + inlineSignWithV4KeyAlice(); + inlineSignWithV4KeyBob(); + inlineSignWithV4KeyCarol(); + inlineSignWithV6Key(); + + verifyMessageByRevokedKey(); + incompleteMessageProcessing(); + } + + private void roundtripUnarmoredPlaintextMessage() + throws PGPException, IOException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator() + .setArmored(false) + .setIsPadded(false); + + gen.getConfiguration().setCompressionNegotiator(conf -> CompressionAlgorithmTags.UNCOMPRESSED); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream msgOut = gen.open(bOut); + msgOut.write(PLAINTEXT); + msgOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor(); + OpenPGPMessageInputStream plainIn = processor.process(bIn); + ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); + Streams.pipeAll(plainIn, plainOut); + plainIn.close(); + isEquals(MessageEncryptionMechanism.unencrypted(), plainIn.getResult().getEncryptionMethod()); + + isEncodingEqual(PLAINTEXT, plainOut.toByteArray()); + } + + private void roundtripArmoredPlaintextMessage() + throws PGPException, IOException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator() + .setArmored(true) + .setIsPadded(false); + gen.getConfiguration().setCompressionNegotiator(conf -> CompressionAlgorithmTags.UNCOMPRESSED); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream msgOut = gen.open(bOut); + msgOut.write(PLAINTEXT); + msgOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor(); + OpenPGPMessageInputStream plainIn = processor.process(bIn); + ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); + Streams.pipeAll(plainIn, plainOut); + plainIn.close(); + OpenPGPMessageInputStream.Result result = plainIn.getResult(); + isEquals(MessageEncryptionMechanism.unencrypted(), result.getEncryptionMethod()); + + isEncodingEqual(PLAINTEXT, plainOut.toByteArray()); + } + + private void roundTripCompressedMessage() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator() + .setArmored(true) + .setIsPadded(false); + gen.getConfiguration().setCompressionNegotiator(conf -> CompressionAlgorithmTags.ZIP); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream msgOut = gen.open(bOut); + msgOut.write(PLAINTEXT); + msgOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor(); + InputStream plainIn = processor.process(bIn); + ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); + Streams.pipeAll(plainIn, plainOut); + plainIn.close(); + + isEncodingEqual(PLAINTEXT, plainOut.toByteArray()); + } + + private void roundTripCompressedSymEncMessageMessage() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator() + .setArmored(true) + .addEncryptionPassphrase("lal".toCharArray()) + .setSessionKeyExtractionCallback( + sk -> this.encryptionSessionKey = sk + ) + .setIsPadded(false); + gen.getConfiguration() + .setPasswordBasedEncryptionNegotiator(conf -> + MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithmTags.AES_256)) + .setCompressionNegotiator(conf -> CompressionAlgorithmTags.ZIP); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream msgOut = gen.open(bOut); + msgOut.write(PLAINTEXT); + msgOut.close(); + isNotNull(encryptionSessionKey); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageInputStream plainIn = new OpenPGPMessageProcessor() + .addMessagePassphrase("lal".toCharArray()) + .process(bIn); + ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); + Streams.pipeAll(plainIn, plainOut); + plainIn.close(); + OpenPGPMessageInputStream.Result result = plainIn.getResult(); + isEquals(CompressionAlgorithmTags.ZIP, result.getCompressionAlgorithm()); + isTrue(Arrays.areEqual("lal".toCharArray(), result.getDecryptionPassphrase())); + isEncodingEqual(encryptionSessionKey.getKey(), result.getSessionKey().getKey()); + + isEncodingEqual(PLAINTEXT, plainOut.toByteArray()); + } + + private void roundTripSymEncMessageWithMultiplePassphrases() + throws PGPException, IOException + { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator() + .addEncryptionPassphrase("orange".toCharArray()) + .addEncryptionPassphrase("violet".toCharArray()) + .setSessionKeyExtractionCallback(sk -> this.encryptionSessionKey = sk); + gen.getConfiguration().setPasswordBasedEncryptionNegotiator(configuration -> + MessageEncryptionMechanism.aead(SymmetricKeyAlgorithmTags.AES_128, AEADAlgorithmTags.OCB)); + + OutputStream encOut = gen.open(bOut); + encOut.write(PLAINTEXT); + encOut.close(); + + byte[] ciphertext = bOut.toByteArray(); + ByteArrayInputStream bIn = new ByteArrayInputStream(ciphertext); + bOut = new ByteArrayOutputStream(); + + // Try decryption with explicitly set message passphrase + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor(); + processor.addMessagePassphrase("violet".toCharArray()); + OpenPGPMessageInputStream decIn = processor.process(bIn); + Streams.pipeAll(decIn, bOut); + decIn.close(); + OpenPGPMessageInputStream.Result result = decIn.getResult(); + isTrue(Arrays.areEqual("violet".toCharArray(), result.getDecryptionPassphrase())); + isEncodingEqual(encryptionSessionKey.getKey(), result.getSessionKey().getKey()); + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + isEquals(result.getEncryptionMethod(), + MessageEncryptionMechanism.aead(SymmetricKeyAlgorithmTags.AES_128, AEADAlgorithmTags.OCB)); + + // Try decryption with wrong passphrase and then request proper one dynamically + bOut = new ByteArrayOutputStream(); + bIn = new ByteArrayInputStream(ciphertext); + processor = new OpenPGPMessageProcessor(); + decIn = processor.setMissingMessagePassphraseCallback(new StackPassphraseCallback("orange".toCharArray())) + // wrong passphrase, so missing callback is invoked + .addMessagePassphrase("yellow".toCharArray()) + .process(bIn); + + Streams.pipeAll(decIn, bOut); + decIn.close(); + result = decIn.getResult(); + isTrue(Arrays.areEqual("orange".toCharArray(), result.getDecryptionPassphrase())); + isEncodingEqual(encryptionSessionKey.getKey(), result.getSessionKey().getKey()); + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + } + + private void roundTripV4KeyEncryptedMessageAlice() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.addEncryptionCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.ALICE_CERT)); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream enc = gen.open(bOut); + enc.write(PLAINTEXT); + enc.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor(); + processor.addDecryptionKey(OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.ALICE_KEY)); + + OpenPGPMessageInputStream decIn = processor.process(bIn); + + bOut = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, bOut); + isEncodingEqual(bOut.toByteArray(), PLAINTEXT); + OpenPGPMessageInputStream.Result result = decIn.getResult(); + isEquals(MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithmTags.AES_256), + result.getEncryptionMethod()); + } + + private void roundTripV4KeyEncryptedMessageBob() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.addEncryptionCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.BOB_CERT)); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream enc = gen.open(bOut); + enc.write(PLAINTEXT); + enc.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor(); + processor.addDecryptionKey(OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.BOB_KEY)); + + OpenPGPMessageInputStream decIn = processor.process(bIn); + + bOut = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, bOut); + decIn.close(); + OpenPGPMessageInputStream.Result result = decIn.getResult(); + isEquals(MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithmTags.AES_256), + result.getEncryptionMethod()); + isEncodingEqual(bOut.toByteArray(), PLAINTEXT); + } + + private void roundTripV4KeyEncryptedMessageCarol() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.addEncryptionCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.CAROL_CERT)); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream enc = gen.open(bOut); + enc.write(PLAINTEXT); + enc.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor(); + processor.addDecryptionKey(OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.CAROL_KEY)); + + OpenPGPMessageInputStream decIn = processor.process(bIn); + + bOut = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, bOut); + decIn.close(); + OpenPGPMessageInputStream.Result result = decIn.getResult(); + isEquals(MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithmTags.AES_256), + result.getEncryptionMethod()); + isEncodingEqual(bOut.toByteArray(), PLAINTEXT); + } + + private void roundTripV6KeyEncryptedMessage() + throws IOException, PGPException + { + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY); + + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator() + .setArmored(true) + .addEncryptionCertificate(key) + .setIsPadded(false); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream msgOut = gen.open(bOut); + msgOut.write(PLAINTEXT); + msgOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor() + .addDecryptionKey(key); + + OpenPGPMessageInputStream plainIn = processor.process(bIn); + ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); + Streams.pipeAll(plainIn, plainOut); + plainIn.close(); + OpenPGPMessageInputStream.Result result = plainIn.getResult(); + isEquals(MessageEncryptionMechanism.aead(SymmetricKeyAlgorithmTags.AES_256, AEADAlgorithmTags.OCB), + result.getEncryptionMethod()); + + isEncodingEqual(PLAINTEXT, plainOut.toByteArray()); + } + + private void encryptWithV4V6KeyDecryptWithV4() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.addEncryptionCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.ALICE_CERT)); + gen.addEncryptionCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.V6_CERT)); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream enc = gen.open(bOut); + enc.write(PLAINTEXT); + enc.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor() + .addDecryptionKey(OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.ALICE_KEY)); + + OpenPGPMessageInputStream decIn = processor.process(bIn); + + bOut = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, bOut); + decIn.close(); + isEncodingEqual(bOut.toByteArray(), PLAINTEXT); + OpenPGPMessageInputStream.Result result = decIn.getResult(); + isEquals(MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithmTags.AES_256), + result.getEncryptionMethod()); + } + + private void encryptWithV4V6KeyDecryptWithV6() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.addEncryptionCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.ALICE_CERT)); + gen.addEncryptionCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.V6_CERT)); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream enc = gen.open(bOut); + enc.write(PLAINTEXT); + enc.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor() + .addDecryptionKey(OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY)); + + OpenPGPMessageInputStream decIn = processor.process(bIn); + + bOut = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, bOut); + isEncodingEqual(bOut.toByteArray(), PLAINTEXT); + OpenPGPMessageInputStream.Result result = decIn.getResult(); + isEquals(MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithmTags.AES_256), + result.getEncryptionMethod()); + } + + private void encryptDecryptWithLockedKey() + throws IOException, PGPException + { + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY_LOCKED); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + + OpenPGPMessageOutputStream encOut = new OpenPGPMessageGenerator() + .addEncryptionCertificate(key) + .open(bOut); + + encOut.write(PLAINTEXT); + encOut.close(); + + byte[] ciphertext = bOut.toByteArray(); + + // Provide passphrase and key together + ByteArrayInputStream bIn = new ByteArrayInputStream(ciphertext); + bOut = new ByteArrayOutputStream(); + OpenPGPMessageInputStream decIn = new OpenPGPMessageProcessor() + .addDecryptionKey(key, OpenPGPTestKeys.V6_KEY_LOCKED_PASSPHRASE.toCharArray()) + .process(bIn); + Streams.pipeAll(decIn, bOut); + decIn.close(); + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + OpenPGPMessageInputStream.Result result = decIn.getResult(); + PGPSessionKey sk = result.getSessionKey(); + + // Provide passphrase and key separate from another + bIn = new ByteArrayInputStream(ciphertext); + bOut = new ByteArrayOutputStream(); + decIn = new OpenPGPMessageProcessor() + .addDecryptionKey(key) + .addDecryptionKeyPassphrase(OpenPGPTestKeys.V6_KEY_LOCKED_PASSPHRASE.toCharArray()) + .process(bIn); + Streams.pipeAll(decIn, bOut); + decIn.close(); + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + result = decIn.getResult(); + isEncodingEqual(sk.getKey(), result.getSessionKey().getKey()); + + // Provide passphrase dynamically + bIn = new ByteArrayInputStream(ciphertext); + bOut = new ByteArrayOutputStream(); + decIn = new OpenPGPMessageProcessor() + .addDecryptionKey(key) + .setMissingOpenPGPKeyPassphraseProvider(k -> + OpenPGPTestKeys.V6_KEY_LOCKED_PASSPHRASE.toCharArray()) + .process(bIn); + Streams.pipeAll(decIn, bOut); + decIn.close(); + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + + result = decIn.getResult(); + isEncodingEqual(sk.getKey(), result.getSessionKey().getKey()); + } + + private void encryptDecryptWithMissingKey() + throws IOException, PGPException + { + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + + OutputStream encOut = new OpenPGPMessageGenerator() + .addEncryptionCertificate(key) + .open(bOut); + + encOut.write(PLAINTEXT); + encOut.close(); + + byte[] ciphertext = bOut.toByteArray(); + + // Provide passphrase and key together + ByteArrayInputStream bIn = new ByteArrayInputStream(ciphertext); + bOut = new ByteArrayOutputStream(); + OpenPGPMessageInputStream decIn = new OpenPGPMessageProcessor() + .setMissingOpenPGPKeyProvider(id -> key) + .process(bIn); + Streams.pipeAll(decIn, bOut); + decIn.close(); + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + + OpenPGPMessageInputStream.Result result = decIn.getResult(); + isEquals(key, result.getDecryptionKey().getCertificate()); + isNotNull(result.getSessionKey()); + } + + private void inlineSignWithV4KeyAlice() + throws IOException, PGPException + { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + OpenPGPKey aliceKey = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.ALICE_KEY); + gen.addSigningKey(aliceKey); + + OutputStream signOut = gen.open(bOut); + signOut.write(PLAINTEXT); + signOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + bOut = new ByteArrayOutputStream(); + + OpenPGPCertificate aliceCert = OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.ALICE_CERT); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor() + .addVerificationCertificate(aliceCert); + + OpenPGPMessageInputStream verifIn = processor.process(bIn); + Streams.pipeAll(verifIn, bOut); + verifIn.close(); + OpenPGPMessageInputStream.Result result = verifIn.getResult(); + isEquals(MessageEncryptionMechanism.unencrypted(), result.getEncryptionMethod()); + List signatures = result.getSignatures(); + isEquals(1, signatures.size()); + OpenPGPSignature.OpenPGPDocumentSignature sig = signatures.get(0); + isEquals(aliceCert, sig.getIssuerCertificate()); + + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + } + + private void inlineSignWithV4KeyBob() + throws IOException, PGPException + { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + OpenPGPKey bobKey = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.BOB_KEY); + gen.addSigningKey(bobKey); + + OutputStream signOut = gen.open(bOut); + signOut.write(PLAINTEXT); + signOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + bOut = new ByteArrayOutputStream(); + + OpenPGPCertificate bobCert = OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.BOB_CERT); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor() + .addVerificationCertificate(bobCert); + + OpenPGPMessageInputStream verifIn = processor.process(bIn); + Streams.pipeAll(verifIn, bOut); + verifIn.close(); + OpenPGPMessageInputStream.Result result = verifIn.getResult(); + List signatures = result.getSignatures(); + isEquals(1, signatures.size()); + OpenPGPSignature.OpenPGPDocumentSignature sig = signatures.get(0); + isEquals(bobCert, sig.getIssuerCertificate()); + + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + } + + private void inlineSignWithV4KeyCarol() + throws PGPException, IOException + { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + OpenPGPKey carolKey = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.CAROL_KEY); + gen.addSigningKey(carolKey); + + OutputStream signOut = gen.open(bOut); + signOut.write(PLAINTEXT); + signOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + bOut = new ByteArrayOutputStream(); + + OpenPGPCertificate carolCert = OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.CAROL_CERT); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor() + .addVerificationCertificate(carolCert); + + OpenPGPMessageInputStream verifIn = processor.process(bIn); + Streams.pipeAll(verifIn, bOut); + verifIn.close(); + OpenPGPMessageInputStream.Result result = verifIn.getResult(); + List signatures = result.getSignatures(); + isEquals(1, signatures.size()); + OpenPGPSignature.OpenPGPDocumentSignature sig = signatures.get(0); + isEquals(carolCert, sig.getIssuerCertificate()); + + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + } + + private void inlineSignWithV6Key() + throws PGPException, IOException + { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + OpenPGPKey v6Key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY); + gen.addSigningKey(v6Key); + + OutputStream signOut = gen.open(bOut); + signOut.write(PLAINTEXT); + signOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + bOut = new ByteArrayOutputStream(); + + OpenPGPCertificate v6Cert = OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.V6_CERT); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor() + .addVerificationCertificate(v6Cert); + + OpenPGPMessageInputStream verifIn = processor.process(bIn); + Streams.pipeAll(verifIn, bOut); + verifIn.close(); + OpenPGPMessageInputStream.Result result = verifIn.getResult(); + List signatures = result.getSignatures(); + isEquals(1, signatures.size()); + OpenPGPSignature.OpenPGPDocumentSignature sig = signatures.get(0); + isEquals(v6Cert, sig.getIssuerCertificate()); + + isEncodingEqual(PLAINTEXT, bOut.toByteArray()); + } + + private void verifyMessageByRevokedKey() + throws PGPException, IOException + { + // Create a minimal signed message + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.ALICE_KEY); + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + gen.addSigningKey(key); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream oOut = gen.open(bOut); + oOut.write("Hello, World!\n".getBytes()); + oOut.close(); + + // Load the certificate and import its revocation signature + OpenPGPCertificate cert = OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.ALICE_CERT); + cert = OpenPGPCertificate.join(cert, OpenPGPTestKeys.ALICE_REVOCATION_CERT); + + // Process the signed message using the revoked key + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor(); + processor.addVerificationCertificate(cert); + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageInputStream oIn = processor.process(bIn); + Streams.drain(oIn); + oIn.close(); + + OpenPGPMessageInputStream.Result result = oIn.getResult(); + OpenPGPSignature.OpenPGPDocumentSignature sig = result.getSignatures().get(0); + // signature is no valid + isFalse(sig.isValid()); + } + + private void incompleteMessageProcessing() + throws IOException, PGPException + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator() + .addEncryptionCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.ALICE_CERT)) + .addSigningKey(OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.BOB_KEY)); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream out = gen.open(bOut); + + out.write("Some Data".getBytes(StandardCharsets.UTF_8)); + out.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = new OpenPGPMessageProcessor() + .addVerificationCertificate(OpenPGPCertificate.fromAsciiArmor(OpenPGPTestKeys.BOB_CERT)) + .addDecryptionKey(OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.ALICE_KEY)); + OpenPGPMessageInputStream in = processor.process(bIn); + + // read a single byte (not the entire message) + in.read(); + + in.close(); + OpenPGPMessageInputStream.Result result = in.getResult(); + OpenPGPSignature.OpenPGPDocumentSignature sig = result.getSignatures().get(0); + isFalse(sig.isValid()); + } + + public static void main(String[] args) + { + runTest(new OpenPGPMessageProcessorTest()); + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/StackPassphraseCallback.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/StackPassphraseCallback.java new file mode 100644 index 0000000000..33c8463ba8 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/StackPassphraseCallback.java @@ -0,0 +1,38 @@ +package org.bouncycastle.openpgp.api.test; + +import org.bouncycastle.openpgp.api.MissingPassphraseCallback; + +import java.util.Collection; +import java.util.Collections; +import java.util.Stack; + +/** + * Test implementation of {@link MissingPassphraseCallback} which provides passphrases by popping + * them from a provided {@link Stack}. + */ +public class StackPassphraseCallback + implements MissingPassphraseCallback +{ + private final Stack passphases; + + public StackPassphraseCallback(char[] passphrase) + { + this(Collections.singleton(passphrase)); + } + + public StackPassphraseCallback(Collection passphrases) + { + this.passphases = new Stack<>(); + this.passphases.addAll(passphrases); + } + + @Override + public char[] getPassphrase() + { + if (passphases.isEmpty()) + { + return null; + } + return passphases.pop(); + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/StaticV6OpenPGPMessageGeneratorTest.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/StaticV6OpenPGPMessageGeneratorTest.java new file mode 100644 index 0000000000..c838f34ce9 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/StaticV6OpenPGPMessageGeneratorTest.java @@ -0,0 +1,92 @@ +package org.bouncycastle.openpgp.api.test; + +import org.bouncycastle.bcpg.test.AbstractPacketTest; +import org.bouncycastle.openpgp.KeyIdentifier; +import org.bouncycastle.openpgp.OpenPGPTestKeys; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPMessageGenerator; +import org.bouncycastle.openpgp.api.OpenPGPMessageOutputStream; +import org.bouncycastle.util.encoders.Hex; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +public class StaticV6OpenPGPMessageGeneratorTest + extends AbstractPacketTest +{ + KeyIdentifier signingKeyIdentifier = new KeyIdentifier( + Hex.decode("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9")); + KeyIdentifier encryptionKeyIdentifier = new KeyIdentifier( + Hex.decode("12C83F1E706F6308FE151A417743A1F033790E93E9978488D1DB378DA9930885")); + + @Override + public String getName() + { + return "StaticV6OpenPGPMessageGeneratorTest"; + } + + @Override + public void performTest() + throws Exception + { + staticEncryptedMessage(); + staticSignedMessage(); + } + + private void staticEncryptedMessage() + throws IOException, PGPException + { + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY); + + OpenPGPMessageGenerator gen = getStaticGenerator() + .addEncryptionCertificate(key); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream pgOut = (OpenPGPMessageOutputStream) gen.open(bOut); + pgOut.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + pgOut.close(); + + System.out.println(bOut); + } + + private void staticSignedMessage() + throws IOException, PGPException + { + OpenPGPKey key = OpenPGPKey.fromAsciiArmor(OpenPGPTestKeys.V6_KEY); + OpenPGPMessageGenerator gen = getStaticGenerator() + .addSigningKey(key); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPMessageOutputStream pgOut = (OpenPGPMessageOutputStream) gen.open(bOut); + pgOut.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + pgOut.close(); + + System.out.println(bOut); + } + + /** + * Return a pre-configured {@link OpenPGPMessageGenerator} which has the complex logic of evaluating + * recipient keys to determine suitable subkeys, algorithms etc. swapped out for static configuration + * tailored to the V6 test key. + * + * @return static message generator + */ + public OpenPGPMessageGenerator getStaticGenerator() + { + OpenPGPMessageGenerator gen = new OpenPGPMessageGenerator(); + + gen.getConfiguration() + .setEncryptionKeySelector(keyRing -> Collections.singletonList(keyRing.getKey(encryptionKeyIdentifier))) + .setSigningKeySelector(keyRing -> Collections.singletonList(keyRing.getKey(signingKeyIdentifier))); + + return gen; + } + + public static void main(String[] args) + { + runTest(new StaticV6OpenPGPMessageGeneratorTest()); + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/RegressionTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/RegressionTest.java index 80fdbcfec7..e9c0977d3c 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/test/RegressionTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/RegressionTest.java @@ -3,6 +3,9 @@ import java.security.Security; import org.bouncycastle.bcpg.test.SignatureSubpacketsTest; +import org.bouncycastle.openpgp.api.test.OpenPGPMessageGeneratorTest; +import org.bouncycastle.openpgp.api.test.OpenPGPMessageProcessorTest; +import org.bouncycastle.openpgp.api.test.StaticV6OpenPGPMessageGeneratorTest; import org.bouncycastle.util.test.SimpleTest; import org.bouncycastle.util.test.Test; @@ -84,7 +87,11 @@ public class RegressionTest new PGPv5KeyTest(), new PGPv5MessageDecryptionTest(), - new PGPv6SignatureTest() + new PGPv6SignatureTest(), + + new OpenPGPMessageGeneratorTest(), + new OpenPGPMessageProcessorTest(), + new StaticV6OpenPGPMessageGeneratorTest() }; public static void main(String[] args)