Java 的 BouncyCastle 并不总是验证 OpenSSL ECDSA 签名
Java's BouncyCastle doesn't always verify OpenSSL ECDSA signature
我使用 OpenSSL(在 C++ 中)签署文本,但是我的 Java 程序并不总是验证签名的消息(只有大约五分之一得到验证)。有趣的是 https://kjur.github.io/jsrsasign/sample/sample-ecdsa.html 没有验证其中任何一个:
曲线名称:secp256k1
签名算法:SHA256withECDSA
私钥
431313701ec60d303fa7d027d5f1579eaa57f0e870b23e3a25876e61bed2caa3
公钥
035bcefc4a6ca257e394e82c20027db2af368474afb8917273713644f11a7cecb3
失败:
text to sign=
pcax2727gRo8M6vf9Vjhr1JDrQ3rdPYu6xx81000pcax273z8kaV5Ugsiqz3tvWGo8Gg6sch6V4912341535867163229
signature=
3044022061dff8e39f9324b0794ec2c58abda971898f694ca980baf3c2a4045a9048b441022054a2fb8ef3d383fd7eeb31425dba440e2fd2053778d4ab3725046385c7845cff0000
成功:
text to sign=
pcax2727gRo8M6vf9Vjhr1JDrQ3rdPYu6xx81000pcax273z8kaV5Ugsiqz3tvWGo8Gg6sch6V4912341535867122614
signature=
3046022100f200d0fb9e86a16bd46ee2dd11f1840a436d0a5c6823001a516e975a44906fcf022100d062a60611fc0f21d81fa3140741c8b6e650fff33d2c48aef69a3a40d7c7b3ca
Java
private static final String SHA256WITH_ECDSA = "SHA256withECDSA";
public static boolean isValidSignature(PublicKey pub, byte[] dataToVerify, byte[] signature) {
try {
Signature sign = Signature.getInstance(SHA256WITH_ECDSA, BouncyCastleProvider.PROVIDER_NAME);
sign.initVerify(pub);
sign.update(dataToVerify);
return sign.verify(signature);
} catch (Exception e) {
log.error("Error: " + e.getMessage());
}
return false;
}
C++
std::vector<unsigned char> utils::crypto::sign(std::string& private_key_58, std::string& message) {
auto priv_bytes = utils::base58::decode_base(private_key_58);
auto digest = utils::crypto::sha256(message);
auto key = utils::crypto::ec_new_keypair(priv_bytes);
auto signature = ECDSA_do_sign(digest.data(), digest.size(), key);
auto der_len = ECDSA_size(key);
auto der = (uint8_t*) calloc(der_len, sizeof(uint8_t));
auto der_copy = der;
i2d_ECDSA_SIG(signature, &der_copy);
std::vector<unsigned char> s (der, der+der_len);
return s;
}
std::vector<unsigned char> utils::crypto::sha256(std::string& str) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256_CTX sha256;
SHA256_Init(&sha256);
SHA256_Update(&sha256, str.c_str(), str.size());
SHA256_Final(hash, &sha256);
std::vector<unsigned char> data(hash, hash + SHA256_DIGEST_LENGTH);
return data;
}
EC_KEY *utils::crypto::ec_new_keypair(std::vector<unsigned char>& priv_bytes) {
EC_KEY *key = nullptr;
BIGNUM *priv = nullptr;
BN_CTX *ctx = nullptr;
const EC_GROUP *group = nullptr;
EC_POINT *pub = nullptr;
key = EC_KEY_new_by_curve_name(NID_secp256k1);
if (!key) {
std::cerr << "Can't generate curve secp256k1\n";
std::abort();
}
priv = BN_new();
BN_bin2bn(priv_bytes.data(), 32, priv);
EC_KEY_set_private_key(key, priv);
ctx = BN_CTX_new();
BN_CTX_start(ctx);
group = EC_KEY_get0_group(key);
pub = EC_POINT_new(group);
EC_POINT_mul(group, pub, priv, NULL, NULL, ctx);
EC_KEY_set_public_key(key, pub);
EC_POINT_free(pub);
BN_CTX_end(ctx);
BN_CTX_free(ctx);
BN_clear_free(priv);
return key;
}
Neardupes ECDSA signature length and (还有更多链接)
ASN.1 DER 编码是可变大小的 除了某些非常有限的数据,特别是对于 ECDSA(或 DSA)签名。 ECDSA_size
returns 给定密钥可能的 最大 长度,但每个实际签名可能是该长度或更短,具体取决于值 r 的二进制表示和签名中的 s,出于您的目的,本质上可以将其视为随机数。
在实际签名短于 ECDSA_size
的情况下,您仍然对整个缓冲区进行编码并将其传递给您的 Java;注意到 'failed' 示例末尾的两个零字节(0000
十六进制)? DER 解码器 可以 忽略尾随垃圾,当我在较旧的 BouncyCastle 和 SunEC 提供商上测试这种情况时,它实际上工作正常,但从 BouncyCastle 1.54 开始对我来说失败了——相当明显的异常,
java.security.SignatureException: error decoding signature bytes.
-- 并且 SunEC 从 8u121 开始,原因或异常类似于 java.security.SignatureException: Invalid encoding for signature
。
在对 'lax' 编码(包括比特币中的 secp256k1 签名)进行了一些成功的攻击之后,许多实施最近使 DER 解码更加严格——参见 https://bitcoin.stackexchange.com/questions/51706/what-can-be-changed-in-signed-bitcoin-transaction and https://en.bitcoin.it/wiki/Transaction_malleability . This is mentioned in the Oracle Java 8u121 release notes 项目 "More checks added to DER encoding parsing code" 虽然我不知道看不到 Bouncy 的任何类似内容。
由于 secp256k1 是一个 Certicom/X9 'prime' (Fp) 曲线组,它的余因子是 1 并且它的顺序非常接近基础字段大小,而基础字段大小又非常接近 256 位是 8 的倍数,因此该组中的签名将 DER 编码为最大长度(并工作)几乎正好是 1/4 (25%) 的时间;剩下的时间他们会失败。
官方最好的解决方案是使用指针中的更新值,这里是 der_copy
,由(任何)i2d*
例程输出,以确定编码的长度,并使用它长度。如果由于某种原因你不能处理可变长度,你可以传输整个缓冲区,然后在传递给 BouncyCastle(或 SunEC)之前截断它,方法是使用 2+signature[1]
作为有效长度——但如果你改为曲线大于约 480 位;上面是不同的,更复杂。
我使用 OpenSSL(在 C++ 中)签署文本,但是我的 Java 程序并不总是验证签名的消息(只有大约五分之一得到验证)。有趣的是 https://kjur.github.io/jsrsasign/sample/sample-ecdsa.html 没有验证其中任何一个:
曲线名称:secp256k1 签名算法:SHA256withECDSA
私钥
431313701ec60d303fa7d027d5f1579eaa57f0e870b23e3a25876e61bed2caa3
公钥
035bcefc4a6ca257e394e82c20027db2af368474afb8917273713644f11a7cecb3
失败:
text to sign=
pcax2727gRo8M6vf9Vjhr1JDrQ3rdPYu6xx81000pcax273z8kaV5Ugsiqz3tvWGo8Gg6sch6V4912341535867163229
signature=
3044022061dff8e39f9324b0794ec2c58abda971898f694ca980baf3c2a4045a9048b441022054a2fb8ef3d383fd7eeb31425dba440e2fd2053778d4ab3725046385c7845cff0000
成功:
text to sign=
pcax2727gRo8M6vf9Vjhr1JDrQ3rdPYu6xx81000pcax273z8kaV5Ugsiqz3tvWGo8Gg6sch6V4912341535867122614
signature=
3046022100f200d0fb9e86a16bd46ee2dd11f1840a436d0a5c6823001a516e975a44906fcf022100d062a60611fc0f21d81fa3140741c8b6e650fff33d2c48aef69a3a40d7c7b3ca
Java
private static final String SHA256WITH_ECDSA = "SHA256withECDSA";
public static boolean isValidSignature(PublicKey pub, byte[] dataToVerify, byte[] signature) {
try {
Signature sign = Signature.getInstance(SHA256WITH_ECDSA, BouncyCastleProvider.PROVIDER_NAME);
sign.initVerify(pub);
sign.update(dataToVerify);
return sign.verify(signature);
} catch (Exception e) {
log.error("Error: " + e.getMessage());
}
return false;
}
C++
std::vector<unsigned char> utils::crypto::sign(std::string& private_key_58, std::string& message) {
auto priv_bytes = utils::base58::decode_base(private_key_58);
auto digest = utils::crypto::sha256(message);
auto key = utils::crypto::ec_new_keypair(priv_bytes);
auto signature = ECDSA_do_sign(digest.data(), digest.size(), key);
auto der_len = ECDSA_size(key);
auto der = (uint8_t*) calloc(der_len, sizeof(uint8_t));
auto der_copy = der;
i2d_ECDSA_SIG(signature, &der_copy);
std::vector<unsigned char> s (der, der+der_len);
return s;
}
std::vector<unsigned char> utils::crypto::sha256(std::string& str) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256_CTX sha256;
SHA256_Init(&sha256);
SHA256_Update(&sha256, str.c_str(), str.size());
SHA256_Final(hash, &sha256);
std::vector<unsigned char> data(hash, hash + SHA256_DIGEST_LENGTH);
return data;
}
EC_KEY *utils::crypto::ec_new_keypair(std::vector<unsigned char>& priv_bytes) {
EC_KEY *key = nullptr;
BIGNUM *priv = nullptr;
BN_CTX *ctx = nullptr;
const EC_GROUP *group = nullptr;
EC_POINT *pub = nullptr;
key = EC_KEY_new_by_curve_name(NID_secp256k1);
if (!key) {
std::cerr << "Can't generate curve secp256k1\n";
std::abort();
}
priv = BN_new();
BN_bin2bn(priv_bytes.data(), 32, priv);
EC_KEY_set_private_key(key, priv);
ctx = BN_CTX_new();
BN_CTX_start(ctx);
group = EC_KEY_get0_group(key);
pub = EC_POINT_new(group);
EC_POINT_mul(group, pub, priv, NULL, NULL, ctx);
EC_KEY_set_public_key(key, pub);
EC_POINT_free(pub);
BN_CTX_end(ctx);
BN_CTX_free(ctx);
BN_clear_free(priv);
return key;
}
Neardupes ECDSA signature length and
ASN.1 DER 编码是可变大小的 除了某些非常有限的数据,特别是对于 ECDSA(或 DSA)签名。 ECDSA_size
returns 给定密钥可能的 最大 长度,但每个实际签名可能是该长度或更短,具体取决于值 r 的二进制表示和签名中的 s,出于您的目的,本质上可以将其视为随机数。
在实际签名短于 ECDSA_size
的情况下,您仍然对整个缓冲区进行编码并将其传递给您的 Java;注意到 'failed' 示例末尾的两个零字节(0000
十六进制)? DER 解码器 可以 忽略尾随垃圾,当我在较旧的 BouncyCastle 和 SunEC 提供商上测试这种情况时,它实际上工作正常,但从 BouncyCastle 1.54 开始对我来说失败了——相当明显的异常,
java.security.SignatureException: error decoding signature bytes.
-- 并且 SunEC 从 8u121 开始,原因或异常类似于 java.security.SignatureException: Invalid encoding for signature
。
在对 'lax' 编码(包括比特币中的 secp256k1 签名)进行了一些成功的攻击之后,许多实施最近使 DER 解码更加严格——参见 https://bitcoin.stackexchange.com/questions/51706/what-can-be-changed-in-signed-bitcoin-transaction and https://en.bitcoin.it/wiki/Transaction_malleability . This is mentioned in the Oracle Java 8u121 release notes 项目 "More checks added to DER encoding parsing code" 虽然我不知道看不到 Bouncy 的任何类似内容。
由于 secp256k1 是一个 Certicom/X9 'prime' (Fp) 曲线组,它的余因子是 1 并且它的顺序非常接近基础字段大小,而基础字段大小又非常接近 256 位是 8 的倍数,因此该组中的签名将 DER 编码为最大长度(并工作)几乎正好是 1/4 (25%) 的时间;剩下的时间他们会失败。
官方最好的解决方案是使用指针中的更新值,这里是 der_copy
,由(任何)i2d*
例程输出,以确定编码的长度,并使用它长度。如果由于某种原因你不能处理可变长度,你可以传输整个缓冲区,然后在传递给 BouncyCastle(或 SunEC)之前截断它,方法是使用 2+signature[1]
作为有效长度——但如果你改为曲线大于约 480 位;上面是不同的,更复杂。