【问题标题】:PHP OpenSSL cannot read public key in PEM formatPHP OpenSSL 无法读取 PEM 格式的公钥
【发布时间】:2021-03-04 02:56:54
【问题描述】:

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

到目前为止,我得到了以下内容:

我的智威汤逊:

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
)
  • sub 是一个 GUID 用户 ID(我们使用 GUID,因此如果用户 ID 泄露,则无法推断出任何信息,例如我们系统中的用户数量或用户注册时间)
  • iat 是发行令牌的纪元时间(UTC)
  • exp 是令牌过期的纪元时间(UTC)
  • aud 不符合 JWT 规范。我滥用这种说法来减轻被盗代币的影响。这是每个客户端请求发送的数据的 MD5 散列,这对于某人来说很难猜到。因此,如果有人盗用此令牌并在未发送适当密码的情况下使用它,该令牌将被自动撤销
  • iss 也不符合 JWT 规范。我滥用了这个声明来列出用于签署 JWT 的密钥的 ID。这样我就可以轮换我的公私密钥对并知道在验证签名时使用哪个密钥
  • jti 是唯一标识 JWT 的 GUID。与已撤销令牌的内存存储相比

我使用 PS256 算法而不是 RS256,因为我在博客文章中读到它更安全。老实说,我不知道有什么区别。

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

我的公钥:

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

jsonwebtoken 用于NodeJS,我可以验证此令牌并授权使用它发出的请求。所以所有的数据看起来都很好,关键是有效的,而且数学也算出来了。

但是,当我尝试在 PHP 中验证令牌时遇到了两个问题:

1。公钥似乎无效?

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

此代码打印出“false” - 表明无法从上面发布的 PEM 文本中读取密钥。谷歌搜索我发现this comment in the PHP manual 提供了一个解决方案。我按照指示做了(从我的密钥中删除了换行符,添加了MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A,然后包装为 64 个字符),出于某种原因,openssl_pkey_get_public($pem) 现在实际上返回了一个 OpenSSL 公钥。不过,我并不热衷于使用我不理解的复制/粘贴解决方案,并且评论提到这仅适用于 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 与 RS256),我开始四处搜索,发现 this StackOverflow post 指向我的 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"); }

由于我在$rsa->verify() 行中遇到以下错误,因此没有达到die() 语句:

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

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

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

不过,我不确定它的预期长度。我直接从 JWT 传递了原始签名

【问题讨论】:

  • jwt.io 我也无法使用公钥验证您的令牌。你能展示一下你用来验证它的node.js代码吗?
  • 和一个旁注:iss 也不符合 JWT 规范。我滥用此声明来列出用于签署 JWT 的密钥 ID - 标准是在令牌标头中使用 kid(密钥 ID)声明。
  • @jps 我已经用我在 NodeJS 中验证的代码更新了这个问题。快速确保我没有意外抓取不匹配的 JWT 和密钥并更新它们
  • @jps 我使用从我的问题的“问题 1”生成的公钥更新了我的公钥,并且 jwt.io 对此进行了很好的验证。所以问题不在于密钥,而在于密钥的编码方式。我将在我的问题中包含“正确”键(尽管我不明白其中的区别,希望有人能解释一下)
  • 我不知道发生了什么,在新打开的 jwt.io 选项卡中再次尝试,我现在可以验证签名。当我从编辑历史视图与普通问题视图(不同的换行符)复制密钥时,一些奇怪的效果具有不同的行为,但现在一切都很好......

标签: php openssl jwt


【解决方案1】:

在搞砸了一整天之后,我终于找到了丢失的部分。

首先,关于原始问题的一些简短说明(已在更新中涉及):

  • 要使用 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], "-_", "+/"));

这行得通,我的签名终于验证了!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-05-21
    • 2013-05-16
    • 1970-01-01
    • 1970-01-01
    • 2014-10-11
    • 2018-02-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多