NodeJS Crypto не может проверить подпись, созданную с помощью Web Crypto API
У меня проблемы с проверкой подписей, созданных API Web Crypto.
Вот код, который я использую для генерации ключей RSA в браузере:
let keys;
const generateKeys = async () => {
const options = {
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-256' },
};
keys = await window.crypto.subtle.generateKey(
options,
false, // non-exportable (public key still exportable)
['sign', 'verify'],
);
};
И экспортировать открытый ключ:
const exportPublicKey = async () => {
const publicKey = await window.crypto.subtle.exportKey('spki', keys.publicKey);
let body = window.btoa(String.fromCharCode(...new Uint8Array(publicKey)));
body = body.match(/.{1,64}/g).join('\n');
return `-----BEGIN PUBLIC KEY-----\n${body}\n-----END PUBLIC KEY-----`;
// Output:
//
// -----BEGIN PUBLIC KEY-----
// MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx7J3SUG4sq/HSGIaGZWY
// 8b26cfEpVFYHoDUDUORIJzA/fLE9aj+uOKpGUTSfW69rMm7DAOLDz05KaEJJSI5+
// YbDPr2S82A2ByHHQt+Vu168sGz4noXTTSX2HIdVutaR/IJ0a5pNOa1vRR4MUW/ZO
// YaRir3yC5YXgcFLwwQaifNZ3lZ7WndbYEjTGOcieQQ81IUP2221PZCJI52S95nYm
// VfslsLiPhOFH7XhGSqelGYDi0cKyl0p6dKvYxFswfKKLTuWnu2BEFLjVq4S5Y9Ob
// SGm0KL/8g7pAqjac2sMzzhHtxZ+7k8tynzAf4slJJhHMm5U4DcSelTe5zOkprCJg
// muyv0H1Acb3tfXsBwfURjiE0cvSMhfum5I5epF+f139tsr1zNF24F2WgvEZZbXcG
// g1LveGCJ/0BY0pzE71DU2SYiUhl+HGDv2u32vJO80jCDf2lu7izEt544a+XE+2X0
// zVpwjNQGa2Nd4ApGosa1fbcS5MsEdbyrjMf80SAmOeb9g3y5Zt2MY7M0Njxbvmmd
// mF20PkklpH0L01lhg2AGma4o4ojolYHzDoM5a531xTw1fZIdgbSTowz0SlAHAKD3
// c2KCCsKlBbFcqy4q7yNX63SqmI3sNA3kTH9CQJdBloRvV103Le9C0iY8CAWQmow5
// N/sDJUabgOMqe9yopSjb7LUCAwEAAQ==
// -----END PUBLIC KEY-----
};
Чтобы подписать сообщение:
const generateHash = async (message) => {
const encoder = new TextEncoder();
const buffer = encoder.encode(message);
const digest = await window.crypto.subtle.digest('SHA-256', buffer);
return digest;
};
const signMessage = async (message) => {
const { privateKey } = keys;
const digest = await generateHash(message);
const signature = await window.crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, digest);
return signature;
};
Чтобы проверить сообщение в браузере:
const verifyMessage = async (signature, message) => {
const { publicKey } = keys;
const digest = await generateHash(message);
const result = await window.crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signature, digest);
return result;
};
Когда ключи созданы, открытый ключ экспортируется и отправляется на сервер. Потом:
const message = 'test';
const signature = await signMessage(message);
await verifyMessage(signature, message); // true
sendToServer(message, bufferToHex(signature));
Поскольку подпись является ArrayBuffer, я конвертирую ее в шестнадцатеричный код со следующим кодом:
const bufferToHex = input => [...new Uint8Array(input)]
.map(v => v.toString(16).padStart(2, '0')).join('');
На сервере (NodeJS 8.11.0):
const publicKey = getPublicKey(userId);
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(message, 'utf-8');
const sigBuf = Buffer.from(signature, 'hex');
verifier.verify(publicKey, sigBuf); // false
Я преследовал эту проблему в течение многих дней и просто не могу понять это. Я пробовал оба RSA-SHA256
а также sha256WithRSAEncryption
для проверки безрезультатно. Кроме того, никаких ошибок не выбрасывается. Будем чрезвычайно признательны любой помощи!
4 ответа
Поэтому я не до конца понимаю, почему это так, но для решения этой проблемы мне нужно было преобразовать хеш SHA из ArrayBuffer в шестнадцатеричную строку, а затем снова прочитать в буфер массива с помощью TextEncoder.
const generateHash = async (message) => {
const encoder = new TextEncoder();
const buffer = encoder.encode(message);
const digest = await window.crypto.subtle.digest('SHA-256', buffer);
// Convert to hex string
return [...new Uint8Array(digest)]
.map(v => v.toString(16).padStart(2, '0')).join('');;
};
Тогда при подписании:
const signMessage = async (message) => {
const encoder = new TextEncoder();
const { privateKey } = keys;
const digest = await generateHash(message);
const signature = await window.crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, encoder.encode(digest));
return signature;
};
Подпись больше не проверяется на клиенте, но проверяется на узле. ♂️
Может быть полезно отметить, что и , и хешируют сообщение перед подписанием, поэтому отдельное хеширование не требуется.
Предполагая, что вы смогли правильно создать и загрузить свои ключи, ваш код должен выглядеть следующим образом.
Браузер
const message = 'hello'
const encoder = new TextEncoder()
const algorithmParameters = { name: 'RSASSA-PKCS1-v1_5' }
const signatureBytes = await window.crypto.subtle.sign(
algorithmParameters,
privateKey,
encoder.encode(message)
)
const base64Signature = window.btoa(
String.fromCharCode.apply(null, new Uint8Array(signatureBytes))
)
console.log(base64Signature)
// TiJZTTihhUYAIlOm2PpnvJa/+15WOX2U0iKJ2LXsLecvohhRIWnwFfdHy4ci10mcv/UQgf2+bFf9lfFZUlPPdzckBNfXIqAjafM8XquJiw/t1v+pEGtJpaGASlzuWuL37gp3k8ux3l6zBKKbBVPPASkHVhz37uY1AXeMblfRbFE=
Узел
Эта реализация использует
crypto
, но вы можете использовать
crypto.subtle
чтобы быть более похожим на синтаксис javascript браузера
const crypto = require('crypto')
const message = 'hello'
const base64Signature = 'TiJZTTihhUYAIlOm2PpnvJa/+15WOX2U0iKJ2LXsLecvohhRIWnwFfdHy4ci10mcv/UQgf2+bFf9lfFZUlPPdzckBNfXIqAjafM8XquJiw/t1v+pEGtJpaGASlzuWuL37gp3k8ux3l6zBKKbBVPPASkHVhz37uY1AXeMblfRbFE='
const hashingAlgorithm = 'rsa-sha256'
const doesVerify = crypto.verify(
hashingAlgorithm,
Buffer.from(message),
{ key: publicKey },
Buffer.from(base64Signature, 'base64')
);
console.log(doesVerify)
// true
Проблема в том, что вы подписываете хеш-код своего ввода, тогда как на самом деле вы должны подписывать хеш-код своего ввода. SubtleCrypto внутренне хеширует ввод. Вам не нужно предоставлять хешированный ввод. Поскольку вы предоставили хешированный ввод в качестве аргумента, SubtleCrypto снова хэшировал его, а затем подписал, что привело к несоответствию подписей.
Не прямой ответ, но может быть проще использовать это: https://www.npmjs.com/package/@peculiar/webcrypto чтобы ваш код на клиенте и сервере был согласованным, одновременно решая эту проблему.