0%

RSA签名:Node.js与C++协同

  1. 前言
    1. 平台
    2. 密钥对文件格式
  2. 跨端验证
    1. 签名:Node.js
    2. 验证:C++
  3. 使用各种工具实现的签名和验证
    1. Node.js: Node-RSA
    2. Node.js: crypto
    3. C++: CryptoPP

前言

使用RSA密钥对不仅可以用来加密数据,还可以用来进行签名。前者使用公钥进行加密,私钥进行解密;后者使用私钥进行签名,用公钥验证签名。

平台

  • Node.js - v12.14.0
  • VS2019 - v142 && v100
  • CryptoPP 8.5.0

密钥对文件格式

公钥:test_pub.pem

1
2
3
-----BEGIN PUBLIC KEY-----
......
-----END PUBLIC KEY-----

私钥:test_prv.pem

1
2
3
-----BEGIN PRIVATE KEY-----
......
-----END PRIVATE KEY-----

跨端验证

原本想使用RSASSA-PSS算法(RSA Signature Scheme with Appendix - Probabilistic Signature Scheme)进行签名和验证,这也是crypto++的PSS模式默认使用的签名算法;但是Node.js似乎不支持该算法,无论是node-rsa还是crypto,其自身只能调用PSS算法,关于RSASSA-PSS简单查了一下没找到现有的解决方案,在这一方面就先不折腾太多,先退而求其次使用PKCS1模式。

签名:Node.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

const sign_crypto = (str = '') => {
const prvKey = crypto.createPrivateKey({
key: fs.readFileSync(path.join(__dirname, 'rsa', 'test_prv.pem')).toString('utf8'),
format: 'pem',
type: 'pkcs8'
});
const result = crypto.sign('RSA-SHA256', new TextEncoder().encode(str), {
key: prvKey,
padding: crypto.constants.RSA_PKCS1_PADDING
}).toString('base64');
return result;
}

const msg = '123456';
console.log(sign_crypto(msg));

验证:C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <cryptopp/rsa.h>
#include <cryptopp/base64.h>
#include <cryptopp/pssr.h>
#include <iostream>

bool RSAVerify(const std::string& msg, const std::string& base64SignStr, const std::string& base64PubKeyStr)
{
using namespace CryptoPP;
try
{
StringSource pubKeySrc(base64PubKeyStr, true, new Base64Decoder);

std::string signStrAscii;
StringSource s1(base64SignStr, true, new Base64Decoder(new StringSink(signStrAscii)));

RSASS<PKCS1v15, SHA256>::Verifier verifier(pubKeySrc);
StringSource s2(msg + signStrAscii, true,
new SignatureVerificationFilter(verifier, nullptr,
SignatureVerificationFilter::THROW_EXCEPTION |
SignatureVerificationFilter::PUT_MESSAGE
)
);

return true;
}
catch (const std::exception& e)
{
std::cerr << e.what() << std::endl;
return false;
}
}

int main()
{
std::string msg, signStr;
msg = "123456";
signStr = "...";

if (RSAVerify(msg, signStr, pub))
{
std::cout << "验证成功" << std::endl;
}
return 0;
}

使用各种工具实现的签名和验证

由于不同工具和平台的算法对于PSS模式的签名实现各不相同(应该只是某些细节参数设置不同),只能保证在其自身的工具集中签名和验证相匹配,一旦使用A签名B验证或者B签名A验证等方式,就很难做到全都能验证通过。

这里先做一个记录,关于Node-RSA, node.js/crypto, crypto++…自身实现的PSS模式签名和验证的方法。

Node.js: Node-RSA

使用PSS(Probabilistic Signature Scheme)签名模式,签名函数为sha256

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs');
const NodeRSA = require('node-rsa');
const path = require('path');

/// 签名
const sign_node_rsa = (str = '') => {
const prvKey = new NodeRSA(fs.readFileSync(path.join(__dirname, 'rsa', 'test_prv.pem')), 'pkcs8-private-pem', { signingScheme: 'pss-sha256' });
return prvKey.sign(str, 'base64', 'utf8');
}

