如何从JWK重建RSA公钥以验证JWT?
解决从JWK生成RSA公钥并验证JWT签名的问题
问题根源
你当前的代码错误在于直接将JWK中的n(模数)和e(指数)字符串拼接,没有正确处理它们的编码格式,也没有遵循X.509公钥的ASN.1结构规范:
- JWK的
n和e是Base64URL编码(而非标准Base64)的二进制数据,需要先解码为原始字节 - PEM格式的公钥要求内容是DER编码的ASN.1结构,而非简单拼接字符串
正确的转换步骤
1. 处理Base64URL解码
Base64URL和标准Base64的区别在于:替换+为-,/为_,并去掉末尾的=填充符。需要先把n和e转换为标准Base64再解码。
2. 构建ASN.1结构
RSA公钥的ASN.1 DER结构是一个嵌套序列:
Sequence (2 elements) Sequence (2 elements) ObjectIdentifier 1.2.840.113549.1.1.1 (RSA Encryption) NULL BitString (1 element) Sequence (2 elements) Integer (n: 模数) Integer (e: 指数)
3. 转换为PEM格式
将DER编码的结构添加PEM头、尾,并按每64字符分段。
完整PHP实现代码
// 解析JWK $jwkJson = '{ "kty": "RSA", "use": "enc", "n": "ozmvkuGzWNHs9cEcC5PWwbG-dmSjPcoQFxEbqH_fBjkj_nfTTKshdiSq5ciulWEa_rrqQ2qwcSADNxtTzRR1qfud-NvsM8VltT7xDuVVqPTZoWLKa0BWXgQQ-1mCm1KdGltYWccB0R1LoF-rb3DEEZySsHvqErYzYt4M_rqjEiK5Y9y1h3k1h5Yk4zGLWchko3jiDS-pVevvWsQsN-Y3KuB19485G9P_MXLtfJWQ4wC4jlo9etdD_hgDfxX-hQy3wuwHfHifLdxvxiB8X5Is4m6DuY4_7hS5RwXAjO1QSd-zUYZNT_2yWVR56_jyiZEiOdgIm9QtLPZCTKzqsXoqZQ", "e": "AQAB" }'; $jwk = json_decode($jwkJson); // Base64URL转标准Base64并解码 function base64url_decode($data) { $padding = strlen($data) % 4; if ($padding > 0) { $data .= str_repeat('=', 4 - $padding); } return base64_decode(strtr($data, '-_', '+/')); } $n = base64url_decode($jwk->n); $e = base64url_decode($jwk->e); // 构建ASN.1 DER结构 // 先构建内层的n和e序列 $seqNE = pack('H*', '30') . // Sequence标签 pack('H*', dechex(strlen($n) + strlen($e) + 4)) . // 长度 pack('H*', '02') . // Integer标签 pack('H*', dechex(strlen($n))) . // n的长度 $n . pack('H*', '02') . // Integer标签 pack('H*', dechex(strlen($e))) . // e的长度 $e; // 构建外层的AlgorithmIdentifier序列 $algSeq = pack('H*', '300d06092a864886f70d0101010500'); // 构建最外层的公钥序列 $pubKeySeq = pack('H*', '30') . // Sequence标签 pack('H*', dechex(strlen($algSeq) + strlen($seqNE) + 2)) . // 长度 $algSeq . pack('H*', '03') . // BitString标签 pack('H*', dechex(strlen($seqNE) + 1)) . // 长度(+1是因为BitString的unused bits字段) pack('H*', '00') . // unused bits为0 $seqNE; // 转换为PEM格式 $pem = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($pubKeySeq), 64, "\n") . "-----END PUBLIC KEY-----"; // 验证签名(替换成你的$dataToVerify和$signature) $isSignatureValid = openssl_verify($dataToVerify, $signature, $pem, OPENSSL_ALGO_SHA256); // 检查结果 if ($isSignatureValid === 1) { echo "签名验证通过"; } elseif ($isSignatureValid === 0) { echo "签名验证失败"; } else { echo "验证出错: " . openssl_error_string(); }
关键细节解释
base64url_decode函数:处理JWK特有的编码格式,确保正确解码出原始二进制的模数和指数- ASN.1结构打包:使用
pack函数手动构建符合X.509标准的DER结构,这是openssl能识别公钥的核心 - PEM分段:必须按每64字符换行,否则openssl无法正确解析
内容的提问来源于stack exchange,提问作者Darren H
相关产品推荐
相关产品推荐

