From 3adc1c5204d433d0fed4c8d04efb9991a3bfe007 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Jan 2026 14:19:25 +0100 Subject: [PATCH 1/3] Remove workaround removing key flag packets before adding new ones. This workaround can be removed, as per 6c5020715ee785a0223a6923993ca6f6304b3902 setKeyFlags() will remove existing keyFlag packets on its own --- .../java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java index 612320cd4d..902cbac6d5 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java @@ -10,7 +10,6 @@ import org.bouncycastle.bcpg.PublicKeyUtils; import org.bouncycastle.bcpg.PublicSubkeyPacket; import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SignatureSubpacketTags; import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; @@ -235,7 +234,6 @@ public WithPrimaryKey signOnlyKey() @Override public PGPSignatureSubpacketGenerator apply(PGPSignatureSubpacketGenerator subpackets) { - subpackets.removePacketsOfType(SignatureSubpacketTags.KEY_FLAGS); subpackets.setKeyFlags(true, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA); return subpackets; } From 1d05a25710032b8d1798a8e28300bb11bcd31259 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Jan 2026 14:37:59 +0100 Subject: [PATCH 2/3] OpenPGPKeyGenerator: Add factory methods for generating singleton- and composite RSA keys --- .../openpgp/api/OpenPGPKeyGenerator.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java index 902cbac6d5..aaf3e2299c 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKeyGenerator.java @@ -10,6 +10,7 @@ import org.bouncycastle.bcpg.PublicKeyUtils; import org.bouncycastle.bcpg.PublicSubkeyPacket; import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SignatureSubpacketTags; import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; @@ -240,6 +241,93 @@ public PGPSignatureSubpacketGenerator apply(PGPSignatureSubpacketGenerator subpa })); } + /** + * Generate an OpenPGP key based on a single, multipurpose RSA key of the given strength. + * The key will carry a direct-key signature containing default preferences and flags. + * If the passed in userId is non-null, it will be added to the key. + * + * @param bitStrength strength of the RSA key in bits (recommended values: 3072 and above). + * @param userId optional user-id + * @return builder + * @throws PGPException if the key cannot be generated. + */ + public WithPrimaryKey singletonRSAKey(int bitStrength, String userId) + throws PGPException + { + WithPrimaryKey builder = withPrimaryKey( + new KeyPairGeneratorCallback() + { + @Override + public PGPKeyPair generateFrom(PGPKeyPairGenerator generator) + throws PGPException + { + return generator.generateRsaKeyPair(bitStrength); + } + }, + SignatureParameters.Callback.Util.modifyHashedSubpackets( + new SignatureSubpacketsFunction() + { + @Override + public PGPSignatureSubpacketGenerator apply(PGPSignatureSubpacketGenerator subpackets) + { + subpackets.removePacketsOfType(SignatureSubpacketTags.KEY_FLAGS); + subpackets.setKeyFlags(KeyFlags.CERTIFY_OTHER + | KeyFlags.SIGN_DATA + | KeyFlags.ENCRYPT_STORAGE + | KeyFlags.ENCRYPT_COMMS); + return subpackets; + } + } + ) + ); + + if (userId != null) + { + builder.addUserId(userId); + } + + return builder; + } + + /** + * Generate an OpenPGP key composed of 3 individual component keys based on RSA of the given strength in bits. + * The primary key will be used to certify third-party keys. + * A subkey is used for signing and a third subkey is used for encryption of messages both for storage and + * communications. + * The primary key will carry a direct-key signature containing default preferences and flags. + * The subkeys will be bound with subkey binding signatures. + * If the passed in userId is non-null, it will be added to the key. + * + * @param bitStrength strength of the keys in bits (recommended values: 3072 and above) + * @param userId optional user-id + * @return builder + * @throws PGPException if the key cannot be generated + */ + public WithPrimaryKey compositeRSAKey(int bitStrength, String userId) + throws PGPException + { + KeyPairGeneratorCallback generatorCallback = new KeyPairGeneratorCallback() + { + @Override + public PGPKeyPair generateFrom(PGPKeyPairGenerator generator) + throws PGPException + { + return generator.generateRsaKeyPair(bitStrength); + } + }; + + WithPrimaryKey builder = withPrimaryKey(generatorCallback) + .addSigningSubkey(generatorCallback) + .addEncryptionSubkey(generatorCallback); + + if (userId != null) + { + builder.addUserId(userId); + } + + return builder; + } + /** * Generate an OpenPGP key with a certification-capable primary key. * See {@link PGPKeyPairGenerator#generatePrimaryKey()} for the primary key type From ffd852d13ff6bd1176bd997be479060c9c33bc7b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Jan 2026 14:51:10 +0100 Subject: [PATCH 3/3] Add tests for RSA key factory methods --- .../api/test/OpenPGPV6KeyGeneratorTest.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) 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 index 87a3dd5b8b..d8bc32966c 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPV6KeyGeneratorTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPV6KeyGeneratorTest.java @@ -60,6 +60,9 @@ protected void performTestWith(OpenPGPApi api) testGenerateEd25519x25519Key(api); testGenerateEd448x448Key(api); + testGenerateSingletonRSAKey(api); + testGenerateCompositeRSAKey(api); + testEnforcesPrimaryOrSubkeyType(api); testGenerateKeyWithoutSignatures(api); } @@ -320,6 +323,59 @@ private void testGenerateEd448x448Key(OpenPGPApi api) isEquals(KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, hashedSubpackets.getKeyFlags()); } + private void testGenerateSingletonRSAKey(OpenPGPApi api) + throws PGPException + { + Date creationTime = currentTimeRounded(); + OpenPGPKeyGenerator generator = api.generateKey(creationTime, false); + + OpenPGPKey key = generator.singletonRSAKey(4096, "Alice ") + .build(); + + isEquals("Singleton RSA key MUST consist of only a single primary key.", 1, key.getKeys().size()); + OpenPGPCertificate.OpenPGPComponentKey primaryKey = key.getPrimaryKey(); + isEquals("Primary key MUST be an RSA key", PublicKeyAlgorithmTags.RSA_GENERAL, primaryKey.getAlgorithm()); + isEquals("Primary key MUST have a strength of 4096 bits.", 4096, primaryKey.getPGPPublicKey().getBitStrength()); + + isEquals("The primary key MUST be the certification key", primaryKey, key.getCertificationKeys().get(0)); + isEquals("The primary key MUST be the encryption key", primaryKey, key.getEncryptionKeys().get(0)); + isEquals("The primary key MUST be the signing key", primaryKey, key.getSigningKeys().get(0)); + + isNotNull(key.getUserId("Alice ")); + } + + private void testGenerateCompositeRSAKey(OpenPGPApi api) + throws PGPException + { + Date creationTime = currentTimeRounded(); + OpenPGPKeyGenerator generator = api.generateKey(creationTime, false); + + OpenPGPKey key = generator.compositeRSAKey(4096, "Alice ") + .build(); + + isEquals("The composite RSA key MUST consist of 3 component keys", 3, key.getKeys().size()); + + OpenPGPCertificate.OpenPGPComponentKey primaryKey = key.getPrimaryKey(); + isEquals("Primary key MUST be an RSA key", PublicKeyAlgorithmTags.RSA_GENERAL, primaryKey.getAlgorithm()); + isEquals("Primary key MUST have a strength of 4096 bits.", 4096, primaryKey.getPGPPublicKey().getBitStrength()); + isEquals("There MUST be only one certification key", 1, key.getCertificationKeys().size()); + isEquals("The primary key MUST be the certification key", primaryKey, key.getCertificationKeys().get(0)); + + isEquals("There MUST be only one signing key", 1, key.getSigningKeys().size()); + OpenPGPCertificate.OpenPGPComponentKey signingKey = key.getSigningKeys().get(0); + isEquals("Signing key MUST be an RSA key", PublicKeyAlgorithmTags.RSA_GENERAL, signingKey.getAlgorithm()); + isEquals("Signing key MUST have a strength of 4096 bits.", 4096, signingKey.getPGPPublicKey().getBitStrength()); + isFalse("The signing key MUST NOT be the primary key", primaryKey.equals(signingKey)); + + isEquals("There MUST be only one encryption key", 1, key.getEncryptionKeys().size()); + OpenPGPCertificate.OpenPGPComponentKey encryptionKey = key.getEncryptionKeys().get(0); + isEquals("Primary key MUST be an RSA key", PublicKeyAlgorithmTags.RSA_GENERAL, encryptionKey.getAlgorithm()); + isEquals("Encryption key MUST have a strength of 4096 bits.", 4096, encryptionKey.getPGPPublicKey().getBitStrength()); + isFalse("The encryption key MUST NOT be the primary key", primaryKey.equals(encryptionKey)); + + isFalse("The signing key MUST NOT be the encryption key", signingKey.equals(encryptionKey)); + } + private void testGenerateCustomKey(OpenPGPApi api) throws PGPException {