跳到主要内容
ecdsa.com
Go

go-jose

go-jose,Kubernetes、Vault 和 Dex 等项目使用的 Go JOSE 实现:从 v4 起解析时必须指定算法列表,签名检查与声明校验分成两次调用,容差默认为一分钟。
许可证
Apache-2.0
检查的版本
4.1.6 ·
源代码
github.com/go-jose/go-jose
文档
pkg.go.dev/github.com/go-jose/go-jose/v4

兼容性概览

JOSE 与 JWT

JWS 算法
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

github.com/go-jose/go-jose/v4(Square 版 go-jose 的延续)实现了 JWS、JWE、JWK 和 JWT,因此凡是 Go 服务之间交换加密或签名令牌的地方,都能见到它。4.1.6 版对 HS、RS、PS、ES 系列和 EdDSA 的签名与验证均成功;ES256K、Ed25519 和 Ed448 不是它能识别的算法名称。

第 4 版把算法列表改为必需:jwt.ParseSigned(token, nil) 会报错 no signature algorithms specified,因此只能按你选定的算法解析令牌。如果该列表中包含 none,未签名的令牌也能解析,但 Claims(key) 绝不会让它们通过验证——读取这类令牌的唯一途径是名称一目了然的 UnsafeClaimsWithoutVerification。

验证分成两步,陷阱就在这里。token.Claims(key, &claims) 检查签名并填充结构体——对于一小时前就已过期的令牌,它照样返回了声明,没有任何报错。过期时间、生效时间(not-before)、颁发者和受众只由带 jwt.Expected 值的 claims.Validate() 或 ValidateWithLeeway 检查,而不含 exp 的令牌连这一关也能通过,因此要像示例那样检查 Expiry 是否已设置。Validate 允许 jwt.DefaultLeeway,即一分钟的容差,检查也证实了这一点。

密钥是有类型的。以 PEM 文本为密钥的 HS256 令牌,用 *ecdsa.PublicKey 验证时报错 error in cryptographic primitive,但把同样的 PEM 字节作为密钥传入时却验证通过——应把密钥保存为解析后的 Go 密钥,而不是字节。ES256 会拒绝 P-384 密钥,报错 expected 256 bit key, got 384 bits instead。DER 编码的 ES256 签名与伪造的签名一样,报出同样笼统的 error in cryptographic primitive;JWT 调试器会显示签名长度。

对于加密令牌,JWE 查看器会显示头部和算法;对于密钥集,JWKS 检测会显示提供方发布的内容,JWK 转换器可以在 JWK 和 PEM 之间转换密钥。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/go-jose/run.sh 于2026年10月20日在容器镜像 golang:1.27.3-trixie 中运行了 go-jose 4.1.6;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • JWS 算法

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

    256/384/512 位的 HS、RS、PS 和 ES 以及 EdDSA 的签名与验证均成功;ES256K、Ed25519 和 Ed448 报错 unknown/unsupported algorithm。

    检查记录
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA
    evidence jws-algorithms: ES256K: go-jose/go-jose: unknown/unsupported algorithm
    evidence jws-algorithms: Ed25519: go-jose/go-jose: unknown/unsupported algorithm
    evidence jws-algorithms: Ed448: go-jose/go-jose: unknown/unsupported algorithm
  • ES256 签名编码

    raw

    ES256 签名是 64 字节的原始 r‖s。

    检查记录
    fact jws-ecdsa-encoding = raw
  • JWS 中的 DER 签名

    rejected

    DER 编码的 ES256 签名会报错 go-jose/go-jose: error in cryptographic primitive。

    检查记录
    fact jws-der-signature = rejected
    evidence jws-der-signature: go-jose/go-jose: error in cryptographic primitive
  • 算法允许列表

    required

    jwt.ParseSigned 要求传入允许的算法;列表为空时报错 no signature algorithms specified。

    检查记录
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: ParseSigned with an empty algorithm list: go-jose/go-jose: no signature algorithms specified
  • 未签名的令牌(alg none)

    rejected

    列表中包含 none 时,未签名的令牌可以解析,但无论使用 EC 密钥还是 HMAC 密钥,Claims() 都会失败;只有 UnsafeClaimsWithoutVerification 能读取它。

    检查记录
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: ParseSigned with none in the list: <nil>
    evidence jwt-alg-none: Claims(*ecdsa.PublicKey): go-jose/go-jose: error in cryptographic primitive
    evidence jwt-alg-none: Claims([]uint8): go-jose/go-jose: error in cryptographic primitive
    evidence jwt-alg-none: UnsafeClaimsWithoutVerification: <nil>, sub fact-check
  • 把公钥用作 HMAC 密钥

    rejected

    以 PEM 文本为密钥的 HS256 令牌,用 *ecdsa.PublicKey 验证失败;只有把 PEM 字节本身作为密钥传入时才验证通过。

    检查记录
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: Claims with the *ecdsa.PublicKey: go-jose/go-jose: error in cryptographic primitive
    evidence jwt-hmac-public-key: Claims with the PEM bytes as key: <nil>
  • 按 alg 检查曲线

    enforced

    用 P-384 密钥签名 ES256 时报错 expected 256 bit key, got 384 bits instead。

    检查记录
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: go-jose/go-jose: expected 256 bit key, got 384 bits instead
  • 解码时校验声明

    no

    对于一小时前就已过期的令牌,Claims() 照样返回了声明,没有报错;时间和颁发者检查只在 Validate 中进行。

    检查记录
    fact jwt-claims-validated-on-decode = no
    evidence jwt-claims-validated-on-decode: Claims() on a token expired an hour ago: <nil>
  • 默认时钟偏差

    60s

    Validate 允许 jwt.DefaultLeeway,即一分钟的容差:57 秒前过期的令牌通过了,63 秒前过期的令牌未通过。

    检查记录
    fact jwt-clock-skew = 60s
    evidence jwt-clock-skew: jwt.DefaultLeeway = 1m0s
  • 是否要求过期时间声明

    no

    不含 exp 的令牌通过了 Validate;需要自行检查 Expiry 是否不为 nil。

    检查记录
    fact jwt-exp-required = no

用法

用 ES256 对注册声明签名,按算法列表解析,先检查签名,再单独校验声明。
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"fmt"
	"time"

	jose "github.com/go-jose/go-jose/v4"
	"github.com/go-jose/go-jose/v4/jwt"
)

func main() {
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key}, (&jose.SignerOptions{}).WithType("JWT"))
	raw, _ := jwt.Signed(signer).Claims(jwt.Claims{Subject: "user-42", Issuer: "https://auth.example.com", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}).Serialize()

	// v4 requires the allowed algorithms when parsing.
	token, _ := jwt.ParseSigned(raw, []jose.SignatureAlgorithm{jose.ES256})
	claims := jwt.Claims{}
	sigErr := token.Claims(&key.PublicKey, &claims) // checks the signature only
	// Validate checks exp, nbf and iss (with jwt.DefaultLeeway of one minute).
	valErr := claims.Validate(jwt.Expected{Issuer: "https://auth.example.com", Time: time.Now()})
	fmt.Println("valid:", sigErr == nil && valErr == nil && claims.Expiry != nil, "sub:", claims.Subject)
}

代码片段测试工具会在容器镜像 golang:1.27.3-trixie 中原样运行每个示例,只要输出不匹配,运行就会失败。

命令
GOFLAGS=-mod=mod go run .
输出必须匹配(正则表达式)
valid: true sub: user-42

库