PAYATHON 2026

AS400/iSeries/IBM i 中用 OpenSSL 生成的 JWT 签名为何验证失败?

排障赵师傅

结论

这段命令有两个独立的问题:

  1. JWT Header 声明的是 RS256,但 openssl req -newkey rsa-pss 生成的是 RSA-PSS 私钥。用这把私钥签发 JWT 时,应选择 PS256,并明确设置 SHA-256、MGF1-SHA-256 和 32 字节盐长度。
  2. Base64 编码时读取 signature.bin,又把管道结果写回同一个文件。Shell 的输出重定向会截断文件,管道中的读写可能发生竞争,导致结果为空或不完整。

保留原证书创建命令时,应使用 PS256,把二进制签名和 Base64url 文本保存到不同文件,并确保接收方的验证实现支持 PS256。

如果接收方只接受 RS256,这把 RSA-PSS 私钥就无法按原样满足要求,双方需要调整算法或密钥约定。

为什么 RS256 与这把私钥不匹配?

RS256 和 PS256 都使用 RSA 与 SHA-256,但填充方式不同:

JWT alg签名方式
RS256RSASSA-PKCS1-v1_5 + SHA-256
PS256RSASSA-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.binRSA-PSS 二进制签名
signature.b64urlJWT 第三段使用的 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 使用随机盐。同一份输入连续签名两次,得到不同签名是正常现象,应通过验签判断是否正确。

在线排查时只使用测试密钥,真实私钥不应提交到第三方调试网站。

备注:内容仅供参考。