Bouncy Castle (Java):如何使用 EC 密钥正确生成 PGP 密钥环?

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

因此,以下代码似乎可以正确生成带有 EC 密钥的 PGP 密钥环(如:它可以使用 Bouncycastle 进行解析)。但是,Thunderbird 和 GnuPG 都存在问题。这是代码,它基于我的一个已经工作的基于 RSA/Elgamal 的实现(为了便于阅读,我只包含了相关方法):

    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();
    }

那么,我得到了什么以及有什么问题:

  1. 当我选择 SHA1 作为哈希算法时。对于主密钥,GnuPG 实际上确实读取了密钥环,但是子密钥缺少“E”(加密)密钥功能(而且我们得到了关于 SHA1 不足的完全正确的警告):
 >> 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 <foo@bar.loc>
sub   cv25519 2021-09-17 []
  1. 使用 SHA256,根本无法解析密钥环:
 >> 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

谁能发现代码的问题,以便 GnuPG 和 Thunderbird 都能正确解析密钥环?

事实证明,这样的密钥环生成没问题——问题出在选择的曲线上。主密钥生成。要修复密钥生成:

  1. 一个人可以保持代码原样并选择一条与 ECDSA 方案一起工作的不同曲线,例如secp256r1 而不是 Curve25519:
    private static final ASN1ObjectIdentifier CURVE_OID = SECObjectIdentifiers.secp256r1;
  1. 但是,如果要使用Curve25519(N.B。这是一个不错的选择,更多信息请参考https://safecurves.cr.yp.to/),那么我们必须将EdDSA与Ed25519一起使用(参见: RFC 8032) 作为主密钥。然而,subkey/encryption 密钥的生成保持不变。 Ed25519 是一个以 edwards25519 为曲线的 EdDSA(相对于 ECDSA)签名方案。两个以正确的方式实现它,上面的代码有两个地方必须改变:
    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;

以及生成主密钥对,如下:

    public AsymmetricCipherKeyPair generateEd25519KeyPair() {

        Ed25519KeyPairGenerator keyPairGenerator = new Ed25519KeyPairGenerator();
        keyPairGenerator.init(new Ed25519KeyGenerationParameters(new SecureRandom()));

        return keyPairGenerator.generateKeyPair();
    }