key-cape/adapters/go/tokenstatus/internal/jose/jose.go
tegwick 08c083bae9
Some checks failed
Authentication acceptance / acceptance (push) Failing after 59s
Authentication acceptance / provider-contract (push) Successful in 13s
Build and Publish Container Image / build-and-push (push) Successful in 31s
Share current-token verification and scoped cleanup through one producer client
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a14bfc-9643-7872-aa12-98326dcf25e7
2026-10-18 11:36:26 +02:00

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)
}