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