Search code examples
javabouncycastlepgpelliptic-curvecurve-25519

Bouncy Castle (Java): How to correctly generate a PGP keyring with EC keys?


As such, the following code seems to correctly generate a PGP keyring with EC keys (as in: it can be parsed with Bouncycastle). However, both Thunderbird and GnuPG do have issues with it. Here is the code, which is based on an already working RSA/Elgamal based implementation of mine (I included only the relevant methods for the sake of readability):

    private static final Provider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
    private static final ASN1ObjectIdentifier CURVE_OID = CryptlibObjectIdentifiers.curvey25519;

    private static final int MASTER_KEY_ALGORITHM = ECDSA;
    private static final int SUB_KEY_ALGORITHM = ECDH;

    private static final int MASTER_KEY_FLAGS = AUTHENTICATION | CERTIFY_OTHER | SIGN_DATA | ENCRYPT_STORAGE | ENCRYPT_COMMS;
    private static final int SUB_KEY_FLAGS = ENCRYPT_COMMS | ENCRYPT_STORAGE;

    private static final int[] PREFERRED_HASH_ALGORITHMS = {SHA256, SHA1, SHA384, SHA512, SHA224};
    private static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = {AES_256, AES_192, AES_128};

    public PGPKeyRingGenerator createPGPKeyRingGenerator(String identity, String passphrase, int keySize)
        throws PGPException, InvalidAlgorithmParameterException {

        PGPKeyPair masterKeyPair = generateEcPgpKeyPair(MASTER_KEY_ALGORITHM);
        PGPKeyPair subKeyPair = generateEcPgpKeyPair(SUB_KEY_ALGORITHM);

        PGPDigestCalculator sha1Calc = new BcPGPDigestCalculatorProvider().get(SHA1);
        PGPDigestCalculator sha256Calc = new BcPGPDigestCalculatorProvider().get(SHA256);

        PGPSignatureSubpacketVector masterKeySubPacket = generateMasterkeySubpacket(MASTER_KEY_FLAGS);
        PGPSignatureSubpacketVector subKeySubPacket = generateSubkeySubpacket(SUB_KEY_FLAGS);

        PGPKeyRingGenerator keyRingGenerator =
            new PGPKeyRingGenerator(POSITIVE_CERTIFICATION, masterKeyPair, identity, sha1Calc, masterKeySubPacket,
                                    null,
                                    new JcaPGPContentSignerBuilder(masterKeyPair.getPublicKey().getAlgorithm(), SHA256)
                                        .setProvider(BOUNCY_CASTLE_PROVIDER),
                                    new JcePBESecretKeyEncryptorBuilder(AES_256, sha256Calc)
                                        .setProvider(BOUNCY_CASTLE_PROVIDER)
                                        .build(passphrase.toCharArray()));

        keyRingGenerator.addSubKey(subKeyPair, subKeySubPacket, null);

        return keyRingGenerator;
    }

    private AsymmetricCipherKeyPair generateEcKeyPair(ASN1ObjectIdentifier curveOid) {

        X9ECParameters curve = CustomNamedCurves.getByOID(curveOid);
        ECNamedDomainParameters ecDomainParameters = new ECNamedDomainParameters(curveOid, curve.getCurve(), curve.getG(), curve.getN(), curve.getH(), curve.getSeed());

        ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator();
        keyPairGenerator.init(new ECKeyGenerationParameters(ecDomainParameters, new SecureRandom()));

        return keyPairGenerator.generateKeyPair();
    }

    private BcPGPKeyPair generateEcPgpKeyPair(int algorithm)
        throws InvalidAlgorithmParameterException, PGPException {

        return new BcPGPKeyPair(algorithm, generateEcKeyPair(CURVE_OID), new Date());
    }

    private PGPSignatureSubpacketVector generateMasterkeySubpacket(int keyFlags) {

        PGPSignatureSubpacketGenerator subpacketGen = new PGPSignatureSubpacketGenerator();

        subpacketGen.setKeyFlags(false, keyFlags);
        subpacketGen.setPreferredSymmetricAlgorithms(false, PREFERRED_SYMMETRIC_ALGORITHMS);
        subpacketGen.setPreferredHashAlgorithms(false, PREFERRED_HASH_ALGORITHMS);

        return subpacketGen.generate();
    }

    private PGPSignatureSubpacketVector generateSubkeySubpacket(int keyFlags) {

        PGPSignatureSubpacketGenerator subpacketGen = new PGPSignatureSubpacketGenerator();
        subpacketGen.setKeyFlags(false, keyFlags);

        return subpacketGen.generate();
    }

So, what do I get here and what are the issues:

  1. When I pick SHA1 as hashing alg. for the master key, GnuPG actually does read the keyring, but the subkey is missing the "E" (encryption) key capability (and also we get the perfectly correct warning about SHA1 being insufficient):
 >> gpg ./ec-sha1.asc
gpg: WARNING: no command supplied.  Trying to guess what you mean ...
gpg: ECDSA key 8DF084241E957FFF requires a 256 bit or larger hash (hash is SHA1)
gpg: ECDSA key 8DF084241E957FFF requires a 256 bit or larger hash (hash is SHA1)
pub   cv25519 2021-09-17 [SCA]
      553E322AB50692F67E23FE7B8DF084241E957FFF
uid           Foo Bar <[email protected]>
sub   cv25519 2021-09-17 []
  1. With SHA256, the keyring cannot be parsed at all:
 >> gpg ./ec.asc
gpg: WARNING: no command supplied.  Trying to guess what you mean ...
gpg: Fatal: _gcry_mpi_ec_add_points: Montgomery not yet supported

Can anyone spot the issue with the code, so that both GnuPG and Thunderbird will be able to correctly parse the keyring?


Solution

  • Turns out, the keyring generation as such is alright -- the issue was with the chosen curve resp. the master key generation. To fix the key generation:

    1. One either can keep the code as is and pick a different curve which works with the ECDSA scheme e.g. secp256r1 instead of Curve25519:
        private static final ASN1ObjectIdentifier CURVE_OID = SECObjectIdentifiers.secp256r1;
    
    1. However, if Curve25519 is to be used (N.B. which is a good choice, for further information please refer to https://safecurves.cr.yp.to/), then we have to use EdDSA with Ed25519 (see: RFC 8032) for the master key. The generation of the subkey/encryption key, however, stays the same. Ed25519 is an EdDSA (as opposed to ECDSA) signature scheme with edwards25519 as the curve. To implement it the correct way, two things about the code above have to be changed:
        private static final int MASTER_KEY_ALGORITHM = PublicKeyAlgorithmTags.EDDSA;
        private static final ASN1ObjectIdentifier MASTER_CURVE_OID = EdECObjectIdentifiers.id_Ed25519;
        private static final ASN1ObjectIdentifier SUB_CURVE_OID = CryptlibObjectIdentifiers.curvey25519;
    

    And the master key pair generation, which is as follows:

        public AsymmetricCipherKeyPair generateEd25519KeyPair() {
    
            Ed25519KeyPairGenerator keyPairGenerator = new Ed25519KeyPairGenerator();
            keyPairGenerator.init(new Ed25519KeyGenerationParameters(new SecureRandom()));
    
            return keyPairGenerator.generateKeyPair();
        }