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();
}
那么,我得到了什么以及有什么问题:
- 当我选择 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 []
- 使用 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 都能正确解析密钥环?
事实证明,这样的密钥环生成没问题——问题出在选择的曲线上。主密钥生成。要修复密钥生成:
- 一个人可以保持代码原样并选择一条与 ECDSA 方案一起工作的不同曲线,例如
secp256r1
而不是 Curve25519
:
private static final ASN1ObjectIdentifier CURVE_OID = SECObjectIdentifiers.secp256r1;
- 但是,如果要使用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();
}
因此,以下代码似乎可以正确生成带有 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();
}
那么,我得到了什么以及有什么问题:
- 当我选择 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 []
- 使用 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 都能正确解析密钥环?
事实证明,这样的密钥环生成没问题——问题出在选择的曲线上。主密钥生成。要修复密钥生成:
- 一个人可以保持代码原样并选择一条与 ECDSA 方案一起工作的不同曲线,例如
secp256r1
而不是Curve25519
:
private static final ASN1ObjectIdentifier CURVE_OID = SECObjectIdentifiers.secp256r1;
- 但是,如果要使用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();
}