使用 PHP 8 验证 ECDSA P-384 或 P-256 签名
Verifying an ECDSA P-384 or P-256 signature with PHP 8
我想在浏览器上使用 SubtleCrypto
和 ECDSA
曲线 P-384
或 P-256
对消息“hello”签名,然后在服务器上验证签名PHP 8.
在浏览器上,我生成了一个密钥对并用它来签署消息:
let key = await crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-384'}, true, ['sign']);
let pk = await crypto.subtle.exportKey('spki', key.publicKey);
let pk_hex = [...new Uint8Array(pk)].map(x => x.toString(16).padStart(2, '0')).join('');
let sign = await crypto.subtle.sign({name: 'ECDSA', hash: 'SHA-512'}, key.privateKey, new TextEncoder().encode('hello'));
let sign_hex = [...new Uint8Array(sign)].map(x => x.toString(16).padStart(2, '0')).join('');
然后我将 pk_hex
和 sign_hex
发送到服务器。这是他们的样子:
pk_hex:
3076301006072a8648ce3d020106052b810400220362000495914dee09e8b26de437496e16b97f9734e5854a5e0f9f7a6a3d2b63d7c4a47e570bb0d21984b24611903fa5707df154e73e9e0ad7c91b51728dc969dd029868c13db568f3e6829c7679e7354d2b7cb9d2d05109a4444a5292b65ac43fdc778a
sign_hex:
ffcdda67026a606e0a0a8933ec25b1d991a211e8ec8d07fd62d0dfe458ab57330f9fb14df6822af703a398df6ebab475ede8456864dd9ae4cf09eb1113c701a725a627d81b70025e32472df2ce8aff1916aa2b13f27c5b811a77f1c5eaffa726
在服务器上,我重建了签名和public密钥:
$signature = hex2bin($sign_hex);
$public_key = (
"-----BEGIN PUBLIC KEY-----\n" .
chunk_split(base64_encode(hex2bin($pk_hex)), 64, "\n") .
"-----END PUBLIC KEY-----"
);
然后我使用openssl_verify()
来验证签名:
openssl_verify('hello', $signature, $public_key, OPENSSL_ALGO_SHA512);
但我得到的结果是 -1
,对于 P-384
和 P-256
。
如果我尝试使用 RSASSA-PKCS1-v1_5
执行相同的操作,则验证通过。
我做错了什么?
难道是我创建的public密钥格式不正确?如果是这样,我该怎么做才能正确格式化它?或者可能是其他原因?
openssl_error_string()
returns:
error:0909006C:PEM routines:get_name:no start line
openssl_get_curve_names()
returns:
secp112r1, secp112r2, secp128r1, secp128r2, secp160k1, secp160r1, secp160r2, secp192k1, secp224k1, secp224r1, secp256k1, secp384r1, secp521r1, prime192v1, prime192v2, prime192v3, prime239v1, prime239v2, prime239v3, prime256v1, sect113r1, sect113r2, sect131r1, sect131r2, sect163k1, sect163r1, sect163r2, sect193r1, sect193r2, sect233k1, sect233r1, sect239k1, sect283k1, sect283r1, sect409k1, sect409r1, sect571k1, sect571r1, c2pnb163v1, c2pnb163v2, c2pnb163v3, c2pnb176v1, c2tnb191v1, c2tnb191v2, c2tnb191v3, c2pnb208w1, c2tnb239v1, c2tnb239v2, c2tnb239v3, c2pnb272w1, c2pnb304w1, c2tnb359v1, c2pnb368w1, c2tnb431r1, wap-wsg-idm-ecid-wtls1, wap-wsg-idm-ecid-wtls3, wap-wsg-idm-ecid-wtls4, wap-wsg-idm-ecid-wtls5, wap-wsg-idm-ecid-wtls6, wap-wsg-idm-ecid-wtls7, wap-wsg-idm-ecid-wtls8, wap-wsg-idm-ecid-wtls9, wap-wsg-idm-ecid-wtls10, wap-wsg-idm-ecid-wtls11, wap-wsg-idm-ecid-wtls12, Oakley-EC2N-3, Oakley-EC2N-4, brainpoolP160r1, brainpoolP160t1, brainpoolP192r1, brainpoolP192t1, brainpoolP224r1, brainpoolP224t1, brainpoolP256r1, brainpoolP256t1, brainpoolP320r1, brainpoolP320t1, brainpoolP384r1, brainpoolP384t1, brainpoolP512r1, brainpoolP512t1, SM2
解决方案
作为答案的详细信息,问题在于 SubtleCrypto
使用 IEEE P1363 格式对签名进行编码,而 OpenSSL 期望它采用 ASN.1/DER 格式。
我写了下面的函数来在 P1363 和 ASN.1 之间进行转换:
/**
* Converts an IEEE P1363 signature into ASN.1/DER.
*
* @param string $p1363 Binary IEEE P1363 signature.
*/
function p1363_to_asn1(string $p1363): string {
// P1363 format: r followed by s.
// ASN.1 format: 0x30 b1 0x02 b2 r 0x02 b3 s.
//
// r and s must be prefixed with 0x00 if their first byte is > 0x7f.
//
// b1 = length of contents.
// b2 = length of r after being prefixed if necessary.
// b3 = length of s after being prefixed if necessary.
$asn1 = ''; // ASN.1 contents.
$len = 0; // Length of ASN.1 contents.
$c_len = intdiv(strlen($p1363), 2); // Length of each P1363 component.
// Separate P1363 signature into its two equally sized components.
foreach (str_split($p1363, $c_len) as $c) {
// 0x02 prefix before each component.
$asn1 .= "\x02";
if (unpack('C', $c)[1] > 0x7f) {
// Add 0x00 because first byte of component > 0x7f.
// Length of component = ($c_len + 1).
$asn1 .= pack('C', $c_len + 1) . "\x00";
$len += 2 + ($c_len + 1);
} else {
$asn1 .= pack('C', $c_len);
$len += 2 + $c_len;
}
// Append formatted component to ASN.1 contents.
$asn1 .= $c;
}
// 0x30 b1, then contents.
return "\x30" . pack('C', $len) . $asn1;
}
用法示例:
$sign_hex = bin2hex(p1363_to_asn1(hex2bin($sign_hex)));
EC 签名可以指定为两种格式:r|s (IEEE P1363) 或 ASN.1/DER。 WebCrypto 使用 r|s 格式,而 PHP 需要 ASN.1 格式。
详细解释了 ASN.1 格式 here。以 ASN.1 格式发布的签名是十六进制编码的:
$sign_hex = '3066023100ffcdda67026a606e0a0a8933ec25b1d991a211e8ec8d07fd62d0dfe458ab57330f9fb14df6822af703a398df6ebab475023100ede8456864dd9ae4cf09eb1113c701a725a627d81b70025e32472df2ce8aff1916aa2b13f27c5b811a77f1c5eaffa726';
至此,PHP验证成功
这两种格式都是在ECC上下文中定义的,而RSASSA-PKCS1-v1_5是RSA上下文中的签名方案,使用统一的签名格式,所以一般不会出现相应的问题。
编辑:
ASN.1格式详解here:
0x30 b1 0x02 b2 (r) 0x02 b3 (s)
将发布的签名结果等分在r
:
ffcdda67026a606e0a0a8933ec25b1d991a211e8ec8d07fd62d0dfe458ab57330f9fb14df6822af703a398df6ebab475
和s
:
ede8456864dd9ae4cf09eb1113c701a725a627d81b70025e32472df2ce8aff1916aa2b13f27c5b811a77f1c5eaffa726
由于在这两种情况下前导字节都大于 0x7f
,因此在这两种情况下都必须添加前导 0x00
。
b2
和b3
分别表示r
和s
的长度,都是0x31
。 b1
表示后续数据的长度为0x66
,最终得到:
3066 0231 00ffcdda67026a606e0a0a8933ec25b1d991a211e8ec8d07fd62d0dfe458ab57330f9fb14df6822af703a398df6ebab475 0231 00ede8456864dd9ae4cf09eb1113c701a725a627d81b70025e32472df2ce8aff1916aa2b13f27c5b811a77f1c5eaffa726
我想在浏览器上使用 SubtleCrypto
和 ECDSA
曲线 P-384
或 P-256
对消息“hello”签名,然后在服务器上验证签名PHP 8.
在浏览器上,我生成了一个密钥对并用它来签署消息:
let key = await crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-384'}, true, ['sign']);
let pk = await crypto.subtle.exportKey('spki', key.publicKey);
let pk_hex = [...new Uint8Array(pk)].map(x => x.toString(16).padStart(2, '0')).join('');
let sign = await crypto.subtle.sign({name: 'ECDSA', hash: 'SHA-512'}, key.privateKey, new TextEncoder().encode('hello'));
let sign_hex = [...new Uint8Array(sign)].map(x => x.toString(16).padStart(2, '0')).join('');
然后我将 pk_hex
和 sign_hex
发送到服务器。这是他们的样子:
pk_hex:
3076301006072a8648ce3d020106052b810400220362000495914dee09e8b26de437496e16b97f9734e5854a5e0f9f7a6a3d2b63d7c4a47e570bb0d21984b24611903fa5707df154e73e9e0ad7c91b51728dc969dd029868c13db568f3e6829c7679e7354d2b7cb9d2d05109a4444a5292b65ac43fdc778a
sign_hex:
ffcdda67026a606e0a0a8933ec25b1d991a211e8ec8d07fd62d0dfe458ab57330f9fb14df6822af703a398df6ebab475ede8456864dd9ae4cf09eb1113c701a725a627d81b70025e32472df2ce8aff1916aa2b13f27c5b811a77f1c5eaffa726
在服务器上,我重建了签名和public密钥:
$signature = hex2bin($sign_hex);
$public_key = (
"-----BEGIN PUBLIC KEY-----\n" .
chunk_split(base64_encode(hex2bin($pk_hex)), 64, "\n") .
"-----END PUBLIC KEY-----"
);
然后我使用openssl_verify()
来验证签名:
openssl_verify('hello', $signature, $public_key, OPENSSL_ALGO_SHA512);
但我得到的结果是 -1
,对于 P-384
和 P-256
。
如果我尝试使用 RSASSA-PKCS1-v1_5
执行相同的操作,则验证通过。
我做错了什么?
难道是我创建的public密钥格式不正确?如果是这样,我该怎么做才能正确格式化它?或者可能是其他原因?
openssl_error_string()
returns:
error:0909006C:PEM routines:get_name:no start line
openssl_get_curve_names()
returns:
secp112r1, secp112r2, secp128r1, secp128r2, secp160k1, secp160r1, secp160r2, secp192k1, secp224k1, secp224r1, secp256k1, secp384r1, secp521r1, prime192v1, prime192v2, prime192v3, prime239v1, prime239v2, prime239v3, prime256v1, sect113r1, sect113r2, sect131r1, sect131r2, sect163k1, sect163r1, sect163r2, sect193r1, sect193r2, sect233k1, sect233r1, sect239k1, sect283k1, sect283r1, sect409k1, sect409r1, sect571k1, sect571r1, c2pnb163v1, c2pnb163v2, c2pnb163v3, c2pnb176v1, c2tnb191v1, c2tnb191v2, c2tnb191v3, c2pnb208w1, c2tnb239v1, c2tnb239v2, c2tnb239v3, c2pnb272w1, c2pnb304w1, c2tnb359v1, c2pnb368w1, c2tnb431r1, wap-wsg-idm-ecid-wtls1, wap-wsg-idm-ecid-wtls3, wap-wsg-idm-ecid-wtls4, wap-wsg-idm-ecid-wtls5, wap-wsg-idm-ecid-wtls6, wap-wsg-idm-ecid-wtls7, wap-wsg-idm-ecid-wtls8, wap-wsg-idm-ecid-wtls9, wap-wsg-idm-ecid-wtls10, wap-wsg-idm-ecid-wtls11, wap-wsg-idm-ecid-wtls12, Oakley-EC2N-3, Oakley-EC2N-4, brainpoolP160r1, brainpoolP160t1, brainpoolP192r1, brainpoolP192t1, brainpoolP224r1, brainpoolP224t1, brainpoolP256r1, brainpoolP256t1, brainpoolP320r1, brainpoolP320t1, brainpoolP384r1, brainpoolP384t1, brainpoolP512r1, brainpoolP512t1, SM2
解决方案
作为答案的详细信息,问题在于 SubtleCrypto
使用 IEEE P1363 格式对签名进行编码,而 OpenSSL 期望它采用 ASN.1/DER 格式。
我写了下面的函数来在 P1363 和 ASN.1 之间进行转换:
/**
* Converts an IEEE P1363 signature into ASN.1/DER.
*
* @param string $p1363 Binary IEEE P1363 signature.
*/
function p1363_to_asn1(string $p1363): string {
// P1363 format: r followed by s.
// ASN.1 format: 0x30 b1 0x02 b2 r 0x02 b3 s.
//
// r and s must be prefixed with 0x00 if their first byte is > 0x7f.
//
// b1 = length of contents.
// b2 = length of r after being prefixed if necessary.
// b3 = length of s after being prefixed if necessary.
$asn1 = ''; // ASN.1 contents.
$len = 0; // Length of ASN.1 contents.
$c_len = intdiv(strlen($p1363), 2); // Length of each P1363 component.
// Separate P1363 signature into its two equally sized components.
foreach (str_split($p1363, $c_len) as $c) {
// 0x02 prefix before each component.
$asn1 .= "\x02";
if (unpack('C', $c)[1] > 0x7f) {
// Add 0x00 because first byte of component > 0x7f.
// Length of component = ($c_len + 1).
$asn1 .= pack('C', $c_len + 1) . "\x00";
$len += 2 + ($c_len + 1);
} else {
$asn1 .= pack('C', $c_len);
$len += 2 + $c_len;
}
// Append formatted component to ASN.1 contents.
$asn1 .= $c;
}
// 0x30 b1, then contents.
return "\x30" . pack('C', $len) . $asn1;
}
用法示例:
$sign_hex = bin2hex(p1363_to_asn1(hex2bin($sign_hex)));
EC 签名可以指定为两种格式:r|s (IEEE P1363) 或 ASN.1/DER。 WebCrypto 使用 r|s 格式,而 PHP 需要 ASN.1 格式。
详细解释了 ASN.1 格式 here。以 ASN.1 格式发布的签名是十六进制编码的:
$sign_hex = '3066023100ffcdda67026a606e0a0a8933ec25b1d991a211e8ec8d07fd62d0dfe458ab57330f9fb14df6822af703a398df6ebab475023100ede8456864dd9ae4cf09eb1113c701a725a627d81b70025e32472df2ce8aff1916aa2b13f27c5b811a77f1c5eaffa726';
至此,PHP验证成功
这两种格式都是在ECC上下文中定义的,而RSASSA-PKCS1-v1_5是RSA上下文中的签名方案,使用统一的签名格式,所以一般不会出现相应的问题。
编辑: ASN.1格式详解here:
0x30 b1 0x02 b2 (r) 0x02 b3 (s)
将发布的签名结果等分在r
:
ffcdda67026a606e0a0a8933ec25b1d991a211e8ec8d07fd62d0dfe458ab57330f9fb14df6822af703a398df6ebab475
和s
:
ede8456864dd9ae4cf09eb1113c701a725a627d81b70025e32472df2ce8aff1916aa2b13f27c5b811a77f1c5eaffa726
由于在这两种情况下前导字节都大于 0x7f
,因此在这两种情况下都必须添加前导 0x00
。
b2
和b3
分别表示r
和s
的长度,都是0x31
。 b1
表示后续数据的长度为0x66
,最终得到:
3066 0231 00ffcdda67026a606e0a0a8933ec25b1d991a211e8ec8d07fd62d0dfe458ab57330f9fb14df6822af703a398df6ebab475 0231 00ede8456864dd9ae4cf09eb1113c701a725a627d81b70025e32472df2ce8aff1916aa2b13f27c5b811a77f1c5eaffa726