PHP OpenSSL 无法读取 PEM 格式的 public 密钥

PHP OpenSSL cannot read public key in PEM format

我有一个 NodeJS 应用程序使用 PS256 算法生成 JSON Web 令牌。我想尝试在 PHP 应用程序中验证这些令牌中的签名。

到目前为止我得到了以下信息:

我的 JWT:

eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTBiYjllYS00YTg0LTQ1ZTMtOTg5My0wYzNhNDYxZmQzMGUiLCJpYXQiOjE2MDU4OTI5NjcsImV4cCI6MTYwNjQ5Nzc2NywiYXVkIjoiNzBiYzcxMTQ1MWM2NDBjOTVlZjgzYjdhNDliMWE0MWIiLCJpc3MiOiIyM2ZhYTRiNC0wNmVlLTRlNGEtYTVjZC05NjJmOTRhMjEzYmYiLCJqdGkiOiI1MTNiYjczZC0zOTY3LTQxYzUtODMwOS00Yjc1ZDI4ZGU3NTIifQ.kLtaSYKyhqzx7Dc7UIz7tqU8TsXabRLxGiaqw21lgCcuf_eBvpiLkFOuXpUs-V8XQunQg8jV-bKlKUIb0pLvipjhRP50IwKDClQgNtIwn4yyX5RyDNGJur0qHNnkHMLaF11NsXGPyhvh-6ogSZjWgyZnkQJkXpz4jggBetwqz1hnicapGfNb6C-UdRcOLyCaiMD4OmvniFVCY6YoKlC6eHdwxsgHAxOSgD1QKiiQX_yAe39ja_axZD2Ii3QaNgO0WXzfWMbqRg_yl0y3kjQFys9iXGvQ1JIKDMLffR3rKVL5PgKSU3e472xcPKf6PNSJzphPi1G_xH2gqg1VVXo3Lg

解码:

Header:
(
    [alg] => PS256
    [typ] => JWT
)

Body:
(
    [sub] => 010bb9ea-4a84-45e3-9893-0c3a461fd30e
    [iat] => 1605892967
    [exp] => 1606497767
    [aud] => 70bc711451c640c95ef83b7a49b1a41b
    [iss] => 23faa4b4-06ee-4e4a-a5cd-962f94a213bf
    [jti] => 513bb73d-3967-41c5-8309-4b75d28de752
)

我在 RS256 上选择了 PS256 算法,因为我在博客 post 上看到它更安全。老实说,我不知道有什么区别。

我在 ES256 上使用 PS256 算法,因为在测试时我发现虽然 ES256 生成更小的签名(因此更小的令牌),但计算时间大约长 3 倍。我的目标是使这个应用程序尽可能具有可扩展性,因此要避免长时间的计算。

我的 public 密钥:

-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA0wO7By66n38BCOOPqxnj78gj8jMKACO5APe+M9wbwemOoipeC9DR
CGxC9q+n/Ki0lNKSujjCpZfnKe5xepL2klyKF7dGQwecgf3So7bve8vvb+vD8C6l
oqbCYEOHdLCDoC2UXEWIVRcV5H+Ahawym+OcE/0pzWlNV9asowIFWj/IXgDVKCFQ
nj164UFlxW1ITqLOQK1WlxqHAIoh20RzpeJTlX9PYx3DDja1Pw7TPomHChMeRNsw
Z7zJiavYrBCTvYE+tm7JrPfbIfc1a9fCY3LlwCTvaBkL2F5yeKdH7FMAlvsvBwCm
QhPE4jcDINUds8bHu2on5NU5VmwHjQ46xwIDAQAB
-----END RSA PUBLIC KEY-----

对 NodeJS 使用 jsonwebtoken 我可以验证此令牌并授权使用它发出的请求。所以所有的数据看起来都不错,关键有效,并且数学检查出来了。

然而,在尝试验证 PHP 中的令牌时,我 运行 遇到了两个问题:

1。 public 密钥似乎无效?

$key = openssl_pkey_get_public($pem);
print_r($key);
die();

此代码打印出“false”——表明无法从上面 posted 的 PEM 文本中读取密钥。谷歌搜索我发现 this comment in the PHP manual 提供了一个解决方案。我按照指示做了(从我的密钥中删除了换行符,在前面加上 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A,然后换行到 64 个字符)并且出于某种原因 openssl_pkey_get_public($pem) 现在实际上返回了一个 OpenSSL Public 密钥。不过,我不太热衷于使用我不理解的 copy/paste 解决方案,并且评论提到这仅适用于 2048 位密钥,如果我们想升级我们的安全性,我会担心未来。