/// 验证
const verify_node_rsa = (str = '', sign = '') => {
const pubKey = new NodeRSA(fs.readFileSync(path.join(__dirname, 'rsa', 'test_pub.pem')), 'pkcs8-public-pem', { signingScheme: 'pss-sha256' });
return pubKey.verify(str, sign, 'utf8', 'base64');
}

const str = '123456';
const sign = sign_node_rsa(str);
console.log(verify_node_rsa(str, sign)); // true

Node.js: crypto

使用Node.js自带的crypto模块进行签名(Node-RSA的底层也是crypto),签名模式是PSS,签名算法是sha256。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

/// 签名
const sign_crypto = (str = '') => {
const prvKey = crypto.createPrivateKey({
key: fs.readFileSync(path.join(__dirname, 'rsa', 'test_prv.pem')).toString('utf8'),
format: 'pem',
type: 'pkcs8'
});
const result = crypto.sign('RSA-SHA256', new TextEncoder().encode(str), {
key: prvKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO
}).toString('base64');
return result;
}

/// 验证
const verify_crypto = (str = '', sign = '') => {
const pubKey = crypto.createPublicKey({
key: fs.readFileSync(path.join(__dirname, 'rsa', 'test_pub.pem')).toString('utf8'),
format: 'pem',
type: 'spki'
});
const result = crypto.verify('RSA-SHA256', new TextEncoder().encode(str), {
key: pubKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO
}, Buffer.from(sign, 'base64'));
return result;
}

const str = '123456';
let sign = sign_crypto(str);
console.log(verify_crypto(str, sign)); // true

C++: CryptoPP

加密模式为PSS,加密算法为sha256。其中,公钥和私钥字符串不包含文件的首尾行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <cryptopp/rsa.h>
#include <cryptopp/base64.h>
#include <cryptopp/pssr.h>
#include <iostream>

bool RSASign(const std::string& msg, std::string* base64SignStr, const std::string& base64PrvKeyStr)
{
using namespace CryptoPP;
try
{
if (base64SignStr == nullptr) throw std::exception("Null output pointer");

AutoSeededRandomPool rng;
StringSource prvKeySrc(base64PrvKeyStr, true, new Base64Decoder);

RSASS<PSS, SHA256>::Signer signer(prvKeySrc);

StringSource s(msg, true,
new SignerFilter(rng, signer,
new Base64Encoder(new StringSink(*base64SignStr))
)
);

return true;
}
catch (const std::exception& e)
{
std::cerr << e.what() << std::endl;
return false;
}
}

bool RSAVerify(const std::string& msg, const std::string& base64SignStr, const std::string& base64PubKeyStr)
{
using namespace CryptoPP;
try
{
StringSource pubKeySrc(base64PubKeyStr, true, new Base64Decoder);

std::string signStrAscii;
StringSource s1(base64SignStr, true, new Base64Decoder(new StringSink(signStrAscii)));

RSASS<PSS, SHA256>::Verifier verifier(pubKeySrc);
StringSource s2(msg + signStrAscii, true,
new SignatureVerificationFilter(verifier, nullptr,
SignatureVerificationFilter::THROW_EXCEPTION |
SignatureVerificationFilter::PUT_MESSAGE
)
);

return true;
}
catch (const std::exception& e)
{
std::cerr << e.what() << std::endl;
return false;
}
}

int main(int argc, char* argv[])
{
std::string pub, prv;
ReadRSAPubKey("./test_pub.pem", &pub);
ReadRSAPrvKey("./test_prv.pem", &prv);

std::string msg, signStr;
msg = "123456";
if (RSASign(msg, &signStr, prv))
{
std::cout << signStr << std::endl;
}
if (RSAVerify(msg, signStr, pub))
{
std::cout << "验证成功" << std::endl;
}

return 0;
}