AS400/iSeries/IBM i 中用 OpenSSL 生成的 JWT 签名为何验证失败?
结论
这段命令有两个独立的问题:
- JWT Header 声明的是
RS256,但openssl req -newkey rsa-pss生成的是 RSA-PSS 私钥。用这把私钥签发 JWT 时,应选择PS256,并明确设置 SHA-256、MGF1-SHA-256 和 32 字节盐长度。 - Base64 编码时读取
signature.bin,又把管道结果写回同一个文件。Shell 的输出重定向会截断文件,管道中的读写可能发生竞争,导致结果为空或不完整。
保留原证书创建命令时,应使用 PS256,把二进制签名和 Base64url 文本保存到不同文件,并确保接收方的验证实现支持 PS256。
如果接收方只接受 RS256,这把 RSA-PSS 私钥就无法按原样满足要求,双方需要调整算法或密钥约定。
为什么 RS256 与这把私钥不匹配?
RS256 和 PS256 都使用 RSA 与 SHA-256,但填充方式不同:
JWT alg | 签名方式 |
|---|---|
RS256 | RSASSA-PKCS1-v1_5 + SHA-256 |
PS256 | RSASSA-PSS + SHA-256,MGF1 使用 SHA-256,盐长度为 32 字节 |
原来的 Header:
{"alg":"RS256","typ":"JWT"}
要求验证端按 PKCS#1 v1.5 验签。即使编码完全正确,RSA-PSS 签名也无法通过这种验证。
证书创建命令中的参数:
-sigopt rsa_pss_saltlen:32
只控制该次证书签名操作的盐长度,不会成为后续 openssl dgst 调用的默认参数。生成 JWT 签名时,仍需明确指定 PSS 参数。
JWT 使用 key.pem 中的私钥签名,cert.pem 提供对应的公钥。Country、State、Name、Email 等证书主体信息不参与 JWT 的签名输入。
原命令的编码问题在哪里?
下面的命令同时读取和覆盖同一个文件:
openssl enc -base64 -A -in signature.bin |
tr -d '\n=' |
tr '+/' '-_' > signature.bin
> signature.bin 会在最后一个管道命令运行前打开并截断文件,而前面的 OpenSSL 命令也要读取它。不能依赖管道中各进程的执行时序来保留输入。
Base64url 的转换规则没有问题:
- 将
+替换为-。 - 将
/替换为_。 - 删除末尾的
=,且输出不含换行。
编码结果应写入另一个文件,例如 signature.b64url。
保留原证书,生成 PS256 JWT
以下示例假定 /test 下已有原命令生成的 key.pem 和 cert.pem。执行时,Shell 环境需按 ASCII/UTF-8 处理这些文本。
将 Header 改为:
{"alg":"PS256","typ":"JWT"}
对应的 Base64url 字符串为:
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9
修改 Header 后必须重新签名:
set -eu
cd /test
header='eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9'
payload='eyJpc3MiOiJDdXN0b21lSUQiLCJzdWIiOiJDdXN0b21lcklEIiwiYXVkIjoiaHR0cHM6Ly90ZXN0LmNvbSIsImlhdCI6MTczOTI2NjIyOSwiZXhwIjoxNzM5MjY2NTI5LCJuYmYiOjE3MzkyNTYxNjksImp0aSI6IjEyMzQ1NiJ9'
# 待签名内容不能包含末尾换行
printf '%s.%s' "$header" "$payload" > signing-input.txt
# 生成符合 PS256 参数要求的二进制签名
openssl dgst -sha256 \
-sign key.pem \
-sigopt rsa_padding_mode:pss \
-sigopt rsa_pss_saltlen:32 \
-sigopt rsa_mgf1_md:sha256 \
-out signature.bin \
signing-input.txt
# 先生成普通 Base64,再转换为 Base64url
openssl base64 -A \
-in signature.bin \
-out signature.b64
tr '+/' '-_' < signature.b64 |
tr -d '=\r\n' > signature.b64url
# 拼成完整 JWT
signature=$(cat signature.b64url)
printf '%s.%s.%s\n' "$header" "$payload" "$signature" > token.jwt
cat token.jwt
| 文件 | 内容 |
|---|---|
signing-input.txt | 原样的 header.payload,没有末尾换行 |
signature.bin | RSA-PSS 二进制签名 |
signature.b64url | JWT 第三段使用的 Base64url 签名 |
token.jwt | 完整的三段式 JWT |
使用 JWT 调试器时,应复制 token.jwt 中的完整 Token。signature.b64url 只有第三段,无法独立验证。
先在本地验证签名
从证书提取公钥,再用与签名相同的参数验签:
openssl x509 \
-in cert.pem \
-pubkey \
-noout > public.pem
openssl dgst -sha256 \
-verify public.pem \
-signature signature.bin \
-sigopt rsa_padding_mode:pss \
-sigopt rsa_pss_saltlen:32 \
-sigopt rsa_mgf1_md:sha256 \
signing-input.txt
公钥、签名、输入内容和参数都匹配时,应输出:
Verified OK
这里验证的是二进制签名,-signature 应传入 signature.bin,不能传入 Base64url 文本。
题目使用的是 4096 位密钥,二进制签名应为 512 字节;转换成无填充、无换行的 Base64url 后,应为 683 个字符。可用以下命令检查:
wc -c signature.bin signature.b64url
长度检查只能发现截断、编码或换行等问题,不能代替验签。
在 IBM i 和在线调试器中还要注意什么?
RPG 与 OpenSSL 必须使用相同的字节
JWT 的签名输入是:
BASE64URL(UTF8(header)) + "." + BASE64URL(UTF8(payload))
这串内容交给签名算法时按 ASCII 字节处理。在 IBM i 上,需要检查 RPG 字符字段、IFS 文件与 PASE 程序之间是否发生 CCSID 转换,尤其要避免:
- 将 EBCDIC 字节直接作为 JWT 签名输入。
- 固定长度字段带入尾部空格。
- 增加换行或 UTF-8 BOM。
- 对二进制签名执行文本字符集转换。
即使两份 JSON 解码后表达的内容相同,只要编码后的 header.payload 字节不同,签名就不匹配。openssl dgst -sha256 已经负责计算摘要,不要预先计算一次 SHA-256,再把摘要当作普通消息传入。
调试器需要支持算法和公钥格式
如果本地验签成功,在线工具却仍然失败,应检查工具是否支持 PS256,以及能否导入带有 id-RSASSA-PSS 算法标识的公钥。仅支持普通 RSA 公钥导入的实现,可能在导入阶段就出现兼容性问题。
在网页中填入私钥后看到“Valid Signature”,也不能单凭这一点判断命令行结果正确。如果工具用私钥重新生成了签名,它验证的可能是重新生成的 Token。核对时应使用完整 Token 和对应的公钥。
签名正确与 Token 可用是两项检查
示例 Payload 中的 exp 是固定的历史时间,实际使用时需要更新。即使密码学验签通过,接收方仍可能因为 Token 过期、尚未到达 nbf 指定的时间,或 iss、aud 不符合要求而拒绝它。
RSA-PSS 使用随机盐。同一份输入连续签名两次,得到不同签名是正常现象,应通过验签判断是否正确。
在线排查时只使用测试密钥,真实私钥不应提交到第三方调试网站。
备注:内容仅供参考。