根据建议对我的密钥进行更改后,新密钥如下所示:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wO7By66n38BCOOPqxnj
78gj8jMKACO5APe+M9wbwemOoipeC9DRCGxC9q+n/Ki0lNKSujjCpZfnKe5xepL2
klyKF7dGQwecgf3So7bve8vvb+vD8C6loqbCYEOHdLCDoC2UXEWIVRcV5H+Ahawy
m+OcE/0pzWlNV9asowIFWj/IXgDVKCFQnj164UFlxW1ITqLOQK1WlxqHAIoh20Rz
peJTlX9PYx3DDja1Pw7TPomHChMeRNswZ7zJiavYrBCTvYE+tm7JrPfbIfc1a9fC
Y3LlwCTvaBkL2F5yeKdH7FMAlvsvBwCmQhPE4jcDINUds8bHu2on5NU5VmwHjQ46
xwIDAQAB
-----END PUBLIC KEY-----

(请注意,这是 相同的密钥 ,只是在它的开头加上了 32 个魔法字节和“BEGIN RSA PUBLIC KEY" 替换为 "BEGIN PUBLIC KEY")

2。签名验证失败(可能是因为我使用的是 PS256 而不是 RS256)

暂时忽略#1 的问题并继续下一步,我尝试像这样验证我的签名:

$success = openssl_verify($jwtHeader . "." . $jwtBody, $jwtSignature, $key, OPENSSL_ALGO_SHA256);

这返回了 false。意味着签名无效。但我知道签名是有效的,因为它在 NodeJS 中运行良好。所以我怀疑这里的问题围绕着我对算法的选择。

如何让 PHP 正确验证此令牌?

更新 1

这是我用来在 NodeJS 中验证令牌的代码。这是一个 HapiJS + TypeScript 项目,但你应该能够理解它。 jwt 刚定义为 import * as jwt from "jsonwebtoken";:

                        jwt.verify(
                            token,
                            server.plugins["bf-jwtAuth"].publicKeys[tokenData.iss].key,
                            {
                                algorithms: [options.algorithm],
                                audience: userHash,
                                maxAge: options.tokenMaxAge
                            },
                            err =>
                            {
                                // We can disregard the "decoded" parameter
                                // because we already decoded it earlier
                                // We're just interested in the error
                                // (implying a bad signature)
                                if (err !== null)
                                {
                                    request.log([], err);
                                    return reject(Boom.unauthorized());
                                }

                                return resolve(h.authenticated({
                                    credentials: {
                                        user: {
                                            id: tokenData.sub
                                        }
                                    }
                                }));
                            }
                        );

这里没什么可看的,因为我只是依靠第三方工具为我完成所有验证。 jwt.verify(...) 它就像魔法一样奏效。

更新 2

假设我的问题出在所使用的算法上(PS256 vs RS256),我开始搜索并找到 this Whosebug post which pointed me at phpseclib

我们实际上已经通过 Composer 安装了 phpseclib 作为 Google 的 auth SDK 的依赖项,所以我将它提升到顶级依赖项并试一试。不幸的是,我仍然 运行 遇到了问题。到目前为止,这是我的代码:

use phpseclib\Crypt\RSA;

// Setup:
$rsa = new RSA();
$rsa->setHash("sha256");
$rsa->setMGFHash("sha256");
$rsa->setSignatureMode(RSA::SIGNATURE_PSS);

// The variables I'm working with:
$jwt = explode(".", "..."); // [Header, Body, Signature]
$key = "..."; // This is my PEM-encoded string, from above

// Attempting to verify:
$rsa->loadKey($key);
$valid = $rsa->verify($jwt[0] . "." . $jwt[1], base64_decode($jwt[2]));
if ($valid) { die("Valid"); } else { die("Invalid"); }

两个 die() 语句都没有达到,因为我在 $rsa->verify() 行上遇到以下错误:

ErrorException: Invalid signature
at
/app/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php(2693)

查看库中的这一行,它似乎在“长度检查”步骤失败:

if (strlen($s) != $this->k) {
    user_error("Invalid signature");
}

不过我不确定它需要多长时间。我直接从 JWT

传递了原始签名

折腾了一整天,我终于找到了缺失的部分。

首先,关于原始问题的一些快速注释(已在更新中触及):

  • 要使用 PSS 填充 ("PS256") 进行 RSA 签名,您需要第三方库,因为 PHP 中的 OpenSSL 函数不支持此功能。一个常见的建议是 phpseclib
  • 我必须添加到密钥的 32 个魔法字节只是 PHP 的 OpenSSL 函数的一个怪癖,不需要与 phpseclib
  • 一起使用

话虽如此,我的最后一个问题(签名“无效”)是:

JWT 签名是 base64URL 编码的,而不是 base64 编码的

我什至不知道有 RFC specification for base64URL encoding。事实证明,您只需将每个 + 替换为 -,并将每个 / 替换为 _。所以而不是:

$signature = base64_decode($jwt[2]);

应该是:

$signature = base64_decode(strtr($jwt[2], "-_", "+/"));

这有效,我的签名终于生效了!