C++ 如何使用 OpenSSL 验证 Google JWT (RS256)
Posted
技术标签:
【中文标题】C++ 如何使用 OpenSSL 验证 Google JWT (RS256)【英文标题】:C++ How to validate Google JWT (RS256) using OpenSSL 【发布时间】:2021-05-10 00:10:14 【问题描述】:我正在尝试使用 OpenSSL 和 c++ 验证 JWT 令牌。
作为实验和学习的练习,请不要建议使用 3rd 方库来完成这项工作。
令牌具有通常的格式 Header.Payload.Signature
,我可以对其进行 Base64URL 解码,但我无法验证签名。
遵循 RFC 并未提及如何使用 RS256:
根据 JWS 签名输入验证 JWS 签名 ASCII(BASE64URL(UTF8(JWS 保护标头))|| '.' || BASE64URL(JWS Payload)) 以为所使用的算法定义的方式,其中 必须由“alg”(算法)的值准确表示 标头参数,必须存在。
我关注JWT: The Complete Guide to JSON Web Tokens:
接收器如何检查 RS256 签名? JWT 的接收者 然后:
获取标头和有效负载,并使用 SHA-256 对所有内容进行哈希处理 使用公钥解密签名,得到签名哈希 接收方将签名哈希与他自己根据 Header 和 Payload 计算的哈希进行比较这两个哈希值匹配吗?那么这就证明JWT确实是Authentication server创建的!
使用 Base64Url 解码标头时,我得到了一个有效的 JSON。有效载荷也是有效的 JSON:
"alg":"RS256","kid":"03b2d22c2fecf873ed19e5b8cf704afb7e2ed4be","typ":"JWT"
然后我从Google 为给定的孩子恢复了正确的证书。
我的测试代码是:
// Split fields for convenience
static std::string GTOKEN_B64URL_HEADER ("eyJhb...shortened...V1QifQ");
static std::string GTOKEN_B64URL_PAYLOAD("eyJpc...shortened...MzExfQ");
static std::string GTOKEN_B64URL_SIGN ("k7Ppq...shortened...TJCTdQ");
// From https://www.googleapis.com/oauth2/v1/certs using the specified "kid"
static const char* CERT =
"-----BEGIN CERTIFICATE-----\n"
"MIIDJjCCAg6gAwIBAgIIHdBXKdu8rS4wDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\n"
...
"MB7mbimIU22061HCjFbdlEscy26X/BXtxPpQjEwbkzJ5wy2bVu2AIIdo\n"
"-----END CERTIFICATE-----\n";
// Preparation: Get the public key from the PEM cert
//
BIO *memCert = BIO_new_mem_buf(CERT, -1);
X509* cert= PEM_read_bio_X509(memCert, nullptr, nullptr, nullptr);
if (nullptr == cert)
showOpenSSLErrors("Unable to load CERT: ");
return;
EVP_PKEY* key = X509_get_pubkey(cert);
if (nullptr == key)
showOpenSSLErrors("Unable to get pubkey from cert: ");
return;
int idKey = EVP_PKEY_id(key);
int type = EVP_PKEY_type(idKey);
if (type != EVP_PKEY_RSA && type != EVP_PKEY_RSA2)
std::cout << "Key type is not RSA" << std::endl;
return;
RSA* rsa = EVP_PKEY_get1_RSA(key);
if (nullptr == rsa)
showOpenSSLErrors("Invalid RSA: ");
return;
// 1) take the header and the payload, and hash everything with SHA-256
//
std::string whatToValidate;
computeHashSHA256(GTOKEN_B64URL_HEADER+"."+GTOKEN_B64URL_PAYLOAD, whatToValidate);
// 2) decrypt the signature using the public key ...
//
std::string signatureB64 = decodeBase64URL(GTOKEN_B64URL_SIGN);
std::string signature;
signature.resize( RSA_size(rsa) );
int len = RSA_public_decrypt(
signatureB64.size(),
(unsigned char*)signatureB64.data(),
(unsigned char*)signature.data(),
rsa, RSA_NO_PADDING);
if (len == -1)
std::cout << "Decrypt failed" << std::endl;
return;
signature.resize(len);
// 2) ... and obtain the signature hash
std::string signatureHash;
computeHashSHA256(signature, signatureHash);
if (whatToValidate.size() != signatureHash.size())
printf("Len does not match! (%d vs %d) \n", whatToValidate.size(), signatureHash.size());
return;
std::cout << "whatToValidate: " << whatToValidate << std::endl;
std::cout << "signatureHash: " << signatureHash << std::endl;
// 3) the receiver compares the signature hash with the hash that he
// calculated himself based on the Header and the Payload
if (signatureHash != whatToValidate)
printf(" comparison FAILED!!!\n");
// Extra check: Ensure SHA256 algorithm is working
//
const std::string decodedHeader(decodeBase64URL(GTOKEN_B64URL_HEADER));
std::string headerSHA256;
computeHashSHA256(decodedHeader, headerSHA256);
std::cout << "Header: " << decodedHeader << std::endl;
std::cout << "Header SHA256: " << headerSHA256 << std::endl;
std::cout << "Signature size: " << signature.size() << "(" << GTOKEN_B64URL_SIGN.size() << " base64Url)" << std::endl;
std::cout << "Validate: " << whatToValidate.size() << std::endl;
std::cout << std::endl;
这段代码的输出是:
whatToValidate: d4981a11b8d9a686e7f9919cf7d6477c5e7c0e35fcd61133ad2fdb8cb845b49a
signatureHash: e79eee72dcc4412601689f03c0c83e6958b87447172f5109bffebbc7f009c38d
comparison FAILED!!!
Header: "alg":"RS256","kid":"03b2d22c2fecf873ed19e5b8cf704afb7e2ed4be","typ":"JWT"
Header SHA256: 5b53315f0b0424c866ff364e9f7bd2f882c61e4460aa1f503c2abd1ad753426e
Signature size: 256(342 base64Url)
Validate: 64
标头 SHA256 证明 computeHashSHA256() 按预期工作。
我做错了什么?
我可以使用任何替代方法吗? (也尝试了 RSA_verify() 没有运气,因为我真的不知道如何)
编辑
JWS 签名输入的 SHA256 将是 32 个字节。 whatToValidate(SHA256 的 ASCII 表示)将是 64 个字节。签名长度为 256 个字节。
签名看起来不像原始或 ASCII 的 SHA256。
因此问题是:whatToValidate 应该是 JWS 签名输入上的 SHA256 吗?
编辑 - Base64URL 解码签名(二进制):
0x93 0xB3 0xE9 0xA8 0x40 0xBA 0x03 0xB8 0x26 0x5C 0x84 0x97 0xD0 0x66 0xA5 0xF2
0x21 0x90 0x34 0x77 0x03 0x79 0x61 0xEE 0x06 0xC4 0xCD 0x81 0x06 0x22 0x7B 0x59
0xF7 0x2B 0x13 0x5B 0xEC 0x21 0x29 0xD6 0x81 0xB5 0xE1 0x18 0x64 0xE7 0xB2 0x0E
0xE1 0xF6 0x8F 0xB5 0x39 0x98 0xF5 0x28 0x65 0xBC 0xB5 0x5D 0x02 0x0E 0x80 0x8B
0x07 0x7A 0xF0 0x14 0x57 0x6E 0xF6 0x2C 0x9D 0xEE 0x7A 0x2E 0x2D 0xA0 0x1C 0xFD
0xC6 0x45 0xBC 0xE3 0x60 0xA9 0x67 0x05 0x84 0x05 0xBA 0xDC 0x34 0xBC 0x97 0xF1
0x51 0x3E 0x30 0x73 0xEA 0x4D 0x4F 0xF1 0x33 0xE2 0x1C 0x44 0x8E 0x6F 0x3F 0x0B
0xE6 0x62 0xA8 0x9E 0xFE 0x27 0xB3 0xF3 0x41 0xFB 0x5C 0xA0 0xC1 0x06 0x6B 0x91
0x4A 0xA5 0x7C 0xB8 0x85 0xEF 0xB3 0xAE 0x28 0x1C 0xC1 0x74 0x91 0xBB 0xB8 0xF9
0xAD 0xB0 0x13 0x34 0x96 0x4C 0xBF 0x6C 0xD2 0x5A 0x55 0x0D 0x4C 0x2D 0x01 0xC7
0x8D 0xBF 0x4B 0x8E 0x9B 0x31 0xAB 0x2B 0x1B 0x9A 0x8F 0x7A 0x32 0xB5 0x91 0x52
0x7E 0xE7 0xA8 0x7F 0x49 0x3F 0xCF 0x2C 0xAA 0x9B 0xE3 0x11 0x08 0x20 0x4E 0x5D
0x68 0x2B 0x75 0xEB 0xB4 0xE7 0xDA 0x23 0xDA 0xE0 0xCD 0xF7 0xD9 0x0D 0x42 0x15
0x27 0x94 0x86 0xA3 0xCE 0xF5 0xAF 0xD0 0x38 0x32 0xD7 0x05 0xD2 0xB2 0xED 0x7E
0xEC 0xB1 0x3D 0x3C 0xFA 0xE8 0xA4 0x14 0xE1 0x67 0x0E 0x16 0xF5 0x57 0x3B 0xAA
0x84 0x31 0x02 0x3F 0x29 0x34 0x1D 0x68 0xCF 0x82 0x23 0x32 0x4C 0x90 0x93 0x75
编辑 - 解密签名:
0x00 0x01 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x00 0x30 0x31 0x30
0x0d 0x06 0x09 0x60 0x86 0x48 0x01 0x65 0x03 0x04 0x02 0x01 0x05 0x00 0x04 0x20
0xd4 0x98 0x1a 0x11 0xb8 0xd9 0xa6 0x86 0xe7 0xf9 0x91 0x9c 0xf7 0xd6 0x47 0x7c
0x5e 0x7c 0x0e 0x35 0xfc 0xd6 0x11 0x33 0xad 0x2f 0xdb 0x8c 0xb8 0x45 0xb4 0x9a
【问题讨论】:
whatToValidate
是你自己的哈希计算,没关系。 signature
的结果是RSA_public_decrypt
,对吧?!我认为signature
是要比较的哈希值。但是还有computeHashSHA256(signature, signatureHash);
,所以你再次散列它。为什么?
我正在关注链接的 JWT 指南。第二步说:“使用公钥(RSA_public_decrypt)解密签名,并获得签名哈希(在解密签名上计算SHA256)”。
好的,不过只是为了测试,也许你可以打印signature
的值并显示在这里。
嗯,看起来 RSA_public_decrypt
实际上会输出 PKCS#1 编码的哈希值。不过,您实际上应该只使用RSA_verify
;这是basic example 说明您将如何使用该功能。
@jps 当然;我正在写一个包含这个的答案。
【参考方案1】:
您的主要问题是您正在计算调用 RSA_public_decrypt
时恢复的哈希的哈希。
RSA 签名只是一些数据的散列加上一些通过 RSA 算法运行的额外信息。当您致电RSA_public_decrypt
时,您将恢复原始哈希+元数据;不是最初签名的消息。您将其视为已签名的原始消息,然后计算其哈希值以与 JWT 进行比较。
您将遇到的第二个问题是 RSA 签名在输入 RSA 算法之前被填充到密钥模数的大小。这就是为什么 RSA_public_decrypt
的输出是 256 字节长,即使原始哈希只有 32 字节长。实际恢复的哈希是输出的最后 32 个字节。
综上所述,您需要执行以下操作:
// 1) Calculate the SHA256 hash of the base64urled header and payload
std::string to_validate = sha256raw(TOKEN_B64URL_HEADER + "." + TOKEN_B64URL_PAYLOAD);
// 2) decrypt the signature
std::string raw_signature = decodeBase64URL(TOKEN_B64URL_SIGNATURE);
std::string decrypted_signature(RSA_size(rsa), '\0');
int len = RSA_public_decrypt(
raw_signature.size(),
reinterpret_cast<unsigned char*>(raw_signature.data()),
reinterpret_cast<unsigned char*>(decrypted_signature.data()),
rsa,
RSA_PKCS1_PADDING // Will verify that the padding is at least structurally correct
);
decrypted_signature.resize(len);
// 3) Extract the last 32 bytes to get the original message hash from the decrypted signature
std::string recovered_hash = decrypted_signature.substr(decrypted_signature.size() - to_validate.size())
// 4) Compare the recovered hash to the hash of the token
if (to_validate == recovered_hash)
std::cout << "Signature verified\n";
else
std::cout << "Signature validation failed\n"
请注意,这仍然不完全正确。签名填充的一部分包含有关用于生成签名的哈希类型的信息(请参阅RFC 8017 section 9.2)。进行最终比较的正确方法是生成适当的填充并将to_validate
附加到它的末尾,而不是剥离decrypted_signature
的填充。 OpenSSL 不提供公共功能,但可以这样做。
这就是为什么你应该使用RSA_verify
而不是摆弄RSA_public_decrypt
。它只是在内部调用RSA_public_decrypt
,但它也使用私有函数以正确的方式进行比较以生成PKCS#1 标头(RSA_public_decrypt
和RSA_PKCS1_PADDING
验证其余填充是否正确)。为此,您需要执行以下操作:
// 1) Calculate the SHA256 hash of the base64urled header and payload
std::string to_validate = sha256raw(TOKEN_B64URL_HEADER + "." + TOKEN_B64URL_PAYLOAD);
// 2) verify the signature
std::string raw_signature = decodeBase64URL(TOKEN_B64URL_SIGNATURE);
int verified = RSA_verify(
NID_sha256,
reinterpret_cast<unsigned char*>(to_validate.data()),
to_validate.size(),
reinterpret_cast<unsigned char*>(raw_signature.data()),
raw_signature.size(),
rsa
);
if (verified)
std::cout << "Signature verified\n";
else
std::cout << "Signature validation failed\n"
查看here 了解两个版本的现场演示
【讨论】:
谢谢,学习了一些关于 RSA 的知识。现在读完之后,似乎 OP 已经很接近了:whatToValidate: d4981a11b8d9a686e7f9919cf7d6477c5e7c0e35fcd61133ad2fdb8cb845b49a
与最后 32 个字节相比:0xd4 0x98 0x1a 0x11 0xb8 0xd9 0xa6 0x86 0xe7 0xf9 0x91 0x9c 0xf7 0xd6 0x47 0x7c 0x5e 0x7c 0x0e 0x35 0xfc 0xd6 0x11 0x33 0xad 0x2f 0xdb 0x8c 0xb8 0x45 0xb4 0x9a
,只需将结果带入相同的形式并进行比较即可。以上是关于C++ 如何使用 OpenSSL 验证 Google JWT (RS256)的主要内容,如果未能解决你的问题,请参考以下文章
验证openssl c ++中的签名,该签名由JAVA DSA签名?
如何使用 Visual Studio 2019 和 c++ 链接 OpenSSL 库?