diff --git a/pg/src/main/java/org/bouncycastle/bcpg/PublicKeyUtils.java b/pg/src/main/java/org/bouncycastle/bcpg/PublicKeyUtils.java new file mode 100644 index 0000000000..3ac93f3fa0 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/bcpg/PublicKeyUtils.java @@ -0,0 +1,54 @@ +package org.bouncycastle.bcpg; + +/** + * Utility methods related to OpenPGP public key algorithms. + */ +public class PublicKeyUtils +{ + + /** + * Return true, if the public key algorithm that corresponds to the given ID is capable of signing. + * @param publicKeyAlgorithm public key algorithm id + * @return true if algorithm can sign + */ + public static boolean isSigningAlgorithm(int publicKeyAlgorithm) + { + switch (publicKeyAlgorithm) + { + case PublicKeyAlgorithmTags.RSA_GENERAL: + case PublicKeyAlgorithmTags.RSA_SIGN: + case PublicKeyAlgorithmTags.DSA: + case PublicKeyAlgorithmTags.ECDSA: + case PublicKeyAlgorithmTags.ELGAMAL_GENERAL: + case PublicKeyAlgorithmTags.EDDSA_LEGACY: + case PublicKeyAlgorithmTags.Ed25519: + case PublicKeyAlgorithmTags.Ed448: + return true; + default: + return false; + } + } + + /** + * Return true, if the public key algorithm that corresponds to the given ID is capable of encryption. + * @param publicKeyAlgorithm public key algorithm id + * @return true if algorithm can encrypt + */ + public static boolean isEncryptionAlgorithm(int publicKeyAlgorithm) + { + switch (publicKeyAlgorithm) + { + case PublicKeyAlgorithmTags.RSA_GENERAL: + case PublicKeyAlgorithmTags.RSA_ENCRYPT: + case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT: + case PublicKeyAlgorithmTags.ECDH: + case PublicKeyAlgorithmTags.ELGAMAL_GENERAL: + case PublicKeyAlgorithmTags.DIFFIE_HELLMAN: + case PublicKeyAlgorithmTags.X25519: + case PublicKeyAlgorithmTags.X448: + return true; + default: + return false; + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPKeyPair.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPKeyPair.java index aad95acd7d..c29b4abaa4 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPKeyPair.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPKeyPair.java @@ -1,6 +1,8 @@ package org.bouncycastle.openpgp; import org.bouncycastle.bcpg.KeyIdentifier; +import org.bouncycastle.bcpg.PublicSubkeyPacket; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; /** * General class to handle JCA key pairs and convert them into OpenPGP ones. @@ -63,4 +65,22 @@ public PGPPrivateKey getPrivateKey() { return priv; } + + public PGPKeyPair asSubkey(KeyFingerPrintCalculator fingerPrintCalculator) + throws PGPException + { + if (pub.getPublicKeyPacket() instanceof PublicSubkeyPacket) + { + return this; // is already subkey + } + + PublicSubkeyPacket pubSubPkt = new PublicSubkeyPacket( + pub.getVersion(), + pub.getAlgorithm(), + pub.getCreationTime(), + pub.getPublicKeyPacket().getKey()); + return new PGPKeyPair( + new PGPPublicKey(pubSubPkt, fingerPrintCalculator), + new PGPPrivateKey(pub.getKeyID(), pubSubPkt, priv.getPrivateKeyDataPacket())); + } } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPKeyRingGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPKeyRingGenerator.java index 9f8f9d0249..2a259746b3 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPKeyRingGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPKeyRingGenerator.java @@ -4,6 +4,10 @@ import java.util.Iterator; import java.util.List; +import org.bouncycastle.asn1.cryptlib.CryptlibObjectIdentifiers; +import org.bouncycastle.bcpg.ECPublicBCPGKey; +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; import org.bouncycastle.bcpg.PublicSubkeyPacket; import org.bouncycastle.bcpg.SignatureSubpacket; import org.bouncycastle.bcpg.SignatureSubpacketTags; @@ -13,36 +17,36 @@ import org.bouncycastle.openpgp.operator.PGPDigestCalculator; /** - * Generator for a PGP master and subkey ring. This class will generate + * Generator for a PGP primary and subkey ring. This class will generate * both the secret and public key rings */ public class PGPKeyRingGenerator { - List keys = new ArrayList(); + private final List keys = new ArrayList(); - private PBESecretKeyEncryptor keyEncryptor; - private PGPDigestCalculator checksumCalculator; - private PGPKeyPair masterKey; - private PGPSignatureSubpacketVector hashedPcks; - private PGPSignatureSubpacketVector unhashedPcks; - private PGPContentSignerBuilder keySignerBuilder; + private final PBESecretKeyEncryptor keyEncryptor; + private final PGPDigestCalculator checksumCalculator; + private final PGPKeyPair primaryKey; + private final PGPSignatureSubpacketVector hashedPcks; + private final PGPSignatureSubpacketVector unhashedPcks; + private final PGPContentSignerBuilder keySignerBuilder; /** * Create a new key ring generator. * - * @param certificationLevel - * @param masterKey + * @param certificationLevel certification level for user-ids + * @param primaryKey primary key * @param id id to associate with the key. * @param checksumCalculator key checksum calculator - * @param hashedPcks - * @param unhashedPcks - * @param keySignerBuilder builder for key certifications - will be initialised with master secret key. + * @param hashedPcks hashed signature subpackets + * @param unhashedPcks unhashed signature subpackets + * @param keySignerBuilder builder for key certifications - will be initialised with primary secret key. * @param keyEncryptor encryptor for secret subkeys. - * @throws PGPException + * @throws PGPException error during signature generation */ public PGPKeyRingGenerator( int certificationLevel, - PGPKeyPair masterKey, + PGPKeyPair primaryKey, String id, PGPDigestCalculator checksumCalculator, PGPSignatureSubpacketVector hashedPcks, @@ -51,28 +55,28 @@ public PGPKeyRingGenerator( PBESecretKeyEncryptor keyEncryptor) throws PGPException { - this.masterKey = masterKey; + this.primaryKey = sanitizeKeyPair(primaryKey); this.keyEncryptor = keyEncryptor; this.checksumCalculator = checksumCalculator; this.keySignerBuilder = keySignerBuilder; this.hashedPcks = hashedPcks; this.unhashedPcks = unhashedPcks; - keys.add(new PGPSecretKey(certificationLevel, masterKey, id, checksumCalculator, hashedPcks, unhashedPcks, keySignerBuilder, keyEncryptor)); + keys.add(new PGPSecretKey(certificationLevel, primaryKey, id, checksumCalculator, hashedPcks, unhashedPcks, keySignerBuilder, keyEncryptor)); } /** * Create a new key ring generator without a user-id, but instead with a primary key carrying a direct-key signature. - * @param masterKey primary key + * @param primaryKey primary key * @param checksumCalculator checksum calculator * @param hashedPcks hashed signature subpackets * @param unhashedPcks unhashed signature subpackets * @param keySignerBuilder signer builder * @param keyEncryptor key encryptor - * @throws PGPException + * @throws PGPException error during signature generation */ public PGPKeyRingGenerator( - PGPKeyPair masterKey, + PGPKeyPair primaryKey, PGPDigestCalculator checksumCalculator, PGPSignatureSubpacketVector hashedPcks, PGPSignatureSubpacketVector unhashedPcks, @@ -80,7 +84,7 @@ public PGPKeyRingGenerator( PBESecretKeyEncryptor keyEncryptor) throws PGPException { - this.masterKey = masterKey; + this.primaryKey = sanitizeKeyPair(primaryKey); this.keyEncryptor = keyEncryptor; this.checksumCalculator = checksumCalculator; this.keySignerBuilder = keySignerBuilder; @@ -91,7 +95,7 @@ public PGPKeyRingGenerator( try { - sigGen = new PGPSignatureGenerator(keySignerBuilder); + sigGen = new PGPSignatureGenerator(keySignerBuilder, primaryKey.getPublicKey()); } catch (Exception e) { @@ -99,15 +103,15 @@ public PGPKeyRingGenerator( } // Keyring without user-id needs direct key sig - sigGen.init(PGPSignature.DIRECT_KEY, masterKey.getPrivateKey()); + sigGen.init(PGPSignature.DIRECT_KEY, primaryKey.getPrivateKey()); sigGen.setHashedSubpackets(hashedPcks); sigGen.setUnhashedSubpackets(unhashedPcks); - PGPSecretKey secretKey = new PGPSecretKey(masterKey.getPrivateKey(), masterKey.getPublicKey(), checksumCalculator, true, keyEncryptor); + PGPSecretKey secretKey = new PGPSecretKey(primaryKey.getPrivateKey(), primaryKey.getPublicKey(), checksumCalculator, true, keyEncryptor); PGPPublicKey publicKey = secretKey.getPublicKey(); try { - PGPSignature certification = sigGen.generateCertification(masterKey.getPublicKey()); + PGPSignature certification = sigGen.generateCertification(primaryKey.getPublicKey()); publicKey = PGPPublicKey.addCertification(publicKey, certification); } @@ -123,15 +127,15 @@ public PGPKeyRingGenerator( /** * Create a new key ring generator based on an original secret key ring. The default hashed/unhashed sub-packets - * for subkey signatures will be inherited from the first signature on the master key (other than CREATION-TIME + * for subkey signatures will be inherited from the first signature on the primary key (other than CREATION-TIME * which will be ignored). * * @param originalSecretRing the secret key ring we want to add a subkeyto, - * @param secretKeyDecryptor a decryptor for the signing master key. + * @param secretKeyDecryptor a decryptor for the signing primary key. * @param checksumCalculator key checksum calculator - * @param keySignerBuilder builder for key certifications - will be initialised with master secret key. + * @param keySignerBuilder builder for key certifications - will be initialised with primary secret key. * @param keyEncryptor encryptor for secret subkeys. - * @throws PGPException + * @throws PGPException error during signature generation */ public PGPKeyRingGenerator( PGPSecretKeyRing originalSecretRing, @@ -141,14 +145,14 @@ public PGPKeyRingGenerator( PBESecretKeyEncryptor keyEncryptor) throws PGPException { - this.masterKey = new PGPKeyPair(originalSecretRing.getPublicKey(), - originalSecretRing.getSecretKey().extractPrivateKey(secretKeyDecryptor)); + this.primaryKey = sanitizeKeyPair(new PGPKeyPair(originalSecretRing.getPublicKey(), + originalSecretRing.getSecretKey().extractPrivateKey(secretKeyDecryptor))); this.keyEncryptor = keyEncryptor; this.checksumCalculator = checksumCalculator; this.keySignerBuilder = keySignerBuilder; PGPSignature certSig = (PGPSignature)originalSecretRing.getPublicKey().getSignatures().next(); - List hashedVec = new ArrayList(); + List hashedVec = new ArrayList(); PGPSignatureSubpacketVector existing = certSig.getHashedSubPackets(); for (int i = 0; i != existing.size(); i++) { @@ -159,41 +163,98 @@ public PGPKeyRingGenerator( hashedVec.add(existing.packets[i]); } this.hashedPcks = new PGPSignatureSubpacketVector( - (SignatureSubpacket[])hashedVec.toArray(new SignatureSubpacket[hashedVec.size()])); + hashedVec.toArray(new SignatureSubpacket[0])); this.unhashedPcks = certSig.getUnhashedSubPackets(); keys.addAll(originalSecretRing.keys); } + private PGPKeyPair sanitizeKeyPair(PGPKeyPair keyPair) + throws PGPException + { + PGPPublicKey pubKey = keyPair.getPublicKey(); + if (pubKey.getVersion() == PublicKeyPacket.VERSION_6) + { + if (pubKey.getAlgorithm() == PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT || + pubKey.getAlgorithm() == PublicKeyAlgorithmTags.ELGAMAL_GENERAL) + { + // https://www.rfc-editor.org/rfc/rfc9580.html#name-elgamal + throw new PGPException("An implementation MUST NOT generate v6 ElGamal keys"); + } + + if (pubKey.getAlgorithm() == PublicKeyAlgorithmTags.RSA_GENERAL) + { + // https://www.rfc-editor.org/rfc/rfc9580.html#name-rsa + if (pubKey.getBitStrength() < 3072) + { + throw new PGPException("An implementation MUST NOT generate v6 RSA keys of a size less than 3072 bits."); + } + } + + if (pubKey.getAlgorithm() == PublicKeyAlgorithmTags.RSA_ENCRYPT || + pubKey.getAlgorithm() == PublicKeyAlgorithmTags.RSA_SIGN) + { + throw new PGPException("An implementation MUST NOT generate v6 RSA keys of type RSA_ENCRYPT/RSA_SIGN"); + } + + if (pubKey.getAlgorithm() == PublicKeyAlgorithmTags.DSA) + { + // https://www.rfc-editor.org/rfc/rfc9580.html#name-dsa + throw new PGPException("An implementation MUST NOT generate v6 DSA keys."); + } + + if (pubKey.getAlgorithm() == PublicKeyAlgorithmTags.EDDSA_LEGACY) + { + throw new PGPException("An implementation MUST NOT generate v6 EDDSA_LEGACY keys."); + } + + if (pubKey.getAlgorithm() == PublicKeyAlgorithmTags.ECDH) + { + ECPublicBCPGKey ecKey = (ECPublicBCPGKey) pubKey.publicPk.getKey(); + if (ecKey.getCurveOID().equals(CryptlibObjectIdentifiers.curvey25519)) + { + throw new PGPException("An implementation MUST NOT generate v6 ECDH keys over Curve25519Legacy."); + } + } + + if (pubKey.getAlgorithm() == PublicKeyAlgorithmTags.DIFFIE_HELLMAN) + { + throw new PGPException("An implementation MUST NOT generate v6 Diffie-Hellman keys."); + } + } + + return keyPair; + } + /** * Add a sub key to the key ring to be generated with default certification and inheriting - * the hashed/unhashed packets of the master key. + * the hashed/unhashed packets of the primary key. * * @param keyPair the key pair to add. - * @throws PGPException + * @throws PGPException error during signature generation */ public void addSubKey( PGPKeyPair keyPair) throws PGPException { - addSubKey(keyPair, hashedPcks, unhashedPcks); + addSubKey(sanitizeKeyPair(keyPair), hashedPcks, unhashedPcks); } /** * Add a sub key to the key ring to be generated with default certification and inheriting - * the hashed/unhashed packets of the master key. If bindingSignerBldr is not null it will be used to add a Primary Key Binding + * the hashed/unhashed packets of the primary key. If bindingSignerBldr is not null it will be used to add a Primary Key Binding * signature (type 0x19) into the hashedPcks for the key (required for signing subkeys). * * @param keyPair the key pair to add. * @param bindingSignerBldr provide a signing builder to create the Primary Key signature. - * @throws PGPException + * @throws PGPException error during signature generation */ public void addSubKey( PGPKeyPair keyPair, PGPContentSignerBuilder bindingSignerBldr) throws PGPException { - addSubKey(keyPair, hashedPcks, unhashedPcks, bindingSignerBldr); + addSubKey(sanitizeKeyPair(keyPair), hashedPcks, unhashedPcks, bindingSignerBldr); } /** @@ -203,7 +264,7 @@ public void addSubKey( * @param keyPair public/private key pair. * @param hashedPcks hashed packet values to be included in certification. * @param unhashedPcks unhashed packets values to be included in certification. - * @throws PGPException + * @throws PGPException error during signature generation */ public void addSubKey( PGPKeyPair keyPair, @@ -211,7 +272,7 @@ public void addSubKey( PGPSignatureSubpacketVector unhashedPcks) throws PGPException { - addSubKey(keyPair, hashedPcks, unhashedPcks, null); + addSubKey(sanitizeKeyPair(keyPair), hashedPcks, unhashedPcks, null); } /** @@ -223,7 +284,7 @@ public void addSubKey( * @param hashedPcks hashed packet values to be included in certification. * @param unhashedPcks unhashed packets values to be included in certification. * @param bindingSignerBldr provide a signing builder to create the Primary Key signature. - * @throws PGPException + * @throws PGPException error during signature generation */ public void addSubKey( PGPKeyPair keyPair, @@ -232,26 +293,27 @@ public void addSubKey( PGPContentSignerBuilder bindingSignerBldr) throws PGPException { + sanitizeKeyPair(keyPair); try { // // generate the certification // - PGPSignatureGenerator sGen = new PGPSignatureGenerator(keySignerBuilder); + PGPSignatureGenerator sGen = new PGPSignatureGenerator(keySignerBuilder, primaryKey.getPublicKey()); - sGen.init(PGPSignature.SUBKEY_BINDING, masterKey.getPrivateKey()); + sGen.init(PGPSignature.SUBKEY_BINDING, primaryKey.getPrivateKey()); if (bindingSignerBldr != null) { // add primary key binding - PGPSignatureGenerator pGen = new PGPSignatureGenerator(bindingSignerBldr); + PGPSignatureGenerator pGen = new PGPSignatureGenerator(bindingSignerBldr, keyPair.getPublicKey()); pGen.init(PGPSignature.PRIMARYKEY_BINDING, keyPair.getPrivateKey()); PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(hashedPcks); spGen.addEmbeddedSignature(false, - pGen.generateCertification(masterKey.getPublicKey(), keyPair.getPublicKey())); + pGen.generateCertification(primaryKey.getPublicKey(), keyPair.getPublicKey())); sGen.setHashedSubpackets(spGen.generate()); } else @@ -261,9 +323,9 @@ public void addSubKey( sGen.setUnhashedSubpackets(unhashedPcks); - List subSigs = new ArrayList(); + List subSigs = new ArrayList(); - subSigs.add(sGen.generateCertification(masterKey.getPublicKey(), keyPair.getPublicKey())); + subSigs.add(sGen.generateCertification(primaryKey.getPublicKey(), keyPair.getPublicKey())); // replace the public key packet structure with a public subkey one. PGPPublicKey pubSubKey = new PGPPublicKey(keyPair.getPublicKey(), null, subSigs); @@ -299,14 +361,14 @@ public PGPSecretKeyRing generateSecretKeyRing() */ public PGPPublicKeyRing generatePublicKeyRing() { - Iterator it = keys.iterator(); - List pubKeys = new ArrayList(); - - pubKeys.add(((PGPSecretKey)it.next()).getPublicKey()); - + Iterator it = keys.iterator(); + List pubKeys = new ArrayList(); + + pubKeys.add((it.next()).getPublicKey()); + while (it.hasNext()) { - pubKeys.add(((PGPSecretKey)it.next()).getPublicKey()); + pubKeys.add((it.next()).getPublicKey()); } return new PGPPublicKeyRing(pubKeys); diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java index cd49368144..bbf298876a 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKey.java @@ -499,6 +499,13 @@ public byte[] getFingerprint() /** * Return the S2K usage associated with this key. + * This value indicates, how the secret key material is protected: + *
    + *
  • {@link SecretKeyPacket#USAGE_NONE}: Unprotected
  • + *
  • {@link SecretKeyPacket#USAGE_CHECKSUM}: Password-protected using malleable CFB (deprecated)
  • + *
  • {@link SecretKeyPacket#USAGE_SHA1}: Password-protected using CFB
  • + *
  • {@link SecretKeyPacket#USAGE_AEAD}: Password-protected using AEAD (recommended)
  • + *
* * @return the key's S2K usage */ @@ -992,7 +999,15 @@ public static PGPSecretKey copyWithNewPassword( SecretKeyPacket secret; - secret = generateSecretKeyPacket(!(key.secret instanceof SecretSubkeyPacket), key.secret.getPublicKeyPacket(), newEncAlgorithm, s2kUsage, s2k, iv, keyData); + if (newKeyEncryptor.getAeadAlgorithm() > 0) + { + s2kUsage = SecretKeyPacket.USAGE_AEAD; + secret = generateSecretKeyPacket(!(key.secret instanceof SecretSubkeyPacket), key.secret.getPublicKeyPacket(), newEncAlgorithm, newKeyEncryptor.getAeadAlgorithm(), s2kUsage, s2k, iv, keyData); + } + else + { + secret = generateSecretKeyPacket(!(key.secret instanceof SecretSubkeyPacket), key.secret.getPublicKeyPacket(), newEncAlgorithm, s2kUsage, s2k, iv, keyData); + } return new PGPSecretKey(secret, key.pub); } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKeyRing.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKeyRing.java index 98c1d82bf1..04c2191ac5 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKeyRing.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSecretKeyRing.java @@ -492,6 +492,22 @@ public int size() return keys.size(); } + /** + * Return the OpenPGP certificate (Transferable Public Key) of this key. + * + * @return certificate + */ + public PGPPublicKeyRing toCertificate() + { + List pubKeys = new ArrayList<>(); + Iterator it = getPublicKeys(); + while (it.hasNext()) + { + pubKeys.add(it.next()); + } + return new PGPPublicKeyRing(pubKeys); + } + public byte[] getEncoded() throws IOException { diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureSubpacketGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureSubpacketGenerator.java index dbf1e513cc..b4279676b7 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureSubpacketGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureSubpacketGenerator.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.List; @@ -38,7 +39,7 @@ */ public class PGPSignatureSubpacketGenerator { - List packets = new ArrayList(); + List packets = new ArrayList(); /** * Base constructor, creates an empty generator. @@ -56,10 +57,7 @@ public PGPSignatureSubpacketGenerator(PGPSignatureSubpacketVector sigSubV) { if (sigSubV != null) { - for (int i = 0; i != sigSubV.packets.length; i++) - { - packets.add(sigSubV.packets[i]); - } + packets.addAll(Arrays.asList(sigSubV.packets)); } } @@ -78,6 +76,18 @@ public void setRevocable(boolean isCritical, boolean isRevocable) packets.add(new Revocable(isCritical, isRevocable)); } + /** + * Specify, whether the signature should be marked as exportable. + * If this subpacket is missing, the signature is treated as being exportable. + * The subpacket is marked as critical, as is required (for non-exportable signatures) by the spec. + * + * @param isExportable true if the signature should be exportable, false otherwise. + */ + public void setExportable(boolean isExportable) + { + setExportable(true, isExportable); + } + /** * Specify, whether or not the signature should be marked as exportable. * If this subpacket is missing, the signature is treated as being exportable. @@ -119,6 +129,18 @@ public void setTrust(boolean isCritical, int depth, int trustAmount) packets.add(new TrustSignature(isCritical, depth, trustAmount)); } + /** + * Set the number of seconds a key is valid for after the time of its creation. A + * value of zero means the key never expires. + * The subpacket will be marked as critical, as is recommended by the spec. + * + * @param seconds seconds from key creation to expiration + */ + public void setKeyExpirationTime(long seconds) + { + setKeyExpirationTime(true, seconds); + } + /** * Set the number of seconds a key is valid for after the time of its creation. A * value of zero means the key never expires. @@ -131,6 +153,18 @@ public void setKeyExpirationTime(boolean isCritical, long seconds) packets.add(new KeyExpirationTime(isCritical, seconds)); } + /** + * Set the number of seconds a signature is valid for after the time of its creation. + * A value of zero means the signature never expires. + * The subpacket will be marked as critical, as is recommended by the spec. + *. + * @param seconds seconds from signature creation to expiration + */ + public void setSignatureExpirationTime(long seconds) + { + setSignatureExpirationTime(true, seconds); + } + /** * Set the number of seconds a signature is valid for after the time of its creation. * A value of zero means the signature never expires. @@ -143,6 +177,19 @@ public void setSignatureExpirationTime(boolean isCritical, long seconds) packets.add(new SignatureExpirationTime(isCritical, seconds)); } + /** + * Set the creation time for the signature. + * The subpacket will be marked as critical, as is recommended by the spec. + *

+ * Note: this overrides the generation of a creation time when the signature is + * generated. + * @param date date + */ + public void setSignatureCreationTime(Date date) + { + setSignatureCreationTime(true, date); + } + /** * Set the creation time for the signature. *

@@ -274,6 +321,18 @@ public void addPolicyURI(boolean isCritical, String policyUri) packets.add(new PolicyURI(isCritical, policyUri)); } + /** + * Set this keys key flags. + * See {@link PGPKeyFlags}. + * The subpacket will be marked as critical, as is recommended by the spec. + * + * @param flags flags + */ + public void setKeyFlags(int flags) + { + setKeyFlags(true, flags); + } + /** * Set this keys key flags. * See {@link PGPKeyFlags}. @@ -515,6 +574,19 @@ public void setIntendedRecipientFingerprint(boolean isCritical, PGPPublicKey pub addIntendedRecipientFingerprint(isCritical, publicKey); } + /** + * Adds a intended recipient fingerprint for an encrypted payload the signature is associated with. + * The subpacket will be marked as critical, as is recommended by the spec. + * + * @param publicKey the public key the encrypted payload was encrypted against. + */ + public void addIntendedRecipientFingerprint(PGPPublicKey publicKey) + { + // RFC9580 states, that the packet SHOULD be critical if generated in a v6 signature, + // but it doesn't harm to default to critical for any signature version + addIntendedRecipientFingerprint(true, publicKey); + } + /** * Adds a intended recipient fingerprint for an encrypted payload the signature is associated with. * @@ -549,6 +621,25 @@ public boolean removePacket(SignatureSubpacket packet) return packets.remove(packet); } + /** + * Remove all {@link SignatureSubpacket} objects of the given subpacketType from the underlying subpacket vector. + * @param subpacketType type to remove + * @return true if any packet was removed, false otherwise + */ + public boolean removePacketsOfType(int subpacketType) + { + boolean remove = false; + for (int i = packets.size() - 1; i >= 0; i--) + { + if (packets.get(i).getType() == subpacketType) + { + packets.remove(i); + remove = true; + } + } + return remove; + } + /** * Return true if a particular subpacket type exists. * @@ -558,9 +649,9 @@ public boolean removePacket(SignatureSubpacket packet) public boolean hasSubpacket( int type) { - for (int i = 0; i != packets.size(); i++) + for (SignatureSubpacket packet : packets) { - if (((SignatureSubpacket)packets.get(i)).getType() == type) + if (packet.getType() == type) { return true; } @@ -579,30 +670,30 @@ public boolean hasSubpacket( public SignatureSubpacket[] getSubpackets( int type) { - List list = new ArrayList(); + List list = new ArrayList<>(); - for (int i = 0; i != packets.size(); i++) + for (SignatureSubpacket packet : packets) { - if (((SignatureSubpacket)packets.get(i)).getType() == type) + if (packet.getType() == type) { - list.add(packets.get(i)); + list.add(packet); } } - return (SignatureSubpacket[])list.toArray(new SignatureSubpacket[]{}); + return list.toArray(new SignatureSubpacket[0]); } public PGPSignatureSubpacketVector generate() { return new PGPSignatureSubpacketVector( - (SignatureSubpacket[])packets.toArray(new SignatureSubpacket[packets.size()])); + packets.toArray(new SignatureSubpacket[0])); } private boolean contains(int type) { - for (int i = 0; i < packets.size(); ++i) + for (SignatureSubpacket packet : packets) { - if (((SignatureSubpacket)packets.get(i)).getType() == type) + if (packet.getType() == type) { return true; } @@ -610,6 +701,17 @@ private boolean contains(int type) return false; } + /** + * Adds a regular expression. + * The subpacket is marked as critical, as is recommended by the spec. + * + * @param regularExpression the regular expression + */ + public void addRegularExpression(String regularExpression) + { + addRegularExpression(true, regularExpression); + } + /** * Adds a regular expression. * diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureSubpacketVector.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureSubpacketVector.java index 10e36a478e..078faa49f6 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureSubpacketVector.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureSubpacketVector.java @@ -116,7 +116,7 @@ public boolean hasSubpacket( public SignatureSubpacket[] getSubpackets( int type) { - List list = new ArrayList(); + List list = new ArrayList<>(); for (int i = 0; i != packets.length; i++) { @@ -126,20 +126,20 @@ public SignatureSubpacket[] getSubpackets( } } - return (SignatureSubpacket[])list.toArray(new SignatureSubpacket[]{}); + return list.toArray(new SignatureSubpacket[0]); } public PGPSignatureList getEmbeddedSignatures() throws PGPException { SignatureSubpacket[] sigs = getSubpackets(SignatureSubpacketTags.EMBEDDED_SIGNATURE); - ArrayList l = new ArrayList(); + ArrayList l = new ArrayList<>(); - for (int i = 0; i < sigs.length; i++) + for (SignatureSubpacket sig : sigs) { try { - l.add(new PGPSignature(SignaturePacket.fromByteArray(sigs[i].getData()))); + l.add(new PGPSignature(SignaturePacket.fromByteArray(sig.getData()))); } catch (IOException e) { @@ -147,7 +147,7 @@ public PGPSignatureList getEmbeddedSignatures() } } - return new PGPSignatureList((PGPSignature[])l.toArray(new PGPSignature[l.size()])); + return new PGPSignatureList(l.toArray(new PGPSignature[0])); } public NotationData[] getNotationDataOccurrences() @@ -179,7 +179,7 @@ public NotationData[] getNotationDataOccurences() public NotationData[] getNotationDataOccurrences(String notationName) { NotationData[] notations = getNotationDataOccurrences(); - List notationsWithName = new ArrayList(); + List notationsWithName = new ArrayList<>(); for (int i = 0; i != notations.length; i++) { NotationData notation = notations[i]; @@ -188,7 +188,7 @@ public NotationData[] getNotationDataOccurrences(String notationName) notationsWithName.add(notation); } } - return (NotationData[])notationsWithName.toArray(new NotationData[0]); + return notationsWithName.toArray(new NotationData[0]); } public long getIssuerKeyID() diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/KeyPairGeneratorCallback.java b/pg/src/main/java/org/bouncycastle/openpgp/api/KeyPairGeneratorCallback.java new file mode 100644 index 0000000000..07d47f85b8 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/KeyPairGeneratorCallback.java @@ -0,0 +1,22 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator; + +/** + * Callback to generate a {@link PGPKeyPair} from a {@link PGPKeyPairGenerator} instance. + */ +@FunctionalInterface +public interface KeyPairGeneratorCallback +{ + /** + * Generate a {@link PGPKeyPair} by calling a factory method on a given generator instance. + * + * @param generator PGPKeyPairGenerator + * @return generated key pair + * @throws PGPException + */ + PGPKeyPair generateFrom(PGPKeyPairGenerator generator) + throws PGPException; +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPV6KeyGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPV6KeyGenerator.java new file mode 100644 index 0000000000..950f02b818 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPV6KeyGenerator.java @@ -0,0 +1,1146 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.bcpg.AEADAlgorithmTags; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.PublicKeyUtils; +import org.bouncycastle.bcpg.PublicSubkeyPacket; +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.KeyFlags; +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptorFactory; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator; +import org.bouncycastle.openpgp.operator.PGPKeyPairGeneratorProvider; +import org.bouncycastle.util.Arrays; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * High-level generator class for OpenPGP v6 keys. + */ +public class OpenPGPV6KeyGenerator +{ + /** + * Hash algorithm for key signatures if no other one is provided during construction. + */ + public static final int DEFAULT_SIGNATURE_HASH_ALGORITHM = HashAlgorithmTags.SHA3_512; + + // SECONDS + private static final long SECONDS_PER_MINUTE = 60; + private static final long SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE; + private static final long SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR; + private static final long SECONDS_PER_YEAR = 365 * SECONDS_PER_DAY; + + /** + * Standard AEAD encryption preferences (SEIPDv2). + * By default, only announce support for OCB + AES. + */ + public static SignatureSubpacketsFunction DEFAULT_AEAD_ALGORITHM_PREFERENCES = subpackets -> + { + subpackets.removePacketsOfType(SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS); + subpackets.setPreferredAEADCiphersuites(PreferredAEADCiphersuites.builder(false) + .addCombination(SymmetricKeyAlgorithmTags.AES_256, AEADAlgorithmTags.OCB) + .addCombination(SymmetricKeyAlgorithmTags.AES_192, AEADAlgorithmTags.OCB) + .addCombination(SymmetricKeyAlgorithmTags.AES_128, AEADAlgorithmTags.OCB)); + return subpackets; + }; + + /** + * Standard symmetric-key encryption preferences (SEIPDv1). + * By default, announce support for AES. + */ + public static SignatureSubpacketsFunction DEFAULT_SYMMETRIC_KEY_PREFERENCES = subpackets -> + { + subpackets.removePacketsOfType(SignatureSubpacketTags.PREFERRED_SYM_ALGS); + subpackets.setPreferredSymmetricAlgorithms(false, new int[] { + SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192, SymmetricKeyAlgorithmTags.AES_128 + }); + return subpackets; + }; + + /** + * Standard signature hash algorithm preferences. + * By default, only announce SHA3 and SHA2 algorithms. + */ + public static SignatureSubpacketsFunction DEFAULT_HASH_ALGORITHM_PREFERENCES = subpackets -> + { + subpackets.removePacketsOfType(SignatureSubpacketTags.PREFERRED_HASH_ALGS); + subpackets.setPreferredHashAlgorithms(false, new int[] { + HashAlgorithmTags.SHA3_512, HashAlgorithmTags.SHA3_256, + HashAlgorithmTags.SHA512, HashAlgorithmTags.SHA384, HashAlgorithmTags.SHA256 + }); + return subpackets; + }; + + /** + * Standard compression algorithm preferences. + * By default, announce support for all known algorithms. + */ + public static SignatureSubpacketsFunction DEFAULT_COMPRESSION_ALGORITHM_PREFERENCES = subpackets -> + { + subpackets.removePacketsOfType(SignatureSubpacketTags.PREFERRED_COMP_ALGS); + subpackets.setPreferredCompressionAlgorithms(false, new int[] { + CompressionAlgorithmTags.UNCOMPRESSED, CompressionAlgorithmTags.ZIP, + CompressionAlgorithmTags.ZLIB, CompressionAlgorithmTags.BZIP2 + }); + return subpackets; + }; + + /** + * Standard features to announce. + * By default, announce SEIPDv1 (modification detection) and SEIPDv2. + */ + public static SignatureSubpacketsFunction DEFAULT_FEATURES = subpackets -> + { + subpackets.removePacketsOfType(SignatureSubpacketTags.FEATURES); + subpackets.setFeature(false, (byte) (Features.FEATURE_MODIFICATION_DETECTION | Features.FEATURE_SEIPD_V2)); + return subpackets; + }; + + /** + * Standard signature subpackets for signing subkey's binding signatures. + * Sets the keyflag subpacket to SIGN_DATA. + */ + public static SignatureSubpacketsFunction SIGNING_SUBKEY_SUBPACKETS = subpackets -> + { + subpackets.removePacketsOfType(SignatureSubpacketTags.KEY_FLAGS); + subpackets.setKeyFlags(true, KeyFlags.SIGN_DATA); + return subpackets; + }; + + /** + * Standard signature subpackets for encryption subkey's binding signatures. + * Sets the keyflag subpacket to ENCRYPT_STORAGE|ENCRYPT_COMMS. + */ + public static SignatureSubpacketsFunction ENCRYPTION_SUBKEY_SUBPACKETS = subpackets -> + { + subpackets.removePacketsOfType(SignatureSubpacketTags.KEY_FLAGS); + subpackets.setKeyFlags(true, KeyFlags.ENCRYPT_STORAGE | KeyFlags.ENCRYPT_COMMS); + return subpackets; + }; + + /** + * Standard signature subpackets for the direct-key signature. + * Sets default features, hash-, compression-, symmetric-key-, and AEAD algorithm preferences. + */ + public static SignatureSubpacketsFunction DIRECT_KEY_SIGNATURE_SUBPACKETS = subpackets -> + { + subpackets = DEFAULT_FEATURES.apply(subpackets); + subpackets = DEFAULT_HASH_ALGORITHM_PREFERENCES.apply(subpackets); + subpackets = DEFAULT_COMPRESSION_ALGORITHM_PREFERENCES.apply(subpackets); + subpackets = DEFAULT_SYMMETRIC_KEY_PREFERENCES.apply(subpackets); + subpackets = DEFAULT_AEAD_ALGORITHM_PREFERENCES.apply(subpackets); + return subpackets; + }; + + private final Implementation impl; // contains BC or JCA/JCE implementations + private final Configuration conf; + + /** + * Generate a new OpenPGP key generator for v6 keys. + * + * @param kpGenProvider key pair generator provider + * @param contentSignerBuilderProvider content signer builder provider + * @param digestCalculatorProvider digest calculator provider + * @param keyEncryptionBuilderProvider secret key encryption builder provider (AEAD) + * @param keyFingerPrintCalculator calculator for key fingerprints + * @param creationTime key creation time + */ + public OpenPGPV6KeyGenerator( + PGPKeyPairGeneratorProvider kpGenProvider, + PGPContentSignerBuilderProvider contentSignerBuilderProvider, + PGPDigestCalculatorProvider digestCalculatorProvider, + PBESecretKeyEncryptorFactory keyEncryptionBuilderProvider, + KeyFingerPrintCalculator keyFingerPrintCalculator, + Date creationTime) + { + this.impl = new Implementation(kpGenProvider, contentSignerBuilderProvider, digestCalculatorProvider, keyEncryptionBuilderProvider, keyFingerPrintCalculator); + this.conf = new Configuration(new Date((creationTime.getTime() / 1000) * 1000)); + } + + /** + * Generate an OpenPGP key consisting of a certify-only primary key, + * a dedicated signing-subkey and dedicated encryption-subkey. + * The key will carry the provided user-id and be protected using the provided passphrase. + * See {@link PGPKeyPairGenerator#generatePrimaryKey()} for the primary key type, + * {@link PGPKeyPairGenerator#generateSigningSubkey()} for the signing-subkey type and + * {@link PGPKeyPairGenerator#generateEncryptionSubkey()} for the encryption-subkey key type. + * + * @param userId user id + * @param passphrase nullable passphrase. + * @return OpenPGP key + * @throws PGPException if the key cannot be generated + */ + public PGPSecretKeyRing classicKey(String userId, char[] passphrase) + throws PGPException + { + return withPrimaryKey() + .addUserId(userId) + .addSigningSubkey() + .addEncryptionSubkey() + .build(passphrase); + } + + /** + * Generate an OpenPGP key consisting of an Ed25519 certify-only primary key, + * a dedicated Ed25519 sign-only subkey and dedicated X25519 encryption-only subkey. + * The key will carry the provided user-id and be protected using the provided passphrase. + * + * @param userId user id + * @param passphrase nullable passphrase + * @return OpenPGP key + * @throws PGPException if the key cannot be generated + */ + public PGPSecretKeyRing ed25519x25519Key(String userId, char[] passphrase) + throws PGPException + { + return withPrimaryKey(PGPKeyPairGenerator::generateEd25519KeyPair) + .addSigningSubkey(PGPKeyPairGenerator::generateEd25519KeyPair) + .addEncryptionSubkey(PGPKeyPairGenerator::generateX25519KeyPair) + .addUserId(userId) + .build(passphrase); + } + + + /** + * Generate an OpenPGP key consisting of an Ed448 certify-only primary key, + * a dedicated Ed448 sign-only subkey and dedicated X448 encryption-only subkey. + * The key will carry the provided user-id and be protected using the provided passphrase. + * + * @param userId user id + * @param passphrase nullable passphrase + * @return OpenPGP key + * @throws PGPException if the key cannot be generated + */ + public PGPSecretKeyRing ed448x448Key(String userId, char[] passphrase) + throws PGPException + { + return withPrimaryKey(PGPKeyPairGenerator::generateEd448KeyPair) + .addSigningSubkey(PGPKeyPairGenerator::generateEd448KeyPair) + .addEncryptionSubkey(PGPKeyPairGenerator::generateX448KeyPair) + .addUserId(userId) + .build(passphrase); + } + + /** + * Generate a sign-only OpenPGP key. + * The key consists of a single, user-id-less primary key, which is capable of signing and certifying. + * See {@link PGPKeyPairGenerator#generatePrimaryKey()} for the key type. + * + * @param passphrase nullable passphrase to protect the key with + * @return sign-only (+certify) OpenPGP key + * @throws PGPException if the key cannot be generated + */ + public PGPSecretKeyRing signOnlyKey(char[] passphrase) + throws PGPException + { + return signOnlyKey(passphrase, null); + } + + /** + * Generate a sign-only OpenPGP key. + * The key consists of a single, user-id-less primary key, which is capable of signing and certifying. + * It carries a single direct-key signature with signing-related preferences whose subpackets can be + * modified by providing a {@link SignatureSubpacketsFunction}. + * + * @param passphrase nullable passphrase to protect the key with + * @param userSubpackets callback to modify the direct-key signature subpackets with + * @return sign-only (+certify) OpenPGP key + * @throws PGPException if the key cannot be generated + */ + public PGPSecretKeyRing signOnlyKey( + char[] passphrase, + SignatureSubpacketsFunction userSubpackets) + throws PGPException + { + PGPKeyPair primaryKeyPair = impl.kpGenProvider.get(PublicKeyPacket.VERSION_6, conf.keyCreationTime) + .generatePrimaryKey(); + PBESecretKeyEncryptor encryptor = impl.keyEncryptorBuilderProvider + .build(passphrase, primaryKeyPair.getPublicKey().getPublicKeyPacket()); + return signOnlyKey(primaryKeyPair, encryptor, userSubpackets); + } + + /** + * Generate a sign-only OpenPGP key. + * The key consists of a single, user-id-less primary key, which is capable of signing and certifying. + * It carries a single direct-key signature with signing-related preferences whose subpackets can be + * modified by providing a {@link SignatureSubpacketsFunction}. + * + * @param primaryKeyPair signing-capable primary key + * @param keyEncryptor nullable encryptor to protect the primary key with + * @param userSubpackets callback to modify the direct-key signature subpackets with + * @return sign-only (+certify) OpenPGP key + * @throws PGPException if the key cannot be generated + */ + public PGPSecretKeyRing signOnlyKey( + PGPKeyPair primaryKeyPair, + PBESecretKeyEncryptor keyEncryptor, + SignatureSubpacketsFunction userSubpackets) + throws PGPException + { + if (primaryKeyPair.getPublicKey().getPublicKeyPacket() instanceof PublicSubkeyPacket) + { + throw new IllegalArgumentException("Primary key MUST NOT consist of subkey packet."); + } + + return primaryKeyWithDirectKeySig(primaryKeyPair, + baseSubpackets -> + { + // remove unrelated subpackets not needed for sign-only keys + baseSubpackets.removePacketsOfType(SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS); + baseSubpackets.removePacketsOfType(SignatureSubpacketTags.PREFERRED_SYM_ALGS); + baseSubpackets.removePacketsOfType(SignatureSubpacketTags.PREFERRED_COMP_ALGS); + + // replace key flags -> CERTIFY_OTHER|SIGN_DATA + baseSubpackets.removePacketsOfType(SignatureSubpacketTags.KEY_FLAGS); + baseSubpackets.setKeyFlags(true, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA); + return baseSubpackets; + }, + userSubpackets, // apply user-provided subpacket changes + keyEncryptor) + .build(); + } + + /** + * Generate an OpenPGP key with a certification-capable primary key. + * See {@link PGPKeyPairGenerator#generatePrimaryKey()} for the primary key type + * + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey withPrimaryKey() + throws PGPException + { + return withPrimaryKey((SignatureSubpacketsFunction) null); + } + + public WithPrimaryKey withPrimaryKey( + KeyPairGeneratorCallback keyGenCallback) + throws PGPException + { + return withPrimaryKey(keyGenCallback, null); + } + + /** + * Generate an OpenPGP key with a certification-capable primary key. + * See {@link PGPKeyPairGenerator#generatePrimaryKey()} for the primary key type + * The key will carry a direct-key signature, whose subpackets can be modified by overriding the + * given {@link SignatureSubpacketsFunction}. + * + * @param directKeySubpackets nullable callback to modify the direct-key signatures subpackets + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey withPrimaryKey( + SignatureSubpacketsFunction directKeySubpackets) + throws PGPException + { + return withPrimaryKey( + PGPKeyPairGenerator::generatePrimaryKey, + directKeySubpackets); + } + + /** + * Generate an OpenPGP key with a certification-capable primary key. + * The {@link KeyPairGeneratorCallback} can be used to specify the primary key type. + * The key will carry a direct-key signature, whose subpackets can be modified by overriding the + * given {@link SignatureSubpacketsFunction}. + * + * @param keyGenCallback callback to specify the primary key type + * @param directKeySubpackets nullable callback to modify the direct-key signatures subpackets + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey withPrimaryKey( + KeyPairGeneratorCallback keyGenCallback, + SignatureSubpacketsFunction directKeySubpackets) + throws PGPException + { + return withPrimaryKey(keyGenCallback, directKeySubpackets, null); + } + + /** + * Generate an OpenPGP key with a certification-capable primary key. + * The key will carry a direct-key signature, whose subpackets can be modified by overriding the + * given {@link SignatureSubpacketsFunction}. + * + * @param primaryKeyPair primary key + * @param directKeySubpackets nullable callback to modify the direct-key signatures subpackets + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey withPrimaryKey( + PGPKeyPair primaryKeyPair, + SignatureSubpacketsFunction directKeySubpackets) + throws PGPException + { + return withPrimaryKey( + primaryKeyPair, + directKeySubpackets, + null); + } + + /** + * Generate an OpenPGP key with a certification-capable primary key. + * The {@link KeyPairGeneratorCallback} can be used to specify the primary key type. + * The key will carry a direct-key signature, whose subpackets can be modified by overriding the + * given {@link SignatureSubpacketsFunction}. + * IMPORTANT: The custom primary key passphrase will only be used, if in the final step the key is retrieved + * using {@link WithPrimaryKey#build()}. + * If instead {@link WithPrimaryKey#build(char[])} is used, the key-specific passphrase is overwritten with the argument + * passed into {@link WithPrimaryKey#build(char[])}. + * + * @param keyGenCallback callback to specify the primary key type + * @param directKeySubpackets nullable callback to modify the direct-key signatures subpackets + * @param passphrase nullable passphrase to protect the primary key with + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey withPrimaryKey( + KeyPairGeneratorCallback keyGenCallback, + SignatureSubpacketsFunction directKeySubpackets, + char[] passphrase) + throws PGPException + { + PGPKeyPair primaryKeyPair = keyGenCallback.generateFrom( + impl.kpGenProvider.get(PublicKeyPacket.VERSION_6, conf.keyCreationTime)); + PBESecretKeyEncryptor keyEncryptor = impl.keyEncryptorBuilderProvider + .build(passphrase, primaryKeyPair.getPublicKey().getPublicKeyPacket()); + return withPrimaryKey(primaryKeyPair, directKeySubpackets, keyEncryptor); + } + + /** + * Generate an OpenPGP key with a certification-capable primary key. + * The {@link KeyPairGeneratorCallback} can be used to specify the primary key type. + * The key will carry a direct-key signature, whose subpackets can be modified by overriding the + * given {@link SignatureSubpacketsFunction}. + * IMPORTANT: The custom keyEncryptor will only be used, if in the final step the key is retrieved + * using {@link WithPrimaryKey#build()}. + * If instead {@link WithPrimaryKey#build(char[])} is used, the key-specific encryptor is overwritten with + * an encryptor built from the argument passed into {@link WithPrimaryKey#build(char[])}. + * + * @param primaryKeyPair primary key + * @param directKeySubpackets nullable callback to modify the direct-key signatures subpackets + * @param keyEncryptor nullable encryptor to protect the primary key with + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey withPrimaryKey( + PGPKeyPair primaryKeyPair, + SignatureSubpacketsFunction directKeySubpackets, + PBESecretKeyEncryptor keyEncryptor) + throws PGPException + { + if (primaryKeyPair.getPublicKey().getPublicKeyPacket() instanceof PublicSubkeyPacket) + { + throw new IllegalArgumentException("Primary key MUST NOT consist of subkey packet."); + } + + if (!PublicKeyUtils.isSigningAlgorithm(primaryKeyPair.getPublicKey().getAlgorithm())) + { + throw new PGPException("Primary key MUST use signing-capable algorithm."); + } + + return primaryKeyWithDirectKeySig( + primaryKeyPair, + subpackets -> + { + subpackets.setIssuerFingerprint(true, primaryKeyPair.getPublicKey()); + subpackets.setSignatureCreationTime(conf.keyCreationTime); + subpackets.setKeyFlags(true, KeyFlags.CERTIFY_OTHER); + subpackets = DIRECT_KEY_SIGNATURE_SUBPACKETS.apply(subpackets); + subpackets.setKeyExpirationTime(false, 5 * SECONDS_PER_YEAR); + return subpackets; + }, + directKeySubpackets, + keyEncryptor); + } + + /** + * Specify the primary key and attach a direct-key signature. + * The direct-key signature's subpackets will first be modified using the baseSubpackets callback, followed + * by the customSubpackets callback. + * If both baseSubpackets and customSubpackets are null, no direct-key signature will be attached. + * + * @param primaryKeyPair primary key pair + * @param baseSubpackets base signature subpackets callback + * @param customSubpackets user-provided signature subpackets callback + * @param keyEncryptor key encryptor + * @return builder + * @throws PGPException if the key cannot be generated + */ + private WithPrimaryKey primaryKeyWithDirectKeySig( + PGPKeyPair primaryKeyPair, + SignatureSubpacketsFunction baseSubpackets, + SignatureSubpacketsFunction customSubpackets, + PBESecretKeyEncryptor keyEncryptor) + throws PGPException + { + if (baseSubpackets != null || customSubpackets != null) + { + // DK sig + PGPSignatureGenerator dkSigGen = new PGPSignatureGenerator( + impl.contentSignerBuilderProvider.get(primaryKeyPair.getPublicKey()), + primaryKeyPair.getPublicKey()); + dkSigGen.init(PGPSignature.DIRECT_KEY, primaryKeyPair.getPrivateKey()); + + PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator(); + // application-dictated subpackets + if (baseSubpackets != null) + { + subpackets = baseSubpackets.apply(subpackets); + } + + // Allow the user to modify the direct-key signature subpackets + if (customSubpackets != null) + { + subpackets = customSubpackets.apply(subpackets); + } + + dkSigGen.setHashedSubpackets(subpackets.generate()); + + PGPSignature dkSig = dkSigGen.generateCertification(primaryKeyPair.getPublicKey()); + primaryKeyPair = new PGPKeyPair( + PGPPublicKey.addCertification(primaryKeyPair.getPublicKey(), dkSig), + primaryKeyPair.getPrivateKey()); + } + + Key primaryKey = new Key(primaryKeyPair, keyEncryptor); + + return new WithPrimaryKey(impl, conf, primaryKey); + } + + /** + * Intermediate builder class. + * Constructs an OpenPGP key from a specified primary key. + */ + public static class WithPrimaryKey + { + + private final Implementation impl; + private final Configuration conf; + private Key primaryKey; + private final List subkeys = new ArrayList(); + + /** + * Builder. + * + * @param implementation cryptographic implementation + * @param configuration key configuration + * @param primaryKey specified primary key + */ + private WithPrimaryKey(Implementation implementation, Configuration configuration, Key primaryKey) + { + this.impl = implementation; + this.conf = configuration; + this.primaryKey = primaryKey; + } + + /** + * Attach a User-ID with a positive certification to the key. + * + * @param userId user-id + * @return builder + * @throws PGPException if the user-id cannot be added + */ + public WithPrimaryKey addUserId(String userId) + throws PGPException + { + return addUserId(userId, null); + } + + /** + * Attach a User-ID with a positive certification to the key. + * The subpackets of the user-id certification can be modified using the userIdSubpackets callback. + * + * @param userId user-id + * @param userIdSubpackets callback to modify the certification subpackets + * @return builder + * @throws PGPException if the user-id cannot be added + */ + public WithPrimaryKey addUserId( + String userId, + SignatureSubpacketsFunction userIdSubpackets) + throws PGPException + { + return addUserId(userId, PGPSignature.POSITIVE_CERTIFICATION, userIdSubpackets); + } + + /** + * Attach a User-ID with a positive certification to the key. + * The subpackets of the user-id certification can be modified using the userIdSubpackets callback. + * + * @param userId user-id + * @param certificationType signature type + * @param userIdSubpackets callback to modify the certification subpackets + * @return builder + * @throws PGPException if the user-id cannot be added + */ + public WithPrimaryKey addUserId( + String userId, + int certificationType, + SignatureSubpacketsFunction userIdSubpackets) + throws PGPException + { + if (userId == null || userId.trim().isEmpty()) + { + throw new IllegalArgumentException("User-ID cannot be null or empty."); + } + + if (!PGPSignature.isCertification(certificationType)) + { + throw new IllegalArgumentException("Signature type MUST be a certification type (0x10 - 0x13)"); + } + + PGPSignatureGenerator uidSigGen = new PGPSignatureGenerator( + impl.contentSignerBuilderProvider.get(primaryKey.pair.getPublicKey()), + primaryKey.pair.getPublicKey()); + uidSigGen.init(certificationType, primaryKey.pair.getPrivateKey()); + + PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator(); + subpackets.setIssuerFingerprint(true, primaryKey.pair.getPublicKey()); + subpackets.setSignatureCreationTime(conf.keyCreationTime); + + if (userIdSubpackets != null) + { + subpackets = userIdSubpackets.apply(subpackets); + } + uidSigGen.setHashedSubpackets(subpackets.generate()); + + PGPSignature uidSig = uidSigGen.generateCertification(userId, primaryKey.pair.getPublicKey()); + PGPPublicKey pubKey = PGPPublicKey.addCertification(primaryKey.pair.getPublicKey(), userId, uidSig); + primaryKey = new Key(new PGPKeyPair(pubKey, primaryKey.pair.getPrivateKey()), primaryKey.encryptor); + + return this; + } + + /** + * Add an encryption-capable subkey to the OpenPGP key. + * See {@link PGPKeyPairGenerator#generateEncryptionSubkey()} for the key type. + * + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addEncryptionSubkey() + throws PGPException + { + return addEncryptionSubkey(PGPKeyPairGenerator::generateEncryptionSubkey); + } + + /** + * Add an encryption-capable subkey to the OpenPGP key. + * The type of the subkey can be decided by implementing the {@link KeyPairGeneratorCallback}. + * + * @param keyGenCallback callback to decide the encryption subkey type + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addEncryptionSubkey(KeyPairGeneratorCallback keyGenCallback) + throws PGPException + { + return addEncryptionSubkey(keyGenCallback, (char[]) null); + } + + /** + * Add an encryption-capable subkey to the OpenPGP key. + * The type of the subkey can be decided by implementing the {@link KeyPairGeneratorCallback}. + * The binding signature can be modified by implementing the {@link SignatureSubpacketsFunction}. + * + * @param generatorCallback callback to specify the encryption key type. + * @param bindingSubpacketsCallback nullable callback to modify the binding signature subpackets + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addEncryptionSubkey( + KeyPairGeneratorCallback generatorCallback, + SignatureSubpacketsFunction bindingSubpacketsCallback) + throws PGPException + { + PGPKeyPairGenerator generator = impl.kpGenProvider.get( + primaryKey.pair.getPublicKey().getVersion(), + conf.keyCreationTime + ); + PGPKeyPair subkey = generatorCallback.generateFrom(generator); + + return addEncryptionSubkey(subkey, bindingSubpacketsCallback, null); + } + + /** + * Add an encryption-capable subkey to the OpenPGP key. + * The subkey will be protected using the provided subkey passphrase. + * IMPORTANT: The custom subkey passphrase will only be used, if in the final step the key is retrieved + * using {@link #build()}. + * If instead {@link #build(char[])} is used, the key-specific passphrase is overwritten with the argument + * passed into {@link #build(char[])}. + * See {@link PGPKeyPairGenerator#generateEncryptionSubkey()} for the key type. + * + * @param passphrase nullable subkey passphrase + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addEncryptionSubkey(char[] passphrase) + throws PGPException + { + return addEncryptionSubkey(PGPKeyPairGenerator::generateEncryptionSubkey, passphrase); + } + + /** + * Add an encryption-capable subkey to the OpenPGP key. + * The key type can be specified by overriding {@link KeyPairGeneratorCallback}. + * The subkey will be protected using the provided subkey passphrase. + * IMPORTANT: The custom subkey passphrase will only be used, if in the final step the key is retrieved + * using {@link #build()}. + * If instead {@link #build(char[])} is used, the key-specific passphrase is overwritten with the argument + * passed into {@link #build(char[])}. + * + * @param keyGenCallback callback to specify the key type + * @param passphrase nullable passphrase for the encryption subkey + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addEncryptionSubkey(KeyPairGeneratorCallback keyGenCallback, + char[] passphrase) + throws PGPException + { + return addEncryptionSubkey(keyGenCallback, null, passphrase); + } + + /** + * Add an encryption-capable subkey to the OpenPGP key. + * The key type can be specified by overriding {@link KeyPairGeneratorCallback}. + * The binding signatures subpackets can be modified by overriding the {@link SignatureSubpacketsFunction}. + * The subkey will be protected using the provided subkey passphrase. + * IMPORTANT: The custom subkey passphrase will only be used, if in the final step the key is retrieved + * using {@link #build()}. + * If instead {@link #build(char[])} is used, the key-specific passphrase is overwritten with the argument + * passed into {@link #build(char[])}. + * + * @param keyGenCallback callback to specify the key type + * @param bindingSignatureCallback nullable callback to modify the binding signature subpackets + * @param passphrase nullable passphrase for the encryption subkey + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addEncryptionSubkey(KeyPairGeneratorCallback keyGenCallback, + SignatureSubpacketsFunction bindingSignatureCallback, + char[] passphrase) + throws PGPException + { + PGPKeyPair subkey = keyGenCallback.generateFrom( + impl.kpGenProvider.get(PublicKeyPacket.VERSION_6, conf.keyCreationTime)); + subkey = subkey.asSubkey(impl.keyFingerprintCalculator); + PBESecretKeyEncryptor keyEncryptor = impl.keyEncryptorBuilderProvider.build(passphrase, subkey.getPublicKey().getPublicKeyPacket()); + return addEncryptionSubkey(subkey, bindingSignatureCallback, keyEncryptor); + } + + + /** + * Add an encryption-capable subkey to the OpenPGP key. + * IMPORTANT: The custom key encryptor will only be used, if in the final step the key is retrieved + * using {@link #build()}. + * If instead {@link #build(char[])} is used, the key-specific encryptor is overwritten with an encryptor + * built from the argument passed into {@link #build(char[])}. + * + * @param encryptionSubkey encryption subkey + * @param bindingSubpacketsCallback nullable callback to modify the subkey binding signature subpackets + * @param keyEncryptor nullable encryptor to encrypt the encryption subkey + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addEncryptionSubkey( + PGPKeyPair encryptionSubkey, + SignatureSubpacketsFunction bindingSubpacketsCallback, + PBESecretKeyEncryptor keyEncryptor) + throws PGPException + { + if (!(encryptionSubkey.getPublicKey().getPublicKeyPacket() instanceof PublicSubkeyPacket)) + { + throw new IllegalArgumentException("Encryption subkey MUST NOT consist of a primary key packet."); + } + + if (!PublicKeyUtils.isEncryptionAlgorithm(encryptionSubkey.getPublicKey().getAlgorithm())) + { + throw new PGPException("Encryption key MUST use encryption-capable algorithm."); + } + // generate binding signature + PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator(); + subpackets.setIssuerFingerprint(true, primaryKey.pair.getPublicKey()); + subpackets.setSignatureCreationTime(conf.keyCreationTime); + subpackets = ENCRYPTION_SUBKEY_SUBPACKETS.apply(subpackets); + + // allow subpacket customization + if (bindingSubpacketsCallback != null) + { + subpackets = bindingSubpacketsCallback.apply(subpackets); + } + + PGPSignatureGenerator bindingSigGen = new PGPSignatureGenerator( + impl.contentSignerBuilderProvider.get(primaryKey.pair.getPublicKey()), + primaryKey.pair.getPublicKey()); + bindingSigGen.init(PGPSignature.SUBKEY_BINDING, primaryKey.pair.getPrivateKey()); + bindingSigGen.setHashedSubpackets(subpackets.generate()); + + PGPSignature bindingSig = bindingSigGen.generateCertification(primaryKey.pair.getPublicKey(), encryptionSubkey.getPublicKey()); + PGPPublicKey publicSubkey = PGPPublicKey.addCertification(encryptionSubkey.getPublicKey(), bindingSig); + Key subkey = new Key(new PGPKeyPair(publicSubkey, encryptionSubkey.getPrivateKey()), keyEncryptor); + subkeys.add(subkey); + return this; + } + + /** + * Add a signing-capable subkey to the OpenPGP key. + * The binding signature will contain a primary-key back-signature. + * See {@link PGPKeyPairGenerator#generateSigningSubkey()} for the key type. + * + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addSigningSubkey() + throws PGPException + { + return addSigningSubkey(PGPKeyPairGenerator::generateSigningSubkey); + } + + /** + * Add a signing-capable subkey to the OpenPGP key. + * The binding signature will contain a primary-key back-signature. + * The key type can be specified by overriding {@link KeyPairGeneratorCallback}. + * + * @param keyGenCallback callback to specify the signing-subkey type + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addSigningSubkey(KeyPairGeneratorCallback keyGenCallback) + throws PGPException + { + return addSigningSubkey(keyGenCallback, null); + } + + /** + * Add a signing-capable subkey to the OpenPGP key. + * See {@link PGPKeyPairGenerator#generateSigningSubkey()} for the key type. + * The binding signature will contain a primary-key back-signature. + * IMPORTANT: The custom subkey passphrase will only be used, if in the final step the key is retrieved + * using {@link #build()}. + * If instead {@link #build(char[])} is used, the key-specific passphrase is overwritten with the argument + * passed into {@link #build(char[])}. + * + * @param passphrase nullable passphrase + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addSigningSubkey(char[] passphrase) + throws PGPException + { + return addSigningSubkey(PGPKeyPairGenerator::generateSigningSubkey, passphrase); + } + + /** + * Add a signing-capable subkey to the OpenPGP key. + * The signing-key type can be specified by overriding the {@link KeyPairGeneratorCallback}. + * The binding signature will contain a primary-key back-signature. + * IMPORTANT: The custom subkey passphrase will only be used, if in the final step the key is retrieved + * using {@link #build()}. + * If instead {@link #build(char[])} is used, the key-specific passphrase is overwritten with the argument + * passed into {@link #build(char[])}. + * + * @param keyGenCallback callback to specify the signing-key type + * @param passphrase nullable passphrase + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addSigningSubkey(KeyPairGeneratorCallback keyGenCallback, + char[] passphrase) + throws PGPException + { + return addSigningSubkey(keyGenCallback, null, null, passphrase); + } + + /** + * Add a signing-capable subkey to the OpenPGP key. + * The signing-key type can be specified by overriding the {@link KeyPairGeneratorCallback}. + * The binding signature will contain a primary-key back-signature. + * The contents of the binding signature(s) can be modified by overriding the respective + * {@link SignatureSubpacketsFunction} instances. + * IMPORTANT: The custom subkey passphrase will only be used, if in the final step the key is retrieved + * using {@link #build()}. + * If instead {@link #build(char[])} is used, the key-specific passphrase is overwritten with the argument + * passed into {@link #build(char[])}. + * + * @param keyGenCallback callback to specify the signing-key type + * @param bindingSignatureCallback callback to modify the contents of the signing subkey binding signature + * @param backSignatureCallback callback to modify the contents of the embedded primary key binding signature + * @param passphrase nullable passphrase + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addSigningSubkey(KeyPairGeneratorCallback keyGenCallback, + SignatureSubpacketsFunction bindingSignatureCallback, + SignatureSubpacketsFunction backSignatureCallback, + char[] passphrase) + throws PGPException + { + PGPKeyPair subkey = keyGenCallback.generateFrom(impl.kpGenProvider.get(PublicKeyPacket.VERSION_6, conf.keyCreationTime)); + subkey = subkey.asSubkey(impl.keyFingerprintCalculator); + PBESecretKeyEncryptor keyEncryptor = impl.keyEncryptorBuilderProvider.build(passphrase, subkey.getPublicKey().getPublicKeyPacket()); + return addSigningSubkey(subkey, bindingSignatureCallback, backSignatureCallback, keyEncryptor); + } + + /** + * Add a signing-capable subkey to the OpenPGP key. + * The signing-key type can be specified by overriding the {@link KeyPairGeneratorCallback}. + * The binding signature will contain a primary-key back-signature. + * The contents of the binding signature(s) can be modified by overriding the respective + * {@link SignatureSubpacketsFunction} instances. + * IMPORTANT: The custom key encryptor will only be used, if in the final step the key is retrieved + * using {@link #build()}. + * If instead {@link #build(char[])} is used, the key-specific encryptor is overwritten with an encryptor + * built from the argument passed into {@link #build(char[])}. + * + * @param signingSubkey signing subkey + * @param bindingSignatureCallback callback to modify the contents of the signing subkey binding signature + * @param backSignatureCallback callback to modify the contents of the embedded primary key binding signature + * @param keyEncryptor nullable encryptor to protect the signing subkey + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey addSigningSubkey(PGPKeyPair signingSubkey, + SignatureSubpacketsFunction bindingSignatureCallback, + SignatureSubpacketsFunction backSignatureCallback, + PBESecretKeyEncryptor keyEncryptor) + throws PGPException + { + if (!(signingSubkey.getPublicKey().getPublicKeyPacket() instanceof PublicSubkeyPacket)) + { + throw new IllegalArgumentException("Signing subkey MUST NOT consist of primary key packet."); + } + + if (!PublicKeyUtils.isSigningAlgorithm(signingSubkey.getPublicKey().getAlgorithm())) + { + throw new PGPException("Signing key MUST use signing-capable algorithm."); + } + + PGPSignatureSubpacketGenerator backSigSubpackets = new PGPSignatureSubpacketGenerator(); + backSigSubpackets.setIssuerFingerprint(true, signingSubkey.getPublicKey()); + backSigSubpackets.setSignatureCreationTime(conf.keyCreationTime); + if (backSignatureCallback != null) + { + backSigSubpackets = backSignatureCallback.apply(backSigSubpackets); + } + + PGPSignatureSubpacketGenerator bindingSigSubpackets = new PGPSignatureSubpacketGenerator(); + bindingSigSubpackets.setIssuerFingerprint(true, primaryKey.pair.getPublicKey()); + bindingSigSubpackets.setSignatureCreationTime(conf.keyCreationTime); + + bindingSigSubpackets = SIGNING_SUBKEY_SUBPACKETS.apply(bindingSigSubpackets); + + PGPSignatureGenerator backSigGen = new PGPSignatureGenerator( + impl.contentSignerBuilderProvider.get(signingSubkey.getPublicKey()), + signingSubkey.getPublicKey()); + backSigGen.init(PGPSignature.PRIMARYKEY_BINDING, signingSubkey.getPrivateKey()); + backSigGen.setHashedSubpackets(backSigSubpackets.generate()); + PGPSignature backSig = backSigGen.generateCertification( + primaryKey.pair.getPublicKey(), signingSubkey.getPublicKey()); + + try + { + bindingSigSubpackets.addEmbeddedSignature(false, backSig); + } + catch (IOException e) + { + throw new PGPException("Cannot embed back-signature.", e); + } + + if (bindingSignatureCallback != null) + { + bindingSigSubpackets = bindingSignatureCallback.apply(bindingSigSubpackets); + } + + PGPSignatureGenerator bindingSigGen = new PGPSignatureGenerator( + impl.contentSignerBuilderProvider.get(primaryKey.pair.getPublicKey()), + primaryKey.pair.getPublicKey()); + bindingSigGen.init(PGPSignature.SUBKEY_BINDING, primaryKey.pair.getPrivateKey()); + bindingSigGen.setHashedSubpackets(bindingSigSubpackets.generate()); + + PGPSignature bindingSig = bindingSigGen.generateCertification( + primaryKey.pair.getPublicKey(), signingSubkey.getPublicKey()); + + PGPPublicKey signingPubKey = PGPPublicKey.addCertification(signingSubkey.getPublicKey(), bindingSig); + signingSubkey = new PGPKeyPair(signingPubKey, signingSubkey.getPrivateKey()); + subkeys.add(new Key(signingSubkey, keyEncryptor)); + + return this; + } + + /** + * Build the {@link PGPSecretKeyRing OpenPGP key}, allowing individual passphrases for the subkeys. + * + * @return OpenPGP key + * @throws PGPException if the key cannot be generated + */ + public PGPSecretKeyRing build() + throws PGPException + { + PGPSecretKey primarySecretKey = new PGPSecretKey( + primaryKey.pair.getPrivateKey(), + primaryKey.pair.getPublicKey(), + impl.digestCalculatorProvider.get(HashAlgorithmTags.SHA1), + true, + primaryKey.encryptor); + List keys = new ArrayList<>(); + keys.add(primarySecretKey); + + for (Key key : subkeys) + { + PGPSecretKey subkey = new PGPSecretKey( + key.pair.getPrivateKey(), + key.pair.getPublicKey(), + impl.digestCalculatorProvider.get(HashAlgorithmTags.SHA1), + false, + key.encryptor); + keys.add(subkey); + } + + return new PGPSecretKeyRing(keys); + } + + /** + * Build the {@link PGPSecretKeyRing OpenPGP key} using a single passphrase used to protect all subkeys. + * The passphrase will override whichever key protectors were specified in previous builder steps. + * + * @param passphrase nullable passphrase + * @return OpenPGP key + * @throws PGPException if the key cannot be generated + */ + public PGPSecretKeyRing build(char[] passphrase) + throws PGPException + { + PBESecretKeyEncryptor primaryKeyEncryptor = impl.keyEncryptorBuilderProvider + .build(passphrase, primaryKey.pair.getPublicKey().getPublicKeyPacket()); + sanitizeKeyEncryptor(primaryKeyEncryptor); + PGPSecretKey primarySecretKey = new PGPSecretKey( + primaryKey.pair.getPrivateKey(), + primaryKey.pair.getPublicKey(), + impl.digestCalculatorProvider.get(HashAlgorithmTags.SHA1), + true, + primaryKeyEncryptor); + List keys = new ArrayList<>(); + keys.add(primarySecretKey); + + for (Key key : subkeys) + { + PBESecretKeyEncryptor subkeyEncryptor = impl.keyEncryptorBuilderProvider + .build(passphrase, key.pair.getPublicKey().getPublicKeyPacket()); + sanitizeKeyEncryptor(subkeyEncryptor); + PGPSecretKey subkey = new PGPSecretKey( + key.pair.getPrivateKey(), + key.pair.getPublicKey(), + impl.digestCalculatorProvider.get(HashAlgorithmTags.SHA1), + false, + subkeyEncryptor); + keys.add(subkey); + } + + if (passphrase != null) + { + Arrays.fill(passphrase, (char) 0); + } + + return new PGPSecretKeyRing(keys); + } + + protected void sanitizeKeyEncryptor(PBESecretKeyEncryptor keyEncryptor) + { + if (keyEncryptor == null) + { + // Unprotected is okay + return; + } + + S2K s2k = keyEncryptor.getS2K(); + if (s2k.getType() == S2K.SIMPLE || s2k.getType() == S2K.SALTED) + { + throw new IllegalArgumentException("S2K specifiers SIMPLE and SALTED are not allowed for secret key encryption."); + } + else if (s2k.getType() == S2K.ARGON_2) + { + if (keyEncryptor.getAeadAlgorithm() == 0) + { + throw new IllegalArgumentException("Argon2 MUST be used with AEAD."); + } + } + } + } + + /** + * Bundle implementation-specific provider classes. + */ + private static class Implementation + { + final PGPKeyPairGeneratorProvider kpGenProvider; + final PGPContentSignerBuilderProvider contentSignerBuilderProvider; + final PGPDigestCalculatorProvider digestCalculatorProvider; + final PBESecretKeyEncryptorFactory keyEncryptorBuilderProvider; + final KeyFingerPrintCalculator keyFingerprintCalculator; + + public Implementation(PGPKeyPairGeneratorProvider keyPairGeneratorProvider, + PGPContentSignerBuilderProvider contentSignerBuilderProvider, + PGPDigestCalculatorProvider digestCalculatorProvider, + PBESecretKeyEncryptorFactory keyEncryptorBuilderProvider, + KeyFingerPrintCalculator keyFingerPrintCalculator) + { + this.kpGenProvider = keyPairGeneratorProvider; + this.contentSignerBuilderProvider = contentSignerBuilderProvider; + this.digestCalculatorProvider = digestCalculatorProvider; + this.keyEncryptorBuilderProvider = keyEncryptorBuilderProvider; + this.keyFingerprintCalculator = keyFingerPrintCalculator; + } + } + + /** + * Bundle configuration-specific data. + */ + private static class Configuration + { + final Date keyCreationTime; + + public Configuration(Date keyCreationTime) + { + this.keyCreationTime = keyCreationTime; + } + } + + /** + * Tuple of a {@link PGPKeyPair} and (nullable) {@link PBESecretKeyEncryptor}. + */ + private static class Key + { + private final PGPKeyPair pair; + private final PBESecretKeyEncryptor encryptor; + + public Key(PGPKeyPair key, PBESecretKeyEncryptor encryptor) + { + this.pair = key; + this.encryptor = encryptor; + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/SignatureSubpacketsFunction.java b/pg/src/main/java/org/bouncycastle/openpgp/api/SignatureSubpacketsFunction.java new file mode 100644 index 0000000000..177954b692 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/SignatureSubpacketsFunction.java @@ -0,0 +1,28 @@ +package org.bouncycastle.openpgp.api; + +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; + +/** + * Callback to modify the contents of a {@link PGPSignatureSubpacketGenerator}. + * The {@link OpenPGPV6KeyGenerator} already prepopulates the hashed subpacket areas of signatures during + * key generation. This callback is useful to apply custom changes to the hashed subpacket area during the + * generation process. + */ +@FunctionalInterface +public interface SignatureSubpacketsFunction +{ + /** + * Apply some changes to the given {@link PGPSignatureSubpacketGenerator} and return the result. + * It is also possible to replace the whole {@link PGPSignatureSubpacketGenerator} by returning another instance. + * Tipp: In order to replace a subpacket, make sure to prevent duplicates by first removing subpackets + * of the same type using {@link PGPSignatureSubpacketGenerator#removePacketsOfType(int)}. + * To inspect the current contents of the generator, it is best to call + * {@link PGPSignatureSubpacketGenerator#generate()} and in turn inspect its contents using + * {@link PGPSignatureSubpacketVector#toArray()}. + * + * @param subpackets original subpackets + * @return non-null modified subpackets + */ + PGPSignatureSubpacketGenerator apply(PGPSignatureSubpacketGenerator subpackets); +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/bc/BcOpenPGPV6KeyGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/api/bc/BcOpenPGPV6KeyGenerator.java new file mode 100644 index 0000000000..a3de86fe0b --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/bc/BcOpenPGPV6KeyGenerator.java @@ -0,0 +1,79 @@ +package org.bouncycastle.openpgp.api.bc; + +import org.bouncycastle.openpgp.api.OpenPGPV6KeyGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcAEADSecretKeyEncryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcCFBSecretKeyEncryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilderProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPairGeneratorProvider; + +import java.util.Date; + +/** + * Bouncy Castle implementation of {@link OpenPGPV6KeyGenerator}. + */ +public class BcOpenPGPV6KeyGenerator + extends OpenPGPV6KeyGenerator +{ + + /** + * Create a new key generator for OpenPGP v6 keys. + */ + public BcOpenPGPV6KeyGenerator() + { + this(new Date()); + } + + /** + * Create a new key generator for OpenPGP v6 keys. + * The key creation time will be set to {@code creationTime} + * + * @param creationTime creation time of the generated OpenPGP key + */ + public BcOpenPGPV6KeyGenerator(Date creationTime) + { + this(DEFAULT_SIGNATURE_HASH_ALGORITHM, creationTime, true); + } + + /** + * Create a new key generator for OpenPGP v6 keys. + * Signatures on the key will be generated using the specified {@code signatureHashAlgorithm}. + * + * @param signatureHashAlgorithm ID of the hash algorithm to be used for signature generation + */ + public BcOpenPGPV6KeyGenerator(int signatureHashAlgorithm) + { + this(signatureHashAlgorithm, new Date(), true); + } + + /** + * Create a new OpenPGP key generator for v6 keys. + * + * @param signatureHashAlgorithm ID of the hash algorithm used for signatures on the key + * @param creationTime creation time of the key and signatures + */ + public BcOpenPGPV6KeyGenerator(int signatureHashAlgorithm, Date creationTime, boolean aeadProtection) + { + super( + new BcPGPKeyPairGeneratorProvider(), + new BcPGPContentSignerBuilderProvider(signatureHashAlgorithm), + new BcPGPDigestCalculatorProvider(), + keyEncryptorFactory(aeadProtection), + new BcKeyFingerprintCalculator(), + creationTime); + } + + private static PBESecretKeyEncryptorFactory keyEncryptorFactory(boolean aeadProtection) + { + if (aeadProtection) + { + return new BcAEADSecretKeyEncryptorFactory(); + } + else + { + return new BcCFBSecretKeyEncryptorFactory(); + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/jcajce/JcaOpenPGPV6KeyGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/api/jcajce/JcaOpenPGPV6KeyGenerator.java new file mode 100644 index 0000000000..bf4a902804 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/jcajce/JcaOpenPGPV6KeyGenerator.java @@ -0,0 +1,73 @@ +package org.bouncycastle.openpgp.api.jcajce; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.api.OpenPGPV6KeyGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptorFactory; +import org.bouncycastle.openpgp.operator.jcajce.JcaAEADSecretKeyEncryptorFactory; +import org.bouncycastle.openpgp.operator.jcajce.JcaCFBSecretKeyEncryptorFactory; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilderProvider; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPairGeneratorProvider; + +import java.security.Provider; +import java.util.Date; + +public class JcaOpenPGPV6KeyGenerator + extends OpenPGPV6KeyGenerator +{ + + public JcaOpenPGPV6KeyGenerator(Provider provider) + throws PGPException + { + this(new Date(), provider); + } + + public JcaOpenPGPV6KeyGenerator(Date creationTime, Provider provider) + throws PGPException + { + this(DEFAULT_SIGNATURE_HASH_ALGORITHM, creationTime, true, provider); + } + + public JcaOpenPGPV6KeyGenerator(int signatureHashAlgorithm, Provider provider) + throws PGPException + { + this(signatureHashAlgorithm, new Date(), true, provider); + } + + /** + * Create a new OpenPGP key generator for v6 keys. + * + * @param signatureHashAlgorithm ID of the hash algorithm used for signatures on the key + * @param creationTime creation time of the key and signatures + */ + public JcaOpenPGPV6KeyGenerator(int signatureHashAlgorithm, Date creationTime, boolean aeadProtection, Provider provider) + throws PGPException + { + super( + new JcaPGPKeyPairGeneratorProvider() + .setProvider(provider), + new JcaPGPContentSignerBuilderProvider(signatureHashAlgorithm) + .setSecurityProvider(provider), + new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(provider) + .build(), + keyEncryptorFactory(provider, aeadProtection), + new JcaKeyFingerprintCalculator(), + creationTime); + } + + private static PBESecretKeyEncryptorFactory keyEncryptorFactory(Provider provider, boolean aeadProtection) + throws PGPException + { + if (aeadProtection) + { + return new JcaAEADSecretKeyEncryptorFactory().setProvider(provider); + } + else + { + return new JcaCFBSecretKeyEncryptorFactory().setProvider(provider); + + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/AEADSecretKeyEncryptorBuilder.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/AEADSecretKeyEncryptorBuilder.java new file mode 100644 index 0000000000..26b1c033a4 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/AEADSecretKeyEncryptorBuilder.java @@ -0,0 +1,21 @@ +package org.bouncycastle.openpgp.operator; + +import org.bouncycastle.bcpg.PublicKeyPacket; + +/** + * Implementation provider for AEAD-based {@link PBESecretKeyEncryptor PBESecretKeyEncryptors}. + */ +public interface AEADSecretKeyEncryptorBuilder +{ + /** + * Build a new {@link PBESecretKeyEncryptor} using the given passphrase. + * Note: As the AEAD protection mechanism includes the public key packet of the key into the calculation, + * if the key you want to protect is supposed to be a subkey, you need to convert it to one before + * calling this method. See {@link org.bouncycastle.openpgp.PGPKeyPair#asSubkey(KeyFingerPrintCalculator)}. + * + * @param passphrase passphrase + * @param pubKey public primary or subkey packet + * @return encryptor using AEAD + */ + PBESecretKeyEncryptor build(char[] passphrase, PublicKeyPacket pubKey); +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/PBESecretKeyEncryptor.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/PBESecretKeyEncryptor.java index 956cc9067a..c9a9846877 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/PBESecretKeyEncryptor.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/PBESecretKeyEncryptor.java @@ -5,6 +5,52 @@ import org.bouncycastle.bcpg.S2K; import org.bouncycastle.openpgp.PGPException; +/** + * Class responsible for encrypting secret key material or data packets using a passphrase. + *

+ * RFC9580 recommends the following S2K specifiers + usages: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
S2K SpecifierS2K UsageNote
{@link S2K#ARGON_2}{@link org.bouncycastle.bcpg.SecretKeyPacket#USAGE_AEAD}RECOMMENDED; Argon2 MUST be used with AEAD
{@link S2K#SALTED_AND_ITERATED}{@link org.bouncycastle.bcpg.SecretKeyPacket#USAGE_SHA1}MAY be used if Argon2 is not available; Take care to use high octet count + strong passphrase
none{@link org.bouncycastle.bcpg.SecretKeyPacket#USAGE_NONE}Unprotected
+ *

+ * Additionally, implementations MAY use the following combinations with caution: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
S2K SpecifierS2K UsageNote
{@link S2K#SALTED_AND_ITERATED}{@link org.bouncycastle.bcpg.SecretKeyPacket#USAGE_AEAD}Does not provide memory hardness
{@link S2K#SIMPLE}{@link org.bouncycastle.bcpg.SecretKeyPacket#USAGE_SHA1}Only for reading secret keys in backwards compatibility mode
+ */ public abstract class PBESecretKeyEncryptor { protected int encAlgorithm; diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/PBESecretKeyEncryptorFactory.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/PBESecretKeyEncryptorFactory.java new file mode 100644 index 0000000000..ff18584612 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/PBESecretKeyEncryptorFactory.java @@ -0,0 +1,21 @@ +package org.bouncycastle.openpgp.operator; + +import org.bouncycastle.bcpg.PublicKeyPacket; + +/** + * Factory class for password-based secret key encryptors. + * A concrete implementation of this class can not only choose the cryptographic backend (e.g. BC, JCA/JCE), + * but also, whether to use AEAD (RFC9580) or classic CFB (RFC4880). + */ +public interface PBESecretKeyEncryptorFactory +{ + + /** + * Build a new {@link PBESecretKeyEncryptor} instance from the given passphrase and public key packet. + * + * @param passphrase passphrase + * @param pubKeyPacket public-key packet of the key to protect (needed for AEAD) + * @return key encryptor + */ + PBESecretKeyEncryptor build(char[] passphrase, PublicKeyPacket pubKeyPacket); +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPAEADDataEncryptor.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPAEADDataEncryptor.java index a2985263f3..6d791c992e 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPAEADDataEncryptor.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPAEADDataEncryptor.java @@ -4,8 +4,7 @@ /** * A data encryptor, using AEAD. * There are two different flavours of AEAD encryption used with OpenPGP. - * OpenPGP v5 AEAD is slightly different from v6 AEAD. - *

+ * LibrePGP (v5) AEAD is slightly different from RFC9580 (v6) AEAD. * {@link PGPAEADDataEncryptor} instances are generally not constructed directly, but obtained from a * {@link PGPDataEncryptorBuilder}. *

diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPContentSignerBuilderProvider.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPContentSignerBuilderProvider.java new file mode 100644 index 0000000000..e45a97d31c --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPContentSignerBuilderProvider.java @@ -0,0 +1,29 @@ +package org.bouncycastle.openpgp.operator; + +import org.bouncycastle.openpgp.PGPPublicKey; + +/** + * Provider class for {@link PGPContentSignerBuilder} instances. + * Concrete implementations of this class can choose the cryptographic backend (BC, JCA/JCE). + */ +public abstract class PGPContentSignerBuilderProvider +{ + protected final int hashAlgorithmId; + + /** + * Constructor. + * + * @param hashAlgorithmId ID of the hash algorithm the {@link PGPContentSignerBuilder} shall use. + */ + public PGPContentSignerBuilderProvider(int hashAlgorithmId) + { + this.hashAlgorithmId = hashAlgorithmId; + } + + /** + * Return a new instance of the {@link PGPContentSignerBuilder} for the given signing key. + * @param signingKey public part of the signing key + * @return content signer builder + */ + public abstract PGPContentSignerBuilder get(PGPPublicKey signingKey); +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPKeyPairGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPKeyPairGenerator.java new file mode 100644 index 0000000000..c1765861d0 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPKeyPairGenerator.java @@ -0,0 +1,181 @@ +package org.bouncycastle.openpgp.operator; + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Date; + +public abstract class PGPKeyPairGenerator +{ + + protected final Date creationTime; + protected final int version; + protected SecureRandom random; + protected final KeyFingerPrintCalculator fingerPrintCalculator; + + /** + * Create an instance of the key pair generator. + * + * @param version public key version ({@link org.bouncycastle.bcpg.PublicKeyPacket#VERSION_4} + * or {@link org.bouncycastle.bcpg.PublicKeyPacket#VERSION_6}). + * @param creationTime key creation time + * @param random secure random number generator + */ + public PGPKeyPairGenerator(int version, + Date creationTime, + SecureRandom random, + KeyFingerPrintCalculator fingerPrintCalculator) + { + this.creationTime = new Date((creationTime.getTime() / 1000) * 1000); + this.version = version; + this.random = random; + this.fingerPrintCalculator = fingerPrintCalculator; + } + + /** + * Generate a primary key. + * A primary key MUST use a signing-capable public key algorithm. + * + * @return primary key pair + * @throws PGPException if the key pair cannot be generated + */ + public PGPKeyPair generatePrimaryKey() + throws PGPException + { + return generateEd25519KeyPair(); + } + + /** + * Generate an encryption subkey. + * An encryption subkey MUST use an encryption-capable public key algorithm. + * + * @return encryption subkey pair + * @throws PGPException if the key pair cannot be generated + */ + public PGPKeyPair generateEncryptionSubkey() + throws PGPException + { + return generateX25519KeyPair().asSubkey(fingerPrintCalculator); + } + + /** + * Generate a signing subkey. + * A signing subkey MUST use a signing-capable public key algorithm. + * + * @return signing subkey pair + * @throws PGPException if the key pair cannot be generated + */ + public PGPKeyPair generateSigningSubkey() + throws PGPException + { + return generateEd25519KeyPair().asSubkey(fingerPrintCalculator); + } + + /** + * Generate a RSA key pair with the given bit-strength. + * It is recommended to use at least 2048 bits or more. + * The key will be generated over the default exponent
65537
. + * RSA keys are deprecated for OpenPGP v6. + * + * @param bitStrength strength of the key pair in bits + * @return rsa key pair + * @throws PGPException if the key pair cannot be generated + */ + public PGPKeyPair generateRsaKeyPair(int bitStrength) + throws PGPException + { + return generateRsaKeyPair(BigInteger.valueOf(0x10001), bitStrength); + } + + /** + * Generate a RSA key pair with the given bit-strength over a custom exponent. + * It is recommended to use at least 2048 bits or more. + * RSA keys are deprecated for OpenPGP v6. + * + * @param exponent RSA exponent
e
+ * @param bitStrength strength of the key pair in bits + * @return rsa key pair + * @throws PGPException if the key pair cannot be generated + */ + public abstract PGPKeyPair generateRsaKeyPair(BigInteger exponent, int bitStrength) + throws PGPException; + + /** + * Generate an elliptic curve signing key over the twisted Edwards curve25519. + * The key will use {@link PublicKeyAlgorithmTags#Ed25519} which was introduced with RFC9580. + * For legacy Ed25519 keys use {@link #generateLegacyEd25519KeyPair()}. + * + * @see + * RFC9580 - Public Key Algorithms + * @return Ed25519 key pair + * @throws PGPException if the key pair cannot be generated + */ + public abstract PGPKeyPair generateEd25519KeyPair() + throws PGPException; + + /** + * Generate an elliptic curve signing key over the twisted Edwards curve448. + * The key will use {@link PublicKeyAlgorithmTags#Ed448} which was introduced with RFC9580. + * + * @see + * RFC9580 - Public Key Algorithms + * @return Ed448 signing key pair + * @throws PGPException if the key pair cannot be generated + */ + public abstract PGPKeyPair generateEd448KeyPair() + throws PGPException; + + /** + * Generate an elliptic curve Diffie-Hellman encryption key over curve25519. + * THe key will use {@link PublicKeyAlgorithmTags#X25519} which was introduced with RFC9580. + * For legacy X25519 keys use {@link #generateLegacyX25519KeyPair()} instead. + * + * @see + * RFC9580 - Public Key Algorithms + * @return X25519 encryption key pair + * @throws PGPException if the key pair cannot be generated + */ + public abstract PGPKeyPair generateX25519KeyPair() + throws PGPException; + + /** + * Generate an elliptic curve Diffie-Hellman encryption key over curve448. + * THe key will use {@link PublicKeyAlgorithmTags#X448} which was introduced with RFC9580. + * + * @see + * RFC9580 - Public Key Algorithms + * @return X448 encryption key pair + * @throws PGPException if the key pair cannot be generated + */ + public abstract PGPKeyPair generateX448KeyPair() + throws PGPException; + + /** + * Generate a legacy elliptic curve signing key pair over the twisted Edwards curve25519. + * Legacy keys have good application support, but MUST NOT be used as OpenPGP v6 keys. + * The key will use {@link PublicKeyAlgorithmTags#EDDSA_LEGACY} as algorithm ID. + * For OpenPGP v6 (RFC9580) use {@link #generateEd25519KeyPair()} instead. + * + * @see + * Legacy Draft: EdDSA for OpenPGP + * @return legacy Ed25519 key pair + * @throws PGPException if the key pair cannot be generated + */ + public abstract PGPKeyPair generateLegacyEd25519KeyPair() + throws PGPException; + + /** + * Generate a legacy elliptic curve Diffie-Hellman encryption key pair over curve25519. + * Legacy keys have good application support, but MUST NOT be used as OpenPGP v6 keys. + * The key will use {@link PublicKeyAlgorithmTags#ECDH} as algorithm ID. + * For OpenPGP v6 (RFC9580) use {@link #generateX25519KeyPair()} instead. + * + * @return legacy X25519 key pair + * @throws PGPException if the key pair cannot be generated + */ + public abstract PGPKeyPair generateLegacyX25519KeyPair() + throws PGPException; +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPKeyPairGeneratorProvider.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPKeyPairGeneratorProvider.java new file mode 100644 index 0000000000..9b403f9334 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPKeyPairGeneratorProvider.java @@ -0,0 +1,8 @@ +package org.bouncycastle.openpgp.operator; + +import java.util.Date; + +public abstract class PGPKeyPairGeneratorProvider +{ + public abstract PGPKeyPairGenerator get(int version, Date creationTime); +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcAEADSecretKeyEncryptorBuilder.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcAEADSecretKeyEncryptorBuilder.java index c1ffcf0001..3b961fbdd4 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcAEADSecretKeyEncryptorBuilder.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcAEADSecretKeyEncryptorBuilder.java @@ -16,10 +16,12 @@ import org.bouncycastle.crypto.params.HKDFParameters; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.AEADSecretKeyEncryptorBuilder; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.util.Arrays; public class BcAEADSecretKeyEncryptorBuilder + implements AEADSecretKeyEncryptorBuilder { private int aeadAlgorithm; diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcAEADSecretKeyEncryptorFactory.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcAEADSecretKeyEncryptorFactory.java new file mode 100644 index 0000000000..3fa0dc0b27 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcAEADSecretKeyEncryptorFactory.java @@ -0,0 +1,34 @@ +package org.bouncycastle.openpgp.operator.bc; + +import org.bouncycastle.bcpg.AEADAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptorFactory; + +/** + * Return a factory for {@link PBESecretKeyEncryptor} instances which protect the secret key material by deriving + * a key-encryption-key using {@link org.bouncycastle.bcpg.S2K#ARGON_2} S2K and apply + * that key using {@link org.bouncycastle.bcpg.SecretKeyPacket#USAGE_AEAD}. + *

+ * This particular factory uses OCB + AES256 for secret key protection and requires 64MiB of RAM + * for the Argon2 key derivation (see {@link S2K.Argon2Params#memoryConstrainedParameters()}). + */ +public class BcAEADSecretKeyEncryptorFactory + implements PBESecretKeyEncryptorFactory +{ + @Override + public PBESecretKeyEncryptor build(char[] passphrase, PublicKeyPacket pubKeyPacket) + { + if (passphrase == null) + { + return null; + } + return new org.bouncycastle.openpgp.operator.bc.BcAEADSecretKeyEncryptorBuilder( + AEADAlgorithmTags.OCB, + SymmetricKeyAlgorithmTags.AES_256, + S2K.Argon2Params.memoryConstrainedParameters()) + .build(passphrase, pubKeyPacket); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcCFBSecretKeyEncryptorFactory.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcCFBSecretKeyEncryptorFactory.java new file mode 100644 index 0000000000..7b86fb3f34 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcCFBSecretKeyEncryptorFactory.java @@ -0,0 +1,46 @@ +package org.bouncycastle.openpgp.operator.bc; + +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptorFactory; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; + +/** + * Return a factory for {@link PBESecretKeyEncryptor} instances which protect the secret key material by deriving + * a key-encryption-key using {@link org.bouncycastle.bcpg.S2K#SALTED_AND_ITERATED} S2K and apply + * that key using {@link org.bouncycastle.bcpg.SecretKeyPacket#USAGE_SHA1} (CFB mode). + *

+ * This particular factory derives a key-encryption-key via salted+iterated S2K derivation using SHA256 + * and uses AES256 for secret key protection. + */ +public class BcCFBSecretKeyEncryptorFactory + implements PBESecretKeyEncryptorFactory +{ + @Override + public PBESecretKeyEncryptor build(char[] passphrase, PublicKeyPacket pubKeyPacket) + { + if (passphrase == null) + { + return null; + } + + PGPDigestCalculator checksumCalc; + try + { + checksumCalc = new BcPGPDigestCalculatorProvider().get(HashAlgorithmTags.SHA256); + } + catch (PGPException e) + { + throw new RuntimeException(e); // Does not happen in practice + } + + return new BcPBESecretKeyEncryptorBuilder( + SymmetricKeyAlgorithmTags.AES_256, + checksumCalc, + 0xff) // MAX iteration count + .build(passphrase); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPGPContentSignerBuilderProvider.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPGPContentSignerBuilderProvider.java new file mode 100644 index 0000000000..6c0cdc21f6 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPGPContentSignerBuilderProvider.java @@ -0,0 +1,21 @@ +package org.bouncycastle.openpgp.operator.bc; + +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider; + +public class BcPGPContentSignerBuilderProvider + extends PGPContentSignerBuilderProvider +{ + + public BcPGPContentSignerBuilderProvider(int hashAlgorithmId) + { + super(hashAlgorithmId); + } + + @Override + public PGPContentSignerBuilder get(PGPPublicKey signingKey) + { + return new BcPGPContentSignerBuilder(signingKey.getAlgorithm(), hashAlgorithmId); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPGPKeyPairGeneratorProvider.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPGPKeyPairGeneratorProvider.java new file mode 100644 index 0000000000..19defd425c --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPGPKeyPairGeneratorProvider.java @@ -0,0 +1,132 @@ +package org.bouncycastle.openpgp.operator.bc; + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator; +import org.bouncycastle.crypto.generators.Ed448KeyPairGenerator; +import org.bouncycastle.crypto.generators.RSAKeyPairGenerator; +import org.bouncycastle.crypto.generators.X25519KeyPairGenerator; +import org.bouncycastle.crypto.generators.X448KeyPairGenerator; +import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters; +import org.bouncycastle.crypto.params.Ed448KeyGenerationParameters; +import org.bouncycastle.crypto.params.RSAKeyGenerationParameters; +import org.bouncycastle.crypto.params.X25519KeyGenerationParameters; +import org.bouncycastle.crypto.params.X448KeyGenerationParameters; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator; +import org.bouncycastle.openpgp.operator.PGPKeyPairGeneratorProvider; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Date; + +public class BcPGPKeyPairGeneratorProvider + extends PGPKeyPairGeneratorProvider +{ + private SecureRandom random = CryptoServicesRegistrar.getSecureRandom(); + + @Override + public PGPKeyPairGenerator get(int version, Date creationTime) + { + return new BcPGPKeyPairGenerator(version, creationTime, random); + } + + public BcPGPKeyPairGeneratorProvider setSecureRandom(SecureRandom random) + { + this.random = random; + return this; + } + + private static class BcPGPKeyPairGenerator + extends PGPKeyPairGenerator + { + + public BcPGPKeyPairGenerator(int version, Date creationTime, SecureRandom random) + { + super(version, creationTime, random, new BcKeyFingerprintCalculator()); + } + + @Override + public PGPKeyPair generateRsaKeyPair(BigInteger exponent, int bitStrength) + throws PGPException + { + RSAKeyPairGenerator gen = new RSAKeyPairGenerator(); + gen.init(new RSAKeyGenerationParameters(exponent, random, bitStrength, 100)); + AsymmetricCipherKeyPair keyPair = gen.generateKeyPair(); + return new BcPGPKeyPair(version, PublicKeyAlgorithmTags.RSA_GENERAL, keyPair, creationTime); + } + + @Override + public PGPKeyPair generateEd25519KeyPair() + throws PGPException + { + Ed25519KeyPairGenerator gen = new Ed25519KeyPairGenerator(); + gen.init(new Ed25519KeyGenerationParameters(random)); + AsymmetricCipherKeyPair keyPair = gen.generateKeyPair(); + return new BcPGPKeyPair(version, PublicKeyAlgorithmTags.Ed25519, keyPair, creationTime); + } + + @Override + public PGPKeyPair generateEd448KeyPair() + throws PGPException + { + Ed448KeyPairGenerator gen = new Ed448KeyPairGenerator(); + gen.init(new Ed448KeyGenerationParameters(random)); + AsymmetricCipherKeyPair keyPair = gen.generateKeyPair(); + return new BcPGPKeyPair(version, PublicKeyAlgorithmTags.Ed448, keyPair, creationTime); + } + + @Override + public PGPKeyPair generateX25519KeyPair() + throws PGPException + { + X25519KeyPairGenerator gen = new X25519KeyPairGenerator(); + gen.init(new X25519KeyGenerationParameters(random)); + AsymmetricCipherKeyPair keyPair = gen.generateKeyPair(); + return new BcPGPKeyPair(version, PublicKeyAlgorithmTags.X25519, keyPair, creationTime); + } + + @Override + public PGPKeyPair generateX448KeyPair() + throws PGPException + { + X448KeyPairGenerator gen = new X448KeyPairGenerator(); + gen.init(new X448KeyGenerationParameters(random)); + AsymmetricCipherKeyPair keyPair = gen.generateKeyPair(); + return new BcPGPKeyPair(version, PublicKeyAlgorithmTags.X448, keyPair, creationTime); + } + + @Override + public PGPKeyPair generateLegacyEd25519KeyPair() + throws PGPException + { + if (version == PublicKeyPacket.VERSION_6) + { + throw new PGPException("An implementation MUST NOT generate a v6 LegacyEd25519 key pair."); + } + + Ed25519KeyPairGenerator gen = new Ed25519KeyPairGenerator(); + gen.init(new Ed25519KeyGenerationParameters(random)); + AsymmetricCipherKeyPair keyPair = gen.generateKeyPair(); + return new BcPGPKeyPair(version, PublicKeyAlgorithmTags.EDDSA_LEGACY, keyPair, creationTime); + } + + @Override + public PGPKeyPair generateLegacyX25519KeyPair() + throws PGPException + { + if (version == PublicKeyPacket.VERSION_6) + { + throw new PGPException("An implementation MUST NOT generate a v6 LegacyX25519 key pair."); + } + + X25519KeyPairGenerator gen = new X25519KeyPairGenerator(); + gen.init(new X25519KeyGenerationParameters(random)); + AsymmetricCipherKeyPair keyPair = gen.generateKeyPair(); + return new BcPGPKeyPair(version, PublicKeyAlgorithmTags.ECDH, keyPair, creationTime); + } + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaAEADSecretKeyEncryptorBuilder.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaAEADSecretKeyEncryptorBuilder.java index 5fb1aca085..eee860acd9 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaAEADSecretKeyEncryptorBuilder.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaAEADSecretKeyEncryptorBuilder.java @@ -20,10 +20,12 @@ import org.bouncycastle.jcajce.util.ProviderJcaJceHelper; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.AEADSecretKeyEncryptorBuilder; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.util.Arrays; public class JcaAEADSecretKeyEncryptorBuilder + implements AEADSecretKeyEncryptorBuilder { private int aeadAlgorithm; private int symmetricAlgorithm; diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaAEADSecretKeyEncryptorFactory.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaAEADSecretKeyEncryptorFactory.java new file mode 100644 index 0000000000..199c2af3b1 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaAEADSecretKeyEncryptorFactory.java @@ -0,0 +1,35 @@ +package org.bouncycastle.openpgp.operator.jcajce; + +import org.bouncycastle.bcpg.AEADAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptorFactory; + +import java.security.Provider; + +public class JcaAEADSecretKeyEncryptorFactory + implements PBESecretKeyEncryptorFactory +{ + private JcaAEADSecretKeyEncryptorBuilder builder = new JcaAEADSecretKeyEncryptorBuilder( + AEADAlgorithmTags.OCB, + SymmetricKeyAlgorithmTags.AES_256, + S2K.Argon2Params.memoryConstrainedParameters()); + + public JcaAEADSecretKeyEncryptorFactory setProvider(Provider provider) + { + builder.setProvider(provider); + return this; + } + + @Override + public PBESecretKeyEncryptor build(char[] passphrase, PublicKeyPacket pubKeyPacket) + { + if (passphrase == null) + { + return null; + } + return builder.build(passphrase, pubKeyPacket); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaCFBSecretKeyEncryptorFactory.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaCFBSecretKeyEncryptorFactory.java new file mode 100644 index 0000000000..1a11c4e86b --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaCFBSecretKeyEncryptorFactory.java @@ -0,0 +1,52 @@ +package org.bouncycastle.openpgp.operator.jcajce; + +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptorFactory; + +import java.security.Provider; + +public class JcaCFBSecretKeyEncryptorFactory + implements PBESecretKeyEncryptorFactory +{ + private JcaPGPDigestCalculatorProviderBuilder digestCalcProviderBuilder = + new JcaPGPDigestCalculatorProviderBuilder(); + private JcePBESecretKeyEncryptorBuilder encBuilder; + + public JcaCFBSecretKeyEncryptorFactory() + throws PGPException + { + encBuilder = builder(); + } + + public JcaCFBSecretKeyEncryptorFactory setProvider(Provider provider) + throws PGPException + { + digestCalcProviderBuilder.setProvider(provider); + encBuilder = builder(); + return this; + } + + private JcePBESecretKeyEncryptorBuilder builder() + throws PGPException + { + return new JcePBESecretKeyEncryptorBuilder( + SymmetricKeyAlgorithmTags.AES_256, + digestCalcProviderBuilder.build().get(HashAlgorithmTags.SHA1), + 0x60 + ); + } + + @Override + public PBESecretKeyEncryptor build(char[] passphrase, PublicKeyPacket pubKeyPacket) + { + if (passphrase == null) + { + return null; + } + return encBuilder.build(passphrase); + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaPGPContentSignerBuilderProvider.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaPGPContentSignerBuilderProvider.java new file mode 100644 index 0000000000..1f90adda1f --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaPGPContentSignerBuilderProvider.java @@ -0,0 +1,61 @@ +package org.bouncycastle.openpgp.operator.jcajce; + +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider; + +import java.security.Provider; +import java.security.SecureRandom; + +public class JcaPGPContentSignerBuilderProvider + extends PGPContentSignerBuilderProvider +{ + private Provider digestProvider; + private Provider securityProvider; + private SecureRandom secureRandom; + + public JcaPGPContentSignerBuilderProvider(int hashAlgorithmId) + { + super(hashAlgorithmId); + } + + public JcaPGPContentSignerBuilderProvider setDigestProvider(Provider provider) + { + this.digestProvider = provider; + return this; + } + + public JcaPGPContentSignerBuilderProvider setSecurityProvider(Provider provider) + { + this.securityProvider = provider; + return this; + } + + public JcaPGPContentSignerBuilderProvider setSecureRandom(SecureRandom random) + { + this.secureRandom = random; + return this; + } + + @Override + public PGPContentSignerBuilder get(PGPPublicKey signingKey) + { + JcaPGPContentSignerBuilder builder = new JcaPGPContentSignerBuilder( + signingKey.getAlgorithm(), hashAlgorithmId); + if (digestProvider != null) + { + builder.setDigestProvider(digestProvider); + } + + if (securityProvider != null) + { + builder.setProvider(securityProvider); + } + + if (secureRandom != null) + { + builder.setSecureRandom(secureRandom); + } + return builder; + } +} diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaPGPKeyPairGeneratorProvider.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaPGPKeyPairGeneratorProvider.java new file mode 100644 index 0000000000..5632f45dca --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaPGPKeyPairGeneratorProvider.java @@ -0,0 +1,216 @@ +package org.bouncycastle.openpgp.operator.jcajce; + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.jcajce.spec.EdDSAParameterSpec; +import org.bouncycastle.jcajce.spec.XDHParameterSpec; +import org.bouncycastle.jcajce.util.DefaultJcaJceHelper; +import org.bouncycastle.jcajce.util.NamedJcaJceHelper; +import org.bouncycastle.jcajce.util.ProviderJcaJceHelper; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator; +import org.bouncycastle.openpgp.operator.PGPKeyPairGeneratorProvider; + +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.Date; + +public class JcaPGPKeyPairGeneratorProvider + extends PGPKeyPairGeneratorProvider +{ + + private OperatorHelper helper; + private SecureRandom secureRandom = CryptoServicesRegistrar.getSecureRandom(); + + public JcaPGPKeyPairGeneratorProvider() + { + this.helper = new OperatorHelper(new DefaultJcaJceHelper()); + } + + + /** + * Set the provider object to use for creating cryptographic primitives in the resulting factory the builder produces. + * + * @param provider provider object for cryptographic primitives. + * @return the current builder. + */ + public JcaPGPKeyPairGeneratorProvider setProvider(Provider provider) + { + this.helper = new OperatorHelper(new ProviderJcaJceHelper(provider)); + return this; + } + + /** + * Set the provider name to use for creating cryptographic primitives in the resulting factory the builder produces. + * + * @param providerName the name of the provider to reference for cryptographic primitives. + * @return the current builder. + */ + public JcaPGPKeyPairGeneratorProvider setProvider(String providerName) + { + this.helper = new OperatorHelper(new NamedJcaJceHelper(providerName)); + return this; + } + + public JcaPGPKeyPairGeneratorProvider setSecureRandom(SecureRandom random) + { + this.secureRandom = random; + return this; + } + + + @Override + public PGPKeyPairGenerator get(int version, Date creationTime) + { + return new JcaPGPKeyPairGenerator(version, creationTime, helper, secureRandom); + } + + private static class JcaPGPKeyPairGenerator + extends PGPKeyPairGenerator + { + + private final OperatorHelper helper; + + public JcaPGPKeyPairGenerator(int version, Date creationTime, OperatorHelper helper, SecureRandom random) + { + super(version, creationTime, random, new JcaKeyFingerprintCalculator()); + this.helper = helper; + } + + @Override + public PGPKeyPair generateRsaKeyPair(BigInteger exponent, int bitStrength) + throws PGPException + { + try + { + KeyPairGenerator gen = helper.createKeyPairGenerator("RSA"); + gen.initialize(new RSAKeyGenParameterSpec(bitStrength, exponent)); + KeyPair keyPair = gen.generateKeyPair(); + return new JcaPGPKeyPair(version, PublicKeyAlgorithmTags.RSA_GENERAL, keyPair, creationTime); + } + catch (GeneralSecurityException e) + { + throw new PGPException("Cannot generate RSA key pair", e); + } + } + + @Override + public PGPKeyPair generateEd25519KeyPair() + throws PGPException + { + try + { + KeyPairGenerator gen = helper.createKeyPairGenerator("EDDSA"); + gen.initialize(new EdDSAParameterSpec("Ed25519")); + KeyPair keyPair = gen.generateKeyPair(); + return new JcaPGPKeyPair(version, PublicKeyAlgorithmTags.Ed25519, keyPair, creationTime); + } + catch (GeneralSecurityException e) + { + throw new PGPException("Cannot generate Ed25519 key pair", e); + } + } + + @Override + public PGPKeyPair generateEd448KeyPair() + throws PGPException + { + try + { + KeyPairGenerator gen = helper.createKeyPairGenerator("EDDSA"); + gen.initialize(new EdDSAParameterSpec("Ed448")); + KeyPair keyPair = gen.generateKeyPair(); + return new JcaPGPKeyPair(version, PublicKeyAlgorithmTags.Ed448, keyPair, creationTime); + } + catch (GeneralSecurityException e) + { + throw new PGPException("Cannot generate Ed448 key pair", e); + } + } + + @Override + public PGPKeyPair generateX25519KeyPair() + throws PGPException + { + try + { + KeyPairGenerator gen = helper.createKeyPairGenerator("XDH"); + gen.initialize(new XDHParameterSpec("X25519")); + KeyPair keyPair = gen.generateKeyPair(); + return new JcaPGPKeyPair(version, PublicKeyAlgorithmTags.X25519, keyPair, creationTime); + } + catch (GeneralSecurityException e) + { + throw new PGPException("Cannot generate X25519 key pair", e); + } + } + + @Override + public PGPKeyPair generateX448KeyPair() + throws PGPException + { + try + { + KeyPairGenerator gen = helper.createKeyPairGenerator("XDH"); + gen.initialize(new XDHParameterSpec("X448")); + KeyPair keyPair = gen.generateKeyPair(); + return new JcaPGPKeyPair(version, PublicKeyAlgorithmTags.X448, keyPair, creationTime); + } + catch (GeneralSecurityException e) + { + throw new PGPException("Cannot generate X448 key pair", e); + } + } + + @Override + public PGPKeyPair generateLegacyEd25519KeyPair() + throws PGPException + { + if (version == PublicKeyPacket.VERSION_6) + { + throw new PGPException("An implementation MUST NOT generate a v6 LegacyEd25519 key pair."); + } + + try + { + KeyPairGenerator gen = helper.createKeyPairGenerator("EDDSA"); + gen.initialize(new EdDSAParameterSpec("Ed25519")); + KeyPair keyPair = gen.generateKeyPair(); + return new JcaPGPKeyPair(version, PublicKeyAlgorithmTags.EDDSA_LEGACY, keyPair, creationTime); + } + catch (GeneralSecurityException e) + { + throw new PGPException("Cannot generate LegacyEd25519 key pair."); + } + } + + @Override + public PGPKeyPair generateLegacyX25519KeyPair() + throws PGPException + { + if (version == PublicKeyPacket.VERSION_6) + { + throw new PGPException("An implementation MUST NOT generate a v6 LegacyX25519 key pair."); + } + + try + { + KeyPairGenerator gen = helper.createKeyPairGenerator("XDH"); + gen.initialize(new XDHParameterSpec("X25519")); + KeyPair keyPair = gen.generateKeyPair(); + return new JcaPGPKeyPair(version, PublicKeyAlgorithmTags.ECDH, keyPair, creationTime); + } + catch (GeneralSecurityException e) + { + throw new PGPException("Cannot generate LegacyX25519 key pair.", e); + } + } + } +} diff --git a/pg/src/main/jdk1.9/module-info.java b/pg/src/main/jdk1.9/module-info.java index b4b68b6d85..622ea41135 100644 --- a/pg/src/main/jdk1.9/module-info.java +++ b/pg/src/main/jdk1.9/module-info.java @@ -12,6 +12,9 @@ exports org.bouncycastle.gpg.keybox; exports org.bouncycastle.gpg.keybox.bc; exports org.bouncycastle.gpg.keybox.jcajce; + exports org.bouncycastle.openpgp.api; + exports org.bouncycastle.openpgp.api.bc; + exports org.bouncycastle.openpgp.api.jcajce; exports org.bouncycastle.openpgp.bc; exports org.bouncycastle.openpgp.examples; exports org.bouncycastle.openpgp.jcajce; diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPV6KeyGeneratorTest.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPV6KeyGeneratorTest.java new file mode 100644 index 0000000000..161ff338ce --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPV6KeyGeneratorTest.java @@ -0,0 +1,499 @@ +package org.bouncycastle.openpgp.api.test; + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.PublicKeyUtils; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.bouncycastle.openpgp.api.KeyPairGeneratorCallback; +import org.bouncycastle.openpgp.api.OpenPGPV6KeyGenerator; +import org.bouncycastle.openpgp.api.bc.BcOpenPGPV6KeyGenerator; +import org.bouncycastle.openpgp.api.jcajce.JcaOpenPGPV6KeyGenerator; +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPairGeneratorProvider; +import org.bouncycastle.openpgp.test.AbstractPgpKeyPairTest; + +import java.io.IOException; +import java.util.Date; +import java.util.Iterator; + +public class OpenPGPV6KeyGeneratorTest + extends AbstractPgpKeyPairTest +{ + @Override + public String getName() + { + return "OpenPGPV6KeyGeneratorTest"; + } + + @Override + public void performTest() + throws Exception + { + // Run tests using the BC implementation + performTests(new APIProvider() + { + @Override + public OpenPGPV6KeyGenerator getKeyGenerator(int signatureHashAlgorithm, + Date creationTime, + boolean aeadProtection) + { + return new BcOpenPGPV6KeyGenerator(signatureHashAlgorithm, creationTime, aeadProtection); + } + }); + + // Run tests using the JCA/JCE implementation + performTests(new APIProvider() + { + @Override + public OpenPGPV6KeyGenerator getKeyGenerator(int signatureHashAlgorithm, + Date creationTime, + boolean aeadProtection) + throws PGPException + { + return new JcaOpenPGPV6KeyGenerator(signatureHashAlgorithm, creationTime, aeadProtection, + new BouncyCastleProvider()); + } + }); + } + + private void performTests(APIProvider apiProvider) + throws PGPException, IOException + { + testGenerateCustomKey(apiProvider); + + testGenerateSignOnlyKeyBaseCase(apiProvider); + testGenerateAEADProtectedSignOnlyKey(apiProvider); + testGenerateCFBProtectedSignOnlyKey(apiProvider); + + testGenerateClassicKeyBaseCase(apiProvider); + testGenerateProtectedTypicalKey(apiProvider); + + testGenerateEd25519x25519Key(apiProvider); + testGenerateEd448x448Key(apiProvider); + + testEnforcesPrimaryOrSubkeyType(apiProvider); + } + + private void testGenerateSignOnlyKeyBaseCase(APIProvider apiProvider) + throws PGPException + { + OpenPGPV6KeyGenerator generator = apiProvider.getKeyGenerator(); + PGPSecretKeyRing secretKeys = generator.signOnlyKey(null); + + Iterator it = secretKeys.getSecretKeys(); + PGPSecretKey primaryKey = it.next(); + isFalse("sign-only key MUST consists of only a single key", it.hasNext()); + PGPSignature directKeySignature = primaryKey.getPublicKey().getKeySignatures().next(); + isNotNull("Key MUST have direct-key signature", directKeySignature); + isEquals("Direct-key signature MUST be version 6", + SignaturePacket.VERSION_6, directKeySignature.getVersion()); + PGPSignatureSubpacketVector hPackets = directKeySignature.getHashedSubPackets(); + isNotNull("Subpackets MUST contain issuer-fingerprint subpacket", + hPackets.getIssuerFingerprint()); + isFalse("Subpackets MUST NOT contain issuer-key-id subpacket", + hPackets.hasSubpacket(SignatureSubpacketTags.ISSUER_KEY_ID)); + isNotNull("Subpackets MUST contain signature creation-time subpacket", + hPackets.getSignatureCreationTime()); + isEquals("Sign-Only primary key MUST carry CS flags", + KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, hPackets.getKeyFlags()); + + isEquals("Key version mismatch", 6, primaryKey.getPublicKey().getVersion()); + isEquals("Key MUST be unprotected", SecretKeyPacket.USAGE_NONE, primaryKey.getS2KUsage()); + } + + private void testGenerateAEADProtectedSignOnlyKey(APIProvider apiProvider) + throws PGPException + { + OpenPGPV6KeyGenerator generator = apiProvider.getKeyGenerator(true); + PGPSecretKeyRing secretKeys = generator.signOnlyKey("passphrase".toCharArray()); + + Iterator it = secretKeys.getSecretKeys(); + PGPSecretKey primaryKey = it.next(); + isFalse("sign-only key MUST consists of only a single key", it.hasNext()); + + isEquals("Key MUST be AEAD-protected", SecretKeyPacket.USAGE_AEAD, primaryKey.getS2KUsage()); + isNotNull("Secret key MUST be retrievable using the proper passphrase", + primaryKey.extractKeyPair( + new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) + .build("passphrase".toCharArray()))); + } + + private void testGenerateCFBProtectedSignOnlyKey(APIProvider apiProvider) + throws PGPException + { + OpenPGPV6KeyGenerator generator = apiProvider.getKeyGenerator(false); + PGPSecretKeyRing secretKeys = generator.signOnlyKey("passphrase".toCharArray()); + + Iterator it = secretKeys.getSecretKeys(); + PGPSecretKey primaryKey = it.next(); + isFalse("sign-only key MUST consists of only a single key", it.hasNext()); + + isEquals("Key MUST be CFB-protected", SecretKeyPacket.USAGE_SHA1, primaryKey.getS2KUsage()); + isNotNull("Secret key MUST be retrievable using the proper passphrase", + primaryKey.extractKeyPair( + new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) + .build("passphrase".toCharArray()))); + } + + private void testGenerateClassicKeyBaseCase(APIProvider apiProvider) + throws PGPException + { + Date creationTime = currentTimeRounded(); + OpenPGPV6KeyGenerator generator = apiProvider.getKeyGenerator(creationTime); + PGPSecretKeyRing secretKeys = generator + .classicKey("Alice ", null); + + Iterator keys = secretKeys.getSecretKeys(); + PGPSecretKey primaryKey = keys.next(); + isEquals("Primary key version mismatch", PublicKeyPacket.VERSION_6, + primaryKey.getPublicKey().getVersion()); + isEquals(creationTime, primaryKey.getPublicKey().getCreationTime()); + isTrue("Primary key uses signing-capable algorithm", + PublicKeyUtils.isSigningAlgorithm(primaryKey.getPublicKey().getAlgorithm())); + PGPSignature directKeySig = primaryKey.getPublicKey().getKeySignatures().next(); + isEquals("Primary key of a classic key MUST carry C key flag.", + KeyFlags.CERTIFY_OTHER, directKeySig.getHashedSubPackets().getKeyFlags()); + + // Test UIDs + Iterator uids = primaryKey.getUserIDs(); + isEquals("Alice ", uids.next()); + isFalse(uids.hasNext()); + + // Test signing subkey + PGPSecretKey signingSubkey = keys.next(); + isEquals("Signing key version mismatch", PublicKeyPacket.VERSION_6, + signingSubkey.getPublicKey().getVersion()); + isTrue("Signing subkey uses signing-capable algorithm", + PublicKeyUtils.isSigningAlgorithm(signingSubkey.getPublicKey().getAlgorithm())); + isEquals(creationTime, signingSubkey.getPublicKey().getCreationTime()); + PGPSignature signingKeyBinding = signingSubkey.getPublicKey().getKeySignatures().next(); + isEquals("Signing subkey MUST carry S key flag.", + KeyFlags.SIGN_DATA, signingKeyBinding.getHashedSubPackets().getKeyFlags()); + isNotNull("Signing subkey binding MUST carry primary key binding sig", + signingKeyBinding.getHashedSubPackets().getEmbeddedSignatures().get(0)); + + // Test encryption subkey + PGPSecretKey encryptionSubkey = keys.next(); + isEquals("Encryption key version mismatch", PublicKeyPacket.VERSION_6, + encryptionSubkey.getPublicKey().getVersion()); + isTrue("Encryption subkey uses encryption-capable algorithm", + PublicKeyUtils.isEncryptionAlgorithm(encryptionSubkey.getPublicKey().getAlgorithm())); + isEquals(creationTime, encryptionSubkey.getPublicKey().getCreationTime()); + PGPSignature encryptionKeyBinding = encryptionSubkey.getPublicKey().getKeySignatures().next(); + isEquals("Encryption key MUST carry encryption flags", + KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, + encryptionKeyBinding.getHashedSubPackets().getKeyFlags()); + + // Test has no additional keys + isFalse(keys.hasNext()); + + // Test all keys are unprotected + for (PGPSecretKey key : secretKeys) + { + isEquals("(Sub-)keys MUST be unprotected", SecretKeyPacket.USAGE_NONE, key.getS2KUsage()); + } + } + + private void testGenerateProtectedTypicalKey(APIProvider apiProvider) + throws PGPException + { + Date creationTime = currentTimeRounded(); + OpenPGPV6KeyGenerator generator = apiProvider.getKeyGenerator(creationTime); + PGPSecretKeyRing secretKeys = generator + .classicKey("Alice ", "passphrase".toCharArray()); + + // Test creation time + for (PGPPublicKey key : secretKeys.toCertificate()) + { + isEquals(creationTime, key.getCreationTime()); + for (Iterator it = key.getSignatures(); it.hasNext(); ) + { + PGPSignature sig = it.next(); + isEquals(creationTime, sig.getCreationTime()); + } + } + + PGPPublicKey primaryKey = secretKeys.getPublicKey(); + // Test UIDs + Iterator uids = primaryKey.getUserIDs(); + isEquals("Alice ", uids.next()); + isFalse(uids.hasNext()); + + for (PGPSecretKey key : secretKeys) + { + isEquals("(Sub-)keys MUST be protected", SecretKeyPacket.USAGE_AEAD, key.getS2KUsage()); + } + } + + private void testGenerateEd25519x25519Key(APIProvider apiProvider) + throws PGPException + { + Date currentTime = currentTimeRounded(); + String userId = "Foo "; + OpenPGPV6KeyGenerator generator = apiProvider.getKeyGenerator(currentTime); + + PGPSecretKeyRing secretKey = generator.ed25519x25519Key(userId, null); + + Iterator iterator = secretKey.getSecretKeys(); + PGPSecretKey primaryKey = iterator.next(); + PGPSecretKey signingSubkey = iterator.next(); + PGPSecretKey encryptionSubkey = iterator.next(); + isFalse("Unexpected key", iterator.hasNext()); + + isEquals(PublicKeyAlgorithmTags.Ed25519, primaryKey.getPublicKey().getAlgorithm()); + Iterator keySignatures = primaryKey.getPublicKey().getKeySignatures(); + PGPSignature directKeySignature = keySignatures.next(); + isFalse(keySignatures.hasNext()); + PGPSignatureSubpacketVector hashedSubpackets = directKeySignature.getHashedSubPackets(); + isEquals(KeyFlags.CERTIFY_OTHER, hashedSubpackets.getKeyFlags()); + + Iterator userIds = primaryKey.getUserIDs(); + isEquals(userId, userIds.next()); + isFalse(userIds.hasNext()); + Iterator userIdSignatures = primaryKey.getPublicKey().getSignaturesForID(userId); + PGPSignature userIdSig = userIdSignatures.next(); + isFalse(userIdSignatures.hasNext()); + isEquals(PGPSignature.POSITIVE_CERTIFICATION, userIdSig.getSignatureType()); + + isEquals(PublicKeyAlgorithmTags.Ed25519, signingSubkey.getPublicKey().getAlgorithm()); + Iterator signingSubkeySigs = signingSubkey.getPublicKey().getKeySignatures(); + PGPSignature signingSubkeySig = signingSubkeySigs.next(); + isFalse(signingSubkeySigs.hasNext()); + isEquals(PGPSignature.SUBKEY_BINDING, signingSubkeySig.getSignatureType()); + hashedSubpackets = signingSubkeySig.getHashedSubPackets(); + isEquals(KeyFlags.SIGN_DATA, hashedSubpackets.getKeyFlags()); + + isEquals(PublicKeyAlgorithmTags.X25519, encryptionSubkey.getPublicKey().getAlgorithm()); + Iterator encryptionSubkeySigs = encryptionSubkey.getPublicKey().getKeySignatures(); + PGPSignature encryptionSubkeySig = encryptionSubkeySigs.next(); + isFalse(encryptionSubkeySigs.hasNext()); + isEquals(PGPSignature.SUBKEY_BINDING, encryptionSubkeySig.getSignatureType()); + hashedSubpackets = encryptionSubkeySig.getHashedSubPackets(); + isEquals(KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, hashedSubpackets.getKeyFlags()); + } + + private void testGenerateEd448x448Key(APIProvider apiProvider) + throws PGPException + { + Date currentTime = currentTimeRounded(); + String userId = "Foo "; + OpenPGPV6KeyGenerator generator = apiProvider.getKeyGenerator(currentTime); + + PGPSecretKeyRing secretKey = generator.ed448x448Key(userId, null); + + Iterator iterator = secretKey.getSecretKeys(); + PGPSecretKey primaryKey = iterator.next(); + PGPSecretKey signingSubkey = iterator.next(); + PGPSecretKey encryptionSubkey = iterator.next(); + isFalse("Unexpected key", iterator.hasNext()); + + isEquals(PublicKeyAlgorithmTags.Ed448, primaryKey.getPublicKey().getAlgorithm()); + Iterator keySignatures = primaryKey.getPublicKey().getKeySignatures(); + PGPSignature directKeySignature = keySignatures.next(); + isFalse(keySignatures.hasNext()); + PGPSignatureSubpacketVector hashedSubpackets = directKeySignature.getHashedSubPackets(); + isEquals(KeyFlags.CERTIFY_OTHER, hashedSubpackets.getKeyFlags()); + + Iterator userIds = primaryKey.getUserIDs(); + isEquals(userId, userIds.next()); + isFalse(userIds.hasNext()); + Iterator userIdSignatures = primaryKey.getPublicKey().getSignaturesForID(userId); + PGPSignature userIdSig = userIdSignatures.next(); + isFalse(userIdSignatures.hasNext()); + isEquals(PGPSignature.POSITIVE_CERTIFICATION, userIdSig.getSignatureType()); + + isEquals(PublicKeyAlgorithmTags.Ed448, signingSubkey.getPublicKey().getAlgorithm()); + Iterator signingSubkeySigs = signingSubkey.getPublicKey().getKeySignatures(); + PGPSignature signingSubkeySig = signingSubkeySigs.next(); + isFalse(signingSubkeySigs.hasNext()); + isEquals(PGPSignature.SUBKEY_BINDING, signingSubkeySig.getSignatureType()); + hashedSubpackets = signingSubkeySig.getHashedSubPackets(); + isEquals(KeyFlags.SIGN_DATA, hashedSubpackets.getKeyFlags()); + + isEquals(PublicKeyAlgorithmTags.X448, encryptionSubkey.getPublicKey().getAlgorithm()); + Iterator encryptionSubkeySigs = encryptionSubkey.getPublicKey().getKeySignatures(); + PGPSignature encryptionSubkeySig = encryptionSubkeySigs.next(); + isFalse(encryptionSubkeySigs.hasNext()); + isEquals(PGPSignature.SUBKEY_BINDING, encryptionSubkeySig.getSignatureType()); + hashedSubpackets = encryptionSubkeySig.getHashedSubPackets(); + isEquals(KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, hashedSubpackets.getKeyFlags()); + } + + private void testGenerateCustomKey(APIProvider apiProvider) + throws PGPException + { + Date creationTime = currentTimeRounded(); + OpenPGPV6KeyGenerator generator = apiProvider.getKeyGenerator(creationTime); + + PGPSecretKeyRing secretKey = generator + .withPrimaryKey( + keyGen -> keyGen.generateRsaKeyPair(4096), + subpackets -> + { + subpackets.removePacketsOfType(SignatureSubpacketTags.KEY_FLAGS); + subpackets.setKeyFlags(KeyFlags.CERTIFY_OTHER); + + subpackets.removePacketsOfType(SignatureSubpacketTags.FEATURES); + subpackets.setFeature(false, Features.FEATURE_SEIPD_V2); + + subpackets.addNotationData(false, true, + "notation@example.com", "CYBER"); + + subpackets.setPreferredKeyServer(false, "https://example.com/openpgp/cert.asc"); + return subpackets; + }, + "primary-key-passphrase".toCharArray()) + .addUserId("Alice ", PGPSignature.DEFAULT_CERTIFICATION, null) + .addSigningSubkey( + PGPKeyPairGenerator::generateEd448KeyPair, + bindingSubpackets -> + { + bindingSubpackets.addNotationData(false, true, + "notation@example.com", "ZAUBER"); + return bindingSubpackets; + }, + null, + "signing-key-passphrase".toCharArray()) + .addEncryptionSubkey(PGPKeyPairGenerator::generateX448KeyPair, + "encryption-key-passphrase".toCharArray()) + .build(); + + Iterator keyIt = secretKey.getSecretKeys(); + PGPSecretKey primaryKey = keyIt.next(); + isEquals("Primary key MUST be RSA_GENERAL", + PublicKeyAlgorithmTags.RSA_GENERAL, primaryKey.getPublicKey().getAlgorithm()); + isEquals("Primary key MUST be 4096 bits", 4096, primaryKey.getPublicKey().getBitStrength()); + isEquals("Primary key creation time mismatch", + creationTime, primaryKey.getPublicKey().getCreationTime()); + PGPSignature directKeySig = primaryKey.getPublicKey().getKeySignatures().next(); + PGPSignatureSubpacketVector hashedSubpackets = directKeySig.getHashedSubPackets(); + isEquals("Primary key key flags mismatch", + KeyFlags.CERTIFY_OTHER, hashedSubpackets.getKeyFlags()); + isEquals("Primary key features mismatch", + Features.FEATURE_SEIPD_V2, hashedSubpackets.getFeatures().getFeatures()); + isEquals("Primary key sig notation data mismatch", + "CYBER", + hashedSubpackets.getNotationDataOccurrences("notation@example.com")[0].getNotationValue()); + + Iterator uids = primaryKey.getUserIDs(); + String uid = uids.next(); + isFalse("Unexpected additional UID", uids.hasNext()); + PGPSignature uidSig = primaryKey.getPublicKey().getSignaturesForID(uid).next(); + isEquals("UID binding sig type mismatch", + PGPSignature.DEFAULT_CERTIFICATION, uidSig.getSignatureType()); + + PGPSecretKey signingSubkey = keyIt.next(); + isEquals("Subkey MUST be Ed448", + PublicKeyAlgorithmTags.Ed448, signingSubkey.getPublicKey().getAlgorithm()); + isEquals("Subkey creation time mismatch", + creationTime, signingSubkey.getPublicKey().getCreationTime()); + PGPSignature sigSubBinding = signingSubkey.getPublicKey().getKeySignatures().next(); + PGPSignatureSubpacketVector sigSubBindHashPkts = sigSubBinding.getHashedSubPackets(); + isEquals("Encryption subkey key flags mismatch", + KeyFlags.SIGN_DATA, sigSubBindHashPkts.getKeyFlags()); + isEquals("Subkey notation data mismatch", + "ZAUBER", + sigSubBindHashPkts.getNotationDataOccurrences("notation@example.com")[0].getNotationValue()); + isFalse("Missing embedded primary key binding signature", + sigSubBindHashPkts.getEmbeddedSignatures().isEmpty()); + + PGPSecretKey encryptionSubkey = keyIt.next(); + isFalse("Unexpected additional subkey", keyIt.hasNext()); + isEquals("Subkey MUST be X448", + PublicKeyAlgorithmTags.X448, encryptionSubkey.getPublicKey().getAlgorithm()); + isEquals("Subkey creation time mismatch", + creationTime, encryptionSubkey.getPublicKey().getCreationTime()); + PGPSignature encryptionBinding = encryptionSubkey.getPublicKey().getKeySignatures().next(); + PGPSignatureSubpacketVector encBindHashPkts = encryptionBinding.getHashedSubPackets(); + isEquals("Encryption subkey key flags mismatch", + KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, encBindHashPkts.getKeyFlags()); + isTrue("Unexpected embedded primary key binding signature in encryption subkey binding", + encBindHashPkts.getEmbeddedSignatures().isEmpty()); + + BcPBESecretKeyDecryptorBuilder keyDecryptorBuilder = new BcPBESecretKeyDecryptorBuilder( + new BcPGPDigestCalculatorProvider()); + + isNotNull("Could not decrypt primary key using correct passphrase", + primaryKey.extractPrivateKey(keyDecryptorBuilder.build("primary-key-passphrase".toCharArray()))); + isNotNull("Could not decrypt signing subkey using correct passphrase", + signingSubkey.extractPrivateKey(keyDecryptorBuilder.build("signing-key-passphrase".toCharArray()))); + isNotNull("Could not decrypt encryption subkey using correct passphrase", + encryptionSubkey.extractPrivateKey(keyDecryptorBuilder.build("encryption-key-passphrase".toCharArray()))); + } + + private void testEnforcesPrimaryOrSubkeyType(APIProvider apiProvider) + throws PGPException + { + isNotNull(testException( + "Primary key MUST NOT consist of subkey packet.", + "IllegalArgumentException", + () -> + apiProvider.getKeyGenerator().withPrimaryKey((KeyPairGeneratorCallback) keyGenCallback -> + keyGenCallback.generateSigningSubkey() + .asSubkey(new BcKeyFingerprintCalculator())) // subkey as primary key is illegal + )); + + isNotNull(testException( + "Encryption subkey MUST NOT consist of a primary key packet.", + "IllegalArgumentException", + () -> + apiProvider.getKeyGenerator().withPrimaryKey() + .addEncryptionSubkey(new BcPGPKeyPairGeneratorProvider() + .get(6, new Date()) + .generateX25519KeyPair(), null, null) // primary key as subkey is illegal + )); + + isNotNull(testException( + "Signing subkey MUST NOT consist of primary key packet.", + "IllegalArgumentException", + () -> + apiProvider.getKeyGenerator().withPrimaryKey() + .addSigningSubkey(new BcPGPKeyPairGeneratorProvider() + .get(6, new Date()) + .generateEd25519KeyPair(), null, null, null) // primary key as subkey is illegal + )); + } + + private abstract static class APIProvider + { + public OpenPGPV6KeyGenerator getKeyGenerator() + throws PGPException + { + return getKeyGenerator(new Date()); + } + + public OpenPGPV6KeyGenerator getKeyGenerator(Date creationTime) + throws PGPException + { + return getKeyGenerator(OpenPGPV6KeyGenerator.DEFAULT_SIGNATURE_HASH_ALGORITHM, creationTime, true); + } + + public OpenPGPV6KeyGenerator getKeyGenerator(boolean aeadProtection) + throws PGPException + { + return getKeyGenerator(OpenPGPV6KeyGenerator.DEFAULT_SIGNATURE_HASH_ALGORITHM, new Date(), aeadProtection); + } + + public abstract OpenPGPV6KeyGenerator getKeyGenerator(int signatureHashAlgorithm, Date creationTime, boolean aeadProtection) + throws PGPException; + } + + public static void main(String[] args) + { + runTest(new OpenPGPV6KeyGeneratorTest()); + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/AEADProtectedPGPSecretKeyTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/AEADProtectedPGPSecretKeyTest.java index 2d2429bcb5..df48813c75 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/test/AEADProtectedPGPSecretKeyTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/AEADProtectedPGPSecretKeyTest.java @@ -20,6 +20,7 @@ import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.CryptoServicesRegistrar; import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator; import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -31,9 +32,12 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; import org.bouncycastle.openpgp.operator.bc.BcAEADSecretKeyEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair; import org.bouncycastle.openpgp.operator.jcajce.JcaAEADSecretKeyEncryptorBuilder; @@ -42,6 +46,7 @@ import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory; import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.bouncycastle.util.Strings; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; import org.bouncycastle.util.encoders.Hex; public class AEADProtectedPGPSecretKeyTest @@ -65,6 +70,8 @@ public void performTest() testUnlockKeyWithWrongPassphraseBc(); testUnlockKeyWithWrongPassphraseJca(); + + reencryptKey(); } private void unlockTestVector() @@ -360,6 +367,101 @@ private void lockUnlockKeyJca( keyPair.getPrivateKey().getPrivateKeyDataPacket().getEncoded(), dec.getPrivateKeyDataPacket().getEncoded()); } + private void reencryptKey() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException + { + reencryptKeyBc(); + reencryptKeyJca(); + } + + private void reencryptKeyJca() + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, PGPException + { + BouncyCastleProvider prov = new BouncyCastleProvider(); + KeyPairGenerator eddsaGen = KeyPairGenerator.getInstance("EdDSA", prov); + + eddsaGen.initialize(new ECNamedCurveGenParameterSpec("ed25519")); + KeyPair kp = eddsaGen.generateKeyPair(); + Date creationTime = currentTimeRounded(); + String passphrase = "recycle"; + + PGPKeyPair keyPair = new JcaPGPKeyPair(PublicKeyPacket.VERSION_6, PublicKeyAlgorithmTags.Ed25519, kp, creationTime); + PBESecretKeyEncryptor cfbEncBuilder = new JcePBESecretKeyEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_128) + .setProvider(prov) + .setSecureRandom(CryptoServicesRegistrar.getSecureRandom()) + .build(passphrase.toCharArray()); + PGPDigestCalculatorProvider digestProv = new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(prov) + .build(); + + // Encrypt key using CFB mode + PGPSecretKey cfbEncKey = new PGPSecretKey( + keyPair.getPrivateKey(), + keyPair.getPublicKey(), + digestProv.get(HashAlgorithmTags.SHA1), + true, + cfbEncBuilder); + + PBESecretKeyDecryptor cfbDecryptor = new JcePBESecretKeyDecryptorBuilder(digestProv) + .setProvider(prov) + .build(passphrase.toCharArray()); + + JcaAEADSecretKeyEncryptorBuilder aeadEncBuilder = new JcaAEADSecretKeyEncryptorBuilder( + AEADAlgorithmTags.OCB, SymmetricKeyAlgorithmTags.AES_128, S2K.Argon2Params.memoryConstrainedParameters()) + .setProvider(prov); + + PGPSecretKey aeadEncKey = PGPSecretKey.copyWithNewPassword( + cfbEncKey, + cfbDecryptor, + aeadEncBuilder.build(passphrase.toCharArray(), cfbEncKey.getPublicKey().getPublicKeyPacket())); + PBESecretKeyDecryptor aeadDecryptor = new JcePBESecretKeyDecryptorBuilder(digestProv) + .setProvider(prov) + .build(passphrase.toCharArray()); + isNotNull(aeadEncKey.extractPrivateKey(aeadDecryptor)); + } + + private void reencryptKeyBc() + throws PGPException + { + Ed25519KeyPairGenerator gen = new Ed25519KeyPairGenerator(); + gen.init(new Ed25519KeyGenerationParameters(new SecureRandom())); + AsymmetricCipherKeyPair kp = gen.generateKeyPair(); + Date creationTime = currentTimeRounded(); + String passphrase = "recycle"; + PGPKeyPair keyPair = new BcPGPKeyPair(PublicKeyPacket.VERSION_6, PublicKeyAlgorithmTags.Ed25519, kp, creationTime); + + PBESecretKeyEncryptor cfbEncBuilder = new BcPBESecretKeyEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_128) + .build(passphrase.toCharArray()); + PGPDigestCalculatorProvider digestProv = new BcPGPDigestCalculatorProvider(); + + // Encrypt key using CFB mode + PGPSecretKey cfbEncKey = new PGPSecretKey( + keyPair.getPrivateKey(), + keyPair.getPublicKey(), + digestProv.get(HashAlgorithmTags.SHA1), + true, + cfbEncBuilder); + + PBESecretKeyDecryptor cfbDecryptor = new BcPBESecretKeyDecryptorBuilder(digestProv) + .build(passphrase.toCharArray()); + + BcAEADSecretKeyEncryptorBuilder aeadEncBuilder = new BcAEADSecretKeyEncryptorBuilder( + AEADAlgorithmTags.OCB, SymmetricKeyAlgorithmTags.AES_128, + S2K.Argon2Params.memoryConstrainedParameters()); + + // Reencrypt key using AEAD + PGPSecretKey aeadEncKey = PGPSecretKey.copyWithNewPassword( + cfbEncKey, + cfbDecryptor, + aeadEncBuilder.build( + passphrase.toCharArray(), + cfbEncKey.getPublicKey().getPublicKeyPacket())); + + PBESecretKeyDecryptor aeadDecryptor = new BcPBESecretKeyDecryptorBuilder(digestProv) + .build(passphrase.toCharArray()); + isNotNull(aeadEncKey.extractPrivateKey(aeadDecryptor)); + } + public static void main(String[] args) { runTest(new AEADProtectedPGPSecretKeyTest()); diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/PGPKeyPairGeneratorTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPKeyPairGeneratorTest.java new file mode 100644 index 0000000000..daf0fc5d89 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPKeyPairGeneratorTest.java @@ -0,0 +1,303 @@ +package org.bouncycastle.openpgp.test; + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPairGeneratorProvider; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPairGeneratorProvider; + +import java.util.Date; + +public class PGPKeyPairGeneratorTest + extends AbstractPgpKeyPairTest +{ + + @Override + public String getName() + { + return "PGPKeyPairGeneratorTest"; + } + + @Override + public void performTest() + throws Exception + { + performWith((int version, Date creationTime) -> + new BcPGPKeyPairGeneratorProvider() + .get(version, creationTime)); + performWith((int version, Date creationTime) -> + new JcaPGPKeyPairGeneratorProvider() + .setProvider(new BouncyCastleProvider()) + .get(version, creationTime)); + } + + private void performWith(Factory factory) + throws PGPException + { + testGenerateV4RsaKey(factory); + testGenerateV6RsaKey(factory); + + testGenerateV6Ed448Key(factory); + testGenerateV4Ed448Key(factory); + + testGenerateV6Ed25519Key(factory); + testGenerateV4Ed25519Key(factory); + + testGenerateV6X448Key(factory); + testGenerateV4X448Key(factory); + + testGenerateV6X25519Key(factory); + testGenerateV4X25519Key(factory); + + // Legacy formats + testGenerateV6LegacyEd25519KeyFails(factory); + testGenerateV4LegacyEd215519Key(factory); + + testGenerateV6LegacyX25519KeyFails(factory); + testGenerateV4LegacyX215519Key(factory); + } + + private void testGenerateV4RsaKey(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_4, creationTime); + PGPKeyPair kp = gen.generateRsaKeyPair(3072); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_4); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.RSA_GENERAL); + isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getBitStrength(), 3072); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV6RsaKey(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_6, creationTime); + PGPKeyPair kp = gen.generateRsaKeyPair(4096); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_6); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.RSA_GENERAL); + isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getBitStrength(), 4096); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV6Ed25519Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_6, creationTime); + PGPKeyPair kp = gen.generateEd25519KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_6); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.Ed25519); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), Ed25519PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV4Ed25519Key(Factory factory) + throws PGPException + { + + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_4, creationTime); + PGPKeyPair kp = gen.generateEd25519KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_4); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.Ed25519); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), Ed25519PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV6Ed448Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_6, creationTime); + PGPKeyPair kp = gen.generateEd448KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_6); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.Ed448); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), Ed448PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV4Ed448Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_4, creationTime); + PGPKeyPair kp = gen.generateEd448KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_4); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.Ed448); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), Ed448PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV6X25519Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_6, creationTime); + PGPKeyPair kp = gen.generateX25519KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_6); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.X25519); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), X25519PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV4X25519Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_4, creationTime); + PGPKeyPair kp = gen.generateX25519KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_4); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.X25519); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), X25519PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV6X448Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_6, creationTime); + PGPKeyPair kp = gen.generateX448KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_6); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.X448); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), X448PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV4X448Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_4, creationTime); + PGPKeyPair kp = gen.generateX448KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_4); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.X448); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), X448PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + + private void testGenerateV4LegacyEd215519Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_4, creationTime); + PGPKeyPair kp = gen.generateLegacyEd25519KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_4); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.EDDSA_LEGACY); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), Ed25519PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + private void testGenerateV6LegacyEd25519KeyFails(Factory factory) + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_6, creationTime); + isNotNull( + "Expected exception when attempting to generate v6 LegacyEd25519 key with (" + gen.getClass().getSimpleName() + ")", + testException( + "An implementation MUST NOT generate a v6 LegacyEd25519 key pair.", + "PGPException", + gen::generateLegacyEd25519KeyPair)); + } + + private void testGenerateV6LegacyX25519KeyFails(Factory factory) + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_6, creationTime); + isNotNull( + "Expected exception when attempting to generate v6 LegacyX25519 key with (" + gen.getClass().getSimpleName() + ")", + testException( + "An implementation MUST NOT generate a v6 LegacyX25519 key pair.", + "PGPException", + gen::generateLegacyX25519KeyPair)); + } + + private void testGenerateV4LegacyX215519Key(Factory factory) + throws PGPException + { + Date creationTime = currentTimeRounded(); + PGPKeyPairGenerator gen = factory.create(PublicKeyPacket.VERSION_4, creationTime); + PGPKeyPair kp = gen.generateLegacyX25519KeyPair(); + + isEquals("Key version mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getVersion(), PublicKeyPacket.VERSION_4); + isEquals("Key algorithm mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getAlgorithm(), PublicKeyAlgorithmTags.ECDH); + // isEquals("Key bit-strength mismatch (" + gen.getClass().getSimpleName() + ")", + // kp.getPublicKey().getBitStrength(), X25519PublicBCPGKey.LENGTH * 8); + isEquals("Key creation time mismatch (" + gen.getClass().getSimpleName() + ")", + kp.getPublicKey().getCreationTime(), creationTime); + } + + public static void main(String[] args) + { + runTest(new PGPKeyPairGeneratorTest()); + } + + @FunctionalInterface + private interface Factory + { + PGPKeyPairGenerator create(int version, Date creationTime); + } +} diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/PGPKeyRingGeneratorTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPKeyRingGeneratorTest.java new file mode 100644 index 0000000000..e6e02ed5a6 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPKeyRingGeneratorTest.java @@ -0,0 +1,113 @@ +package org.bouncycastle.openpgp.test; + +import org.bouncycastle.bcpg.AEADAlgorithmTags; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.PacketFormat; +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator; +import org.bouncycastle.crypto.generators.X25519KeyPairGenerator; +import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters; +import org.bouncycastle.crypto.params.X25519KeyGenerationParameters; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPKeyRingGenerator; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; + +public class PGPKeyRingGeneratorTest + extends AbstractPgpKeyPairTest +{ + @Override + public String getName() + { + return "PGPKeyRingGeneratorTest"; + } + + @Override + public void performTest() + throws Exception + { + generateMinimalV6Key(); + } + + private void generateMinimalV6Key() + throws PGPException, IOException + { + Date creationTime = currentTimeRounded(); + Ed25519KeyPairGenerator edGen = new Ed25519KeyPairGenerator(); + edGen.init(new Ed25519KeyGenerationParameters(CryptoServicesRegistrar.getSecureRandom())); + AsymmetricCipherKeyPair edKp = edGen.generateKeyPair(); + PGPKeyPair primaryKp = new BcPGPKeyPair(PublicKeyPacket.VERSION_6, PublicKeyAlgorithmTags.Ed25519, edKp, creationTime); + + PGPSignatureSubpacketGenerator hashed = new PGPSignatureSubpacketGenerator(); + hashed.setIssuerFingerprint(true, primaryKp.getPublicKey()); + hashed.setSignatureCreationTime(true, creationTime); + hashed.setKeyFlags(true, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA); + hashed.setFeature(true, (byte) (Features.FEATURE_MODIFICATION_DETECTION | Features.FEATURE_SEIPD_V2)); + hashed.setPreferredHashAlgorithms(false, new int[] { + HashAlgorithmTags.SHA3_512, HashAlgorithmTags.SHA3_256, + HashAlgorithmTags.SHA512, HashAlgorithmTags.SHA384, HashAlgorithmTags.SHA256 + }); + hashed.setPreferredSymmetricAlgorithms(false, new int[] { + SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192, SymmetricKeyAlgorithmTags.AES_128 + }); + hashed.setPreferredAEADCiphersuites(false, new PreferredAEADCiphersuites.Combination[] { + new PreferredAEADCiphersuites.Combination(SymmetricKeyAlgorithmTags.AES_256, AEADAlgorithmTags.OCB), + new PreferredAEADCiphersuites.Combination(SymmetricKeyAlgorithmTags.AES_192, AEADAlgorithmTags.OCB), + new PreferredAEADCiphersuites.Combination(SymmetricKeyAlgorithmTags.AES_128, AEADAlgorithmTags.OCB) + }); + + PGPKeyRingGenerator gen = new PGPKeyRingGenerator( + primaryKp, + new BcPGPDigestCalculatorProvider().get(HashAlgorithmTags.SHA1), + hashed.generate(), + null, + new BcPGPContentSignerBuilder(primaryKp.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA3_512), + null); + + X25519KeyPairGenerator xGen = new X25519KeyPairGenerator(); + xGen.init(new X25519KeyGenerationParameters(CryptoServicesRegistrar.getSecureRandom())); + AsymmetricCipherKeyPair xKp = xGen.generateKeyPair(); + PGPKeyPair subKp = new BcPGPKeyPair(PublicKeyPacket.VERSION_6, PublicKeyAlgorithmTags.X25519, xKp, creationTime); + + hashed = new PGPSignatureSubpacketGenerator(); + hashed.setKeyFlags(false, KeyFlags.ENCRYPT_STORAGE | KeyFlags.ENCRYPT_COMMS); + hashed.setSignatureCreationTime(true, creationTime); + hashed.setIssuerFingerprint(true, primaryKp.getPublicKey()); + + gen.addSubKey(subKp, hashed.generate(), null, null); + + PGPPublicKeyRing certificate = gen.generatePublicKeyRing(); + PGPSecretKeyRing secretKey = gen.generateSecretKeyRing(); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + ArmoredOutputStream aOut = new ArmoredOutputStream(bOut); + BCPGOutputStream pOut = new BCPGOutputStream(aOut, PacketFormat.CURRENT); + secretKey.encode(pOut); + pOut.close(); + aOut.close(); + System.out.println(bOut); + } + + public static void main(String[] args) + { + runTest(new PGPKeyRingGeneratorTest()); + } +} 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 a603b5f55b..c4be38d411 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,7 @@ import java.security.Security; import org.bouncycastle.bcpg.test.SignatureSubpacketsTest; +import org.bouncycastle.openpgp.api.test.OpenPGPV6KeyGeneratorTest; import org.bouncycastle.util.test.SimpleTest; import org.bouncycastle.util.test.Test; @@ -85,7 +86,9 @@ public class RegressionTest new PGPv5KeyTest(), new PGPv5MessageDecryptionTest(), - new PGPv6SignatureTest() + new PGPv6SignatureTest(), + new PGPKeyPairGeneratorTest(), + new OpenPGPV6KeyGeneratorTest() }; public static void main(String[] args) diff --git a/util/src/main/java/org/bouncycastle/asn1/cryptlib/CryptlibObjectIdentifiers.java b/util/src/main/java/org/bouncycastle/asn1/cryptlib/CryptlibObjectIdentifiers.java index 88dd03f5c1..febe07e19b 100644 --- a/util/src/main/java/org/bouncycastle/asn1/cryptlib/CryptlibObjectIdentifiers.java +++ b/util/src/main/java/org/bouncycastle/asn1/cryptlib/CryptlibObjectIdentifiers.java @@ -8,5 +8,10 @@ public class CryptlibObjectIdentifiers public static final ASN1ObjectIdentifier ecc = cryptlib.branch("1").branch("5"); + /** + * Curve25519Legacy for use with ECDH. + * @see + * RFC9580 - ECC Curves for OpenPGP + */ public static final ASN1ObjectIdentifier curvey25519 = ecc.branch("1"); } diff --git a/util/src/main/java/org/bouncycastle/asn1/gnu/GNUObjectIdentifiers.java b/util/src/main/java/org/bouncycastle/asn1/gnu/GNUObjectIdentifiers.java index 0f82da0b00..e80cab700b 100644 --- a/util/src/main/java/org/bouncycastle/asn1/gnu/GNUObjectIdentifiers.java +++ b/util/src/main/java/org/bouncycastle/asn1/gnu/GNUObjectIdentifiers.java @@ -107,5 +107,11 @@ public interface GNUObjectIdentifiers */ ASN1ObjectIdentifier ellipticCurve = new ASN1ObjectIdentifier("1.3.6.1.4.1.11591.15"); + /** + * Ed25519Legacy for use with EdDSALegacy. + * + * @see + * RFC9580 - ECC Curves for OpenPGP + */ ASN1ObjectIdentifier Ed25519 = ellipticCurve.branch("1"); }