Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a14bfc-9643-7872-aa12-98326dcf25e7
171 lines
5.4 KiB
Go
171 lines
5.4 KiB
Go
// Package jose provides strict RS256 JWT signature verification against a JWK
|
|
// set. It is deliberately narrow: it establishes that a token was signed by a
|
|
// key in the given set and returns its claims. It applies no claim policy —
|
|
// issuer, audience, expiry and nonce rules differ per caller and belong with
|
|
// the caller that knows them.
|
|
//
|
|
// The strictness here is the point. A verifier that accepts an unexpected
|
|
// algorithm, an unbounded exponent or an undersized modulus is worse than no
|
|
// verifier, because callers stop checking what it appears to guarantee.
|
|
package jose
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"math/big"
|
|
"strings"
|
|
)
|
|
|
|
// ErrVerification is returned for every verification failure. The cause is
|
|
// deliberately not distinguished: a caller acting on why a token failed tends to
|
|
// leak that distinction to whoever supplied the token.
|
|
var ErrVerification = errors.New("jose: token verification failed")
|
|
|
|
// KeySet maps key IDs to RSA public keys.
|
|
type KeySet map[string]*rsa.PublicKey
|
|
|
|
// ParseJWKS parses a JWK set, keeping only RSA signing keys usable with RS256.
|
|
// Keys that are malformed, too small, or carry an even or trivial exponent are
|
|
// rejected rather than skipped, since a key set that is partly wrong is not
|
|
// evidence of anything.
|
|
func ParseJWKS(raw []byte) (KeySet, error) {
|
|
var document struct {
|
|
Keys []struct {
|
|
Kty string `json:"kty"`
|
|
Use string `json:"use"`
|
|
Alg string `json:"alg"`
|
|
Kid string `json:"kid"`
|
|
N string `json:"n"`
|
|
E string `json:"e"`
|
|
} `json:"keys"`
|
|
}
|
|
if err := json.Unmarshal(raw, &document); err != nil {
|
|
return nil, ErrVerification
|
|
}
|
|
keys := make(KeySet, len(document.Keys))
|
|
for _, key := range document.Keys {
|
|
// Ignore keys for other algorithms or purposes; they are not errors.
|
|
if key.Kty != "RSA" || (key.Use != "" && key.Use != "sig") || (key.Alg != "" && key.Alg != "RS256") {
|
|
continue
|
|
}
|
|
if key.Kid == "" {
|
|
return nil, ErrVerification
|
|
}
|
|
modulus, errN := decode(key.N)
|
|
exponent, errE := decode(key.E)
|
|
if errN != nil || errE != nil || len(exponent) == 0 || len(exponent) > 4 {
|
|
return nil, ErrVerification
|
|
}
|
|
public := &rsa.PublicKey{
|
|
N: new(big.Int).SetBytes(modulus),
|
|
E: int(new(big.Int).SetBytes(exponent).Int64()),
|
|
}
|
|
if public.N.BitLen() < 2048 || public.E < 3 || public.E%2 == 0 {
|
|
return nil, ErrVerification
|
|
}
|
|
if _, duplicate := keys[key.Kid]; duplicate {
|
|
return nil, ErrVerification
|
|
}
|
|
keys[key.Kid] = public
|
|
}
|
|
if len(keys) == 0 {
|
|
return nil, ErrVerification
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// KeyID returns the key ID from a token's JOSE header, after checking that the
|
|
// header names RS256 and carries no critical extensions. Callers use it to
|
|
// decide whether to refresh their key set before verifying.
|
|
func KeyID(token string) (string, error) {
|
|
header, _, _, err := split(token)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return header.Kid, nil
|
|
}
|
|
|
|
// Verify checks a token's RS256 signature against the key set and returns its
|
|
// claims. It does not inspect any claim.
|
|
func Verify(token string, keys KeySet) (map[string]interface{}, error) {
|
|
return verify(token, keys, "")
|
|
}
|
|
|
|
// VerifyTyped applies the same signature verifier with an explicit JOSE type.
|
|
// Existing Verify callers retain their claim/type policy; online access-token
|
|
// consumers opt into the producer's exact JWT header contract here.
|
|
func VerifyTyped(token string, keys KeySet, expectedType string) (map[string]interface{}, error) {
|
|
if expectedType == "" {
|
|
return nil, ErrVerification
|
|
}
|
|
return verify(token, keys, expectedType)
|
|
}
|
|
|
|
func verify(token string, keys KeySet, expectedType string) (map[string]interface{}, error) {
|
|
header, signingInput, signature, err := split(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if expectedType != "" && header.Typ != expectedType {
|
|
return nil, ErrVerification
|
|
}
|
|
public, ok := keys[header.Kid]
|
|
if !ok {
|
|
return nil, ErrVerification
|
|
}
|
|
digest := sha256.Sum256([]byte(signingInput))
|
|
if rsa.VerifyPKCS1v15(public, crypto.SHA256, digest[:], signature) != nil {
|
|
return nil, ErrVerification
|
|
}
|
|
payload, err := decode(strings.Split(signingInput, ".")[1])
|
|
if err != nil {
|
|
return nil, ErrVerification
|
|
}
|
|
claims := map[string]interface{}{}
|
|
if json.Unmarshal(payload, &claims) != nil {
|
|
return nil, ErrVerification
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
type joseHeader struct {
|
|
Alg string `json:"alg"`
|
|
Typ string `json:"typ"`
|
|
Kid string `json:"kid"`
|
|
Crit []string `json:"crit"`
|
|
}
|
|
|
|
// split validates the token's shape and header and returns the header, the
|
|
// signing input, and the decoded signature.
|
|
func split(token string) (joseHeader, string, []byte, error) {
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) != 3 {
|
|
return joseHeader{}, "", nil, ErrVerification
|
|
}
|
|
raw, err := decode(parts[0])
|
|
if err != nil {
|
|
return joseHeader{}, "", nil, ErrVerification
|
|
}
|
|
var header joseHeader
|
|
if json.Unmarshal(raw, &header) != nil {
|
|
return joseHeader{}, "", nil, ErrVerification
|
|
}
|
|
// An unexpected algorithm must fail before any key is selected, so "none"
|
|
// and symmetric algorithms can never reach the RSA path.
|
|
if header.Alg != "RS256" || header.Kid == "" || len(header.Crit) != 0 {
|
|
return joseHeader{}, "", nil, ErrVerification
|
|
}
|
|
signature, err := decode(parts[2])
|
|
if err != nil {
|
|
return joseHeader{}, "", nil, ErrVerification
|
|
}
|
|
return header, parts[0] + "." + parts[1], signature, nil
|
|
}
|
|
|
|
func decode(segment string) ([]byte, error) {
|
|
return base64.RawURLEncoding.DecodeString(segment)
|
|
}
|