Node.js 和 WebCrypto 之间的 ECDSA 签名似乎不兼容?
ECDSA signatures between Node.js and WebCrypto appear to be incompatible?
我在 Node.js 中使用以下示例进行签名 + 验证:https://github.com/nodejs/node-v0.x-archive/issues/6904。在 Node.js 中验证成功,但在 WebCrypto 中验证失败。同样,使用 WebCrypto 签名的消息无法在 Node.js.
中验证
这是我用来验证使用 WebCrypto 从 Node.js 脚本生成的签名的代码 - https://jsfiddle.net/aj49e8sj/。在 Chrome 54.0.2840.27 和 Firefox 48.0.2
中测试
// From https://github.com/nodejs/node-v0.x-archive/issues/6904
var keys = {
priv: '-----BEGIN EC PRIVATE KEY-----\n' +
'MHcCAQEEIF+jnWY1D5kbVYDNvxxo/Y+ku2uJPDwS0r/VuPZQrjjVoAoGCCqGSM49\n' +
'AwEHoUQDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNhB8i3mXyIMq704m2m52FdfKZ2\n' +
'pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' +
'-----END EC PRIVATE KEY-----\n',
pub: '-----BEGIN PUBLIC KEY-----\n' +
'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNh\n' +
'B8i3mXyIMq704m2m52FdfKZ2pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' +
'-----END PUBLIC KEY-----\n'
};
var message = (new TextEncoder('UTF-8')).encode('hello');
// Algorithm used in Node.js script is ecdsa-with-SHA1, key generated with prime256v1
var algorithm = {
name: 'ECDSA',
namedCurve: 'P-256',
hash: {
name: 'SHA-1'
}
};
// Signature from obtained via above Node.js script
var sig64 = 'MEUCIQDkAtiomagyHFi7dNfxMrzx/U0Gk/ZhmwCqaL3TimvlswIgPgeDqgZNqfR5/FZZASYsczUAhGSXjuycLhWnvk20qKc=';
// Decode base64 string into ArrayBuffer
var b64Decode = (str) => Uint8Array.from(atob(str), x => x.charCodeAt(0));
// Get base64 string from public key
const key64 = keys.pub.split('\n')
.filter(x => x.length > 0 && !x.startsWith('-----'))
.join('');
// Convert to buffers
var sig = b64Decode(sig64);
var keySpki = b64Decode(key64);
// Import and verify
// Want 'Verification result: true' but will get 'false'
var importKey = crypto.subtle.importKey('spki', keySpki, algorithm, true, ['verify'])
.then(key => crypto.subtle.verify(algorithm, key, sig, message))
.then(result => console.log('Verification result: ' + result));
使用 SHA-256 而不是 SHA-1 的类似问题的相关问题:Generating ECDSA signature with Node.js/crypto
我检查过的东西:
- 我解码了 Node.js 密钥并验证它们与通过 WebCrypto 生成的密钥具有相同的 OID。这告诉我我正在使用正确的曲线。
- SHA-1 被明确标识为要在两个位置使用的哈希值。
- ECDSA 在 Node.js 和 WebCrypto 中都明确标识。
我如何才能成功验证从 Node.js 收到的签名,反之亦然 - 验证从 WebCrypto 生成的 Node.js 中的签名?还是标准的实现方式略有不同,导致它们不兼容?
编辑:
- WebCrypto 签名(64 字节):uTaUWTfF+AjN3aPj0b5Z2d1HybUEpV/phv/P9RtfKaGXtcYnbgfO43IRg46rznG3/WnWwJ2sV6mPOEnEPR0vWw==
- Node.js 签名(71 字节):MEUCIQDkAtiomagyHFi7dNfxMrzx/U0Gk/ZhmwCqaL3TimvlswIgPgeDqgZNqfR5/FZZASYsczUAhGSXjuycLhWnvk20qKc=
已验证 Node.js 签名是 DER 编码的,而 WebCrypto 签名不是。
我没有使用过这两个库中的任何一个,我不能肯定地说,但一种可能是它们没有为签名使用相同的编码类型。 DSA/ECDSA 有两种主要格式,IEEE P1363(Windows 使用)和 DER(OpenSSL 使用)。
"Windows" 格式要有预设大小(由 DSA 的 Q 和 ECDSA 的 P 决定(Windows 不支持 Char-2,但如果支持的话可能会为 Char-2 ECDSA 的 M))。然后 r
和 s
都用 0
左填充,直到它们满足该长度。
在 r = 0x305
和 s = 0x810522
的太小而不是合法的例子中,sizeof(Q) 是 3 个字节:
// r
000305
// s
810522
对于 "OpenSSL" 格式,它根据 DER 规则编码为 SEQUENCE(INTEGER(r), INTEGER(s)),看起来像
// SEQUENCE
30
// (length of payload)
0A
// INTEGER(r)
02
// (length of payload)
02
// note the leading 0x00 is omitted
0305
// INTEGER(s)
02
// (length of payload)
04
// Since INTEGER is a signed type, but this represented a positive number,
// a 0x00 has to be inserted to keep the sign bit clear.
00810522
或者,简洁地:
- Windows:
000305810522
- OpenSSL:
300A02020305020400810522
"Windows" 格式总是偶数,总是相同的长度。 "OpenSSL" 格式通常大约大 6 个字节,但可以在中间增加或减少一个字节;所以它有时是偶数,有时是奇数。
Base64 解码您的 sig64
值表明它正在使用 DER 编码。使用 WebCrypto 生成几个签名;如果有任何不是以 0x30
开头,那么你就有了 IEEE/DER 问题。
经过几个小时终于找到了零依赖的解决方案!!
在浏览器中:
// Tip: Copy & Paste in the console for test.
// Text to sign:
var source = 'test';
// Auxs
function length(hex) {
return ('00' + (hex.length / 2).toString(16)).slice(-2).toString();
}
function pubKeyToPEM(key) {
var pem = '-----BEGIN PUBLIC KEY-----\n',
keydata = '',
bytes = new Uint8Array( key );
for (var i = 0; i < bytes.byteLength; i++) {
keydata += String.fromCharCode( bytes[ i ] );
}
keydata = window.btoa(keydata);
while(keydata.length > 0) {
pem += keydata.substring(0, 64) + '\n';
keydata = keydata.substring(64);
}
pem = pem + "-----END PUBLIC KEY-----";
return pem;
}
// Generate new keypair.
window.crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["sign", "verify"])
.then(function(keypair) {
// Encode as UTF-8
var enc = new TextEncoder('UTF-8'),
digest = enc.encode(source);
// Sign with subtle
window.crypto.subtle.sign({ name: "ECDSA", hash: {name: "SHA-1"} }, keypair.privateKey, digest)
.then(function(signature) {
signature = new Uint8Array(signature);
// Extract r & s and format it in ASN1 format.
var signHex = Array.prototype.map.call(signature, function(x) { return ('00' + x.toString(16)).slice(-2); }).join(''),
r = signHex.substring(0, 96),
s = signHex.substring(96),
rPre = true,
sPre = true;
while(r.indexOf('00') === 0) {
r = r.substring(2);
rPre = false;
}
if (rPre && parseInt(r.substring(0, 2), 16) > 127) {
r = '00' + r;
}
while(s.indexOf('00') === 0) {
s = s.substring(2);
sPre = false;
}
if(sPre && parseInt(s.substring(0, 2), 16) > 127) {
s = '00' + s;
}
var payload = '02' + length(r) + r +
'02' + length(s) + s,
der = '30' + length(payload) + payload;
// Export public key un PEM format (needed by node)
window.crypto.subtle.exportKey('spki', keypair.publicKey)
.then(function(key) {
var pubKey = pubKeyToPEM(key);
console.log('This is pubKey -> ', pubKey);
console.log('This is signature -> ', der);
});
// For test, we verify the signature, nothing, anecdotal.
window.crypto.subtle.verify({ name: "ECDSA", hash: {name: "SHA-1"} }, keypair.publicKey, signature, digest)
.then(console.log);
});
});
在节点中:
const crypto = require('crypto');
// ----------------------------------------------------------------------------
// Paste from browser!
var puKeyPem = '-----BEGIN PUBLIC KEY-----\n' +
'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmDubwJuORpMMoMnvv59W8tU8PxPChh75\n' +
'vjlfVB2+tPY5KDy1I0ohz2US+2K1T/ROcDCSRAjyONRzzwVBm9S6bqbk3KuaT2KG\n' +
'ikoe0KLfTeQtdEUyq8J0aEOKRXoCJLZq\n' +
'-----END PUBLIC KEY-----';
var hexSign = '306402305df22aa5f4e7200b7c264c891cd3a8c5b4622c25872020832d5bb3d251773592020249a46a8349754dc58c47c4cbb7c9023053b929a98f5c8cccf2c1a4746d82fc751e044b1f76dffdf9ef73f73bee1499c5e20aadddda41e3373760b8b0f3c1bbb2';
// ----------------------------------------------------------------------------
var verifier = crypto.createVerify('sha1'),
digest = 'test';
verifier.update(digest);
verifier.end();
console.log(verifier.verify(puKeyPem, hexSign, 'hex'));
// ----------------------------------------------------------------------------
现在您可以生成兼容的 (nodejs vs webcrypto) 密钥和签名而无需修改它们。下面的例子是针对RSA的,不过ECDSA应该很相似——本质在types/formats/encodings.
生成密钥对(nodejs):
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'der'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'der'
}
});
console.log('PRIVATE', Buffer.from(privateKey).toString('base64'));
console.log('PUBLIC', Buffer.from(publicKey).toString('base64'));
签名消息(nodejs):
const signature = crypto.sign(
'sha256',
Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8'),
{
key: crypto.createPrivateKey({
key: Buffer.from('...base64 encoded private key', 'base64'),
format: 'der',
type: 'pkcs8'
}),
padding: crypto.constants.RSA_PKCS1_PADDING,
dsaEncoding: 'ieee-p1363'
}
);
console.log('SIGNATURE', signature.toString('base64'));
验证消息(webcrypto)- 你必须改变 vanilla js 中的 Buffer 函数:
(async () => console.log(await crypto.subtle.verify(
{ name: 'RSASSA-PKCS1-v1_5' },
await crypto.subtle.importKey(
'spki',
Buffer.from('...base64 encoded public key', 'base64'),
{ name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'} },
false,
[ 'verify' ]
),
Buffer.from('...base64 encoded signature', 'base64'),
Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8')
)))();
我在 Node.js 中使用以下示例进行签名 + 验证:https://github.com/nodejs/node-v0.x-archive/issues/6904。在 Node.js 中验证成功,但在 WebCrypto 中验证失败。同样,使用 WebCrypto 签名的消息无法在 Node.js.
中验证这是我用来验证使用 WebCrypto 从 Node.js 脚本生成的签名的代码 - https://jsfiddle.net/aj49e8sj/。在 Chrome 54.0.2840.27 和 Firefox 48.0.2
中测试// From https://github.com/nodejs/node-v0.x-archive/issues/6904
var keys = {
priv: '-----BEGIN EC PRIVATE KEY-----\n' +
'MHcCAQEEIF+jnWY1D5kbVYDNvxxo/Y+ku2uJPDwS0r/VuPZQrjjVoAoGCCqGSM49\n' +
'AwEHoUQDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNhB8i3mXyIMq704m2m52FdfKZ2\n' +
'pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' +
'-----END EC PRIVATE KEY-----\n',
pub: '-----BEGIN PUBLIC KEY-----\n' +
'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEurOxfSxmqIRYzJVagdZfMMSjRNNh\n' +
'B8i3mXyIMq704m2m52FdfKZ2pQhByd5eyj3lgZ7m7jbchtdgyOF8Io/1ng==\n' +
'-----END PUBLIC KEY-----\n'
};
var message = (new TextEncoder('UTF-8')).encode('hello');
// Algorithm used in Node.js script is ecdsa-with-SHA1, key generated with prime256v1
var algorithm = {
name: 'ECDSA',
namedCurve: 'P-256',
hash: {
name: 'SHA-1'
}
};
// Signature from obtained via above Node.js script
var sig64 = 'MEUCIQDkAtiomagyHFi7dNfxMrzx/U0Gk/ZhmwCqaL3TimvlswIgPgeDqgZNqfR5/FZZASYsczUAhGSXjuycLhWnvk20qKc=';
// Decode base64 string into ArrayBuffer
var b64Decode = (str) => Uint8Array.from(atob(str), x => x.charCodeAt(0));
// Get base64 string from public key
const key64 = keys.pub.split('\n')
.filter(x => x.length > 0 && !x.startsWith('-----'))
.join('');
// Convert to buffers
var sig = b64Decode(sig64);
var keySpki = b64Decode(key64);
// Import and verify
// Want 'Verification result: true' but will get 'false'
var importKey = crypto.subtle.importKey('spki', keySpki, algorithm, true, ['verify'])
.then(key => crypto.subtle.verify(algorithm, key, sig, message))
.then(result => console.log('Verification result: ' + result));
使用 SHA-256 而不是 SHA-1 的类似问题的相关问题:Generating ECDSA signature with Node.js/crypto
我检查过的东西:
- 我解码了 Node.js 密钥并验证它们与通过 WebCrypto 生成的密钥具有相同的 OID。这告诉我我正在使用正确的曲线。
- SHA-1 被明确标识为要在两个位置使用的哈希值。
- ECDSA 在 Node.js 和 WebCrypto 中都明确标识。
我如何才能成功验证从 Node.js 收到的签名,反之亦然 - 验证从 WebCrypto 生成的 Node.js 中的签名?还是标准的实现方式略有不同,导致它们不兼容?
编辑:
- WebCrypto 签名(64 字节):uTaUWTfF+AjN3aPj0b5Z2d1HybUEpV/phv/P9RtfKaGXtcYnbgfO43IRg46rznG3/WnWwJ2sV6mPOEnEPR0vWw==
- Node.js 签名(71 字节):MEUCIQDkAtiomagyHFi7dNfxMrzx/U0Gk/ZhmwCqaL3TimvlswIgPgeDqgZNqfR5/FZZASYsczUAhGSXjuycLhWnvk20qKc=
已验证 Node.js 签名是 DER 编码的,而 WebCrypto 签名不是。
我没有使用过这两个库中的任何一个,我不能肯定地说,但一种可能是它们没有为签名使用相同的编码类型。 DSA/ECDSA 有两种主要格式,IEEE P1363(Windows 使用)和 DER(OpenSSL 使用)。
"Windows" 格式要有预设大小(由 DSA 的 Q 和 ECDSA 的 P 决定(Windows 不支持 Char-2,但如果支持的话可能会为 Char-2 ECDSA 的 M))。然后 r
和 s
都用 0
左填充,直到它们满足该长度。
在 r = 0x305
和 s = 0x810522
的太小而不是合法的例子中,sizeof(Q) 是 3 个字节:
// r
000305
// s
810522
对于 "OpenSSL" 格式,它根据 DER 规则编码为 SEQUENCE(INTEGER(r), INTEGER(s)),看起来像
// SEQUENCE
30
// (length of payload)
0A
// INTEGER(r)
02
// (length of payload)
02
// note the leading 0x00 is omitted
0305
// INTEGER(s)
02
// (length of payload)
04
// Since INTEGER is a signed type, but this represented a positive number,
// a 0x00 has to be inserted to keep the sign bit clear.
00810522
或者,简洁地:
- Windows:
000305810522
- OpenSSL:
300A02020305020400810522
"Windows" 格式总是偶数,总是相同的长度。 "OpenSSL" 格式通常大约大 6 个字节,但可以在中间增加或减少一个字节;所以它有时是偶数,有时是奇数。
Base64 解码您的 sig64
值表明它正在使用 DER 编码。使用 WebCrypto 生成几个签名;如果有任何不是以 0x30
开头,那么你就有了 IEEE/DER 问题。
经过几个小时终于找到了零依赖的解决方案!!
在浏览器中:
// Tip: Copy & Paste in the console for test.
// Text to sign:
var source = 'test';
// Auxs
function length(hex) {
return ('00' + (hex.length / 2).toString(16)).slice(-2).toString();
}
function pubKeyToPEM(key) {
var pem = '-----BEGIN PUBLIC KEY-----\n',
keydata = '',
bytes = new Uint8Array( key );
for (var i = 0; i < bytes.byteLength; i++) {
keydata += String.fromCharCode( bytes[ i ] );
}
keydata = window.btoa(keydata);
while(keydata.length > 0) {
pem += keydata.substring(0, 64) + '\n';
keydata = keydata.substring(64);
}
pem = pem + "-----END PUBLIC KEY-----";
return pem;
}
// Generate new keypair.
window.crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["sign", "verify"])
.then(function(keypair) {
// Encode as UTF-8
var enc = new TextEncoder('UTF-8'),
digest = enc.encode(source);
// Sign with subtle
window.crypto.subtle.sign({ name: "ECDSA", hash: {name: "SHA-1"} }, keypair.privateKey, digest)
.then(function(signature) {
signature = new Uint8Array(signature);
// Extract r & s and format it in ASN1 format.
var signHex = Array.prototype.map.call(signature, function(x) { return ('00' + x.toString(16)).slice(-2); }).join(''),
r = signHex.substring(0, 96),
s = signHex.substring(96),
rPre = true,
sPre = true;
while(r.indexOf('00') === 0) {
r = r.substring(2);
rPre = false;
}
if (rPre && parseInt(r.substring(0, 2), 16) > 127) {
r = '00' + r;
}
while(s.indexOf('00') === 0) {
s = s.substring(2);
sPre = false;
}
if(sPre && parseInt(s.substring(0, 2), 16) > 127) {
s = '00' + s;
}
var payload = '02' + length(r) + r +
'02' + length(s) + s,
der = '30' + length(payload) + payload;
// Export public key un PEM format (needed by node)
window.crypto.subtle.exportKey('spki', keypair.publicKey)
.then(function(key) {
var pubKey = pubKeyToPEM(key);
console.log('This is pubKey -> ', pubKey);
console.log('This is signature -> ', der);
});
// For test, we verify the signature, nothing, anecdotal.
window.crypto.subtle.verify({ name: "ECDSA", hash: {name: "SHA-1"} }, keypair.publicKey, signature, digest)
.then(console.log);
});
});
在节点中:
const crypto = require('crypto');
// ----------------------------------------------------------------------------
// Paste from browser!
var puKeyPem = '-----BEGIN PUBLIC KEY-----\n' +
'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmDubwJuORpMMoMnvv59W8tU8PxPChh75\n' +
'vjlfVB2+tPY5KDy1I0ohz2US+2K1T/ROcDCSRAjyONRzzwVBm9S6bqbk3KuaT2KG\n' +
'ikoe0KLfTeQtdEUyq8J0aEOKRXoCJLZq\n' +
'-----END PUBLIC KEY-----';
var hexSign = '306402305df22aa5f4e7200b7c264c891cd3a8c5b4622c25872020832d5bb3d251773592020249a46a8349754dc58c47c4cbb7c9023053b929a98f5c8cccf2c1a4746d82fc751e044b1f76dffdf9ef73f73bee1499c5e20aadddda41e3373760b8b0f3c1bbb2';
// ----------------------------------------------------------------------------
var verifier = crypto.createVerify('sha1'),
digest = 'test';
verifier.update(digest);
verifier.end();
console.log(verifier.verify(puKeyPem, hexSign, 'hex'));
// ----------------------------------------------------------------------------
现在您可以生成兼容的 (nodejs vs webcrypto) 密钥和签名而无需修改它们。下面的例子是针对RSA的,不过ECDSA应该很相似——本质在types/formats/encodings.
生成密钥对(nodejs):
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'der'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'der'
}
});
console.log('PRIVATE', Buffer.from(privateKey).toString('base64'));
console.log('PUBLIC', Buffer.from(publicKey).toString('base64'));
签名消息(nodejs):
const signature = crypto.sign(
'sha256',
Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8'),
{
key: crypto.createPrivateKey({
key: Buffer.from('...base64 encoded private key', 'base64'),
format: 'der',
type: 'pkcs8'
}),
padding: crypto.constants.RSA_PKCS1_PADDING,
dsaEncoding: 'ieee-p1363'
}
);
console.log('SIGNATURE', signature.toString('base64'));
验证消息(webcrypto)- 你必须改变 vanilla js 中的 Buffer 函数:
(async () => console.log(await crypto.subtle.verify(
{ name: 'RSASSA-PKCS1-v1_5' },
await crypto.subtle.importKey(
'spki',
Buffer.from('...base64 encoded public key', 'base64'),
{ name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'} },
false,
[ 'verify' ]
),
Buffer.from('...base64 encoded signature', 'base64'),
Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8')
)))();