~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
310 lines · 8.7 KB Raw History
package guestidentity

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rsa"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"math/big"
	"strings"
	"sync"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

var (
	ErrTokenRefused = errors.New("guestidentity: guest token refused")

	errNoKey = errors.New("guestidentity: no key in the issuer set matches the token")
)

var verificationMethods = []string{"RS256", "PS256", "ES256"}

type TokenClaims struct {
	Audience Audience

	API string

	Actx string

	ComputeVM string

	Claims map[string]any
}

type Verifier struct {
	Keys func(context.Context) ([]byte, error)

	Issuer func() string

	ClockTolerance time.Duration

	Now func() time.Time

	MaxTokenBytes int

	mu  sync.Mutex
	set map[string]any
}

func NewVerifier(keys func(context.Context) ([]byte, error), issuer func() string) *Verifier {
	return &Verifier{Keys: keys, Issuer: issuer, MaxTokenBytes: 8192, ClockTolerance: 5 * time.Second}
}

func NewOpenBaoVerifier(identity *Client, ingressURL func() string) *Verifier {
	return NewVerifier(IdentityKeys(identity), func() string { return OIDCIssuer(ingressURL()) })
}

func (v *Verifier) Refresh(ctx context.Context) error {
	raw, err := v.Keys(ctx)
	if err != nil {
		return fmt.Errorf("guestidentity: reading the OIDC keys: %w", err)
	}
	set, err := parseJWKSet(raw)
	if err != nil {
		return err
	}
	v.mu.Lock()
	v.set = set
	v.mu.Unlock()
	return nil
}

func (v *Verifier) Verify(ctx context.Context, token string) (TokenClaims, error) {
	if v == nil || v.Keys == nil || v.Issuer == nil {
		return TokenClaims{}, refused("no verifier")
	}
	token = strings.TrimSpace(token)
	if token == "" {
		return TokenClaims{}, refused("no token")
	}
	if limit := v.MaxTokenBytes; limit > 0 && len(token) > limit {
		return TokenClaims{}, refused("the token is longer than %d bytes", limit)
	}
	unverified := jwt.MapClaims{}
	if _, _, err := jwt.NewParser().ParseUnverified(token, unverified); err != nil {
		return TokenClaims{}, refused("malformed token: %v", err)
	}
	auds, err := unverified.GetAudience()
	if err != nil || len(auds) != 1 {
		return TokenClaims{}, refused("the token names no single audience")
	}
	audience, err := ParseAudience(auds[0])
	if err != nil {
		return TokenClaims{}, refused("%v", err)
	}
	if audience.API != KCPAPI {
		return TokenClaims{}, refused("the token's audience is not for %s", KCPAPI)
	}
	issuer := strings.TrimRight(strings.TrimSpace(v.Issuer()), "/")
	if issuer == "" {
		return TokenClaims{}, refused("the verifier names no issuer")
	}

	claims, err := v.parse(ctx, token, issuer, audience)
	if err != nil && (errors.Is(err, jwt.ErrTokenUnverifiable) || errors.Is(err, jwt.ErrTokenSignatureInvalid) || errors.Is(err, errNoKey)) {
		if rerr := v.Refresh(ctx); rerr == nil {
			claims, err = v.parse(ctx, token, issuer, audience)
		}
	}
	if err != nil {
		return TokenClaims{}, refused("token verification failed: %v", err)
	}
	computevm, _ := claims["computevm"].(string)
	if computevm == "" || computevm != audience.Actx {
		return TokenClaims{}, refused("the token's computevm claim is not the audience's context")
	}
	if !ValidComputeVM(computevm) {
		return TokenClaims{}, refused("the token's computevm claim is not a ComputeVM name")
	}
	return TokenClaims{Audience: audience, API: audience.API, Actx: audience.Actx, ComputeVM: computevm, Claims: claims}, nil
}

func (v *Verifier) VerifyAudience(ctx context.Context, token, rawAudience string) (map[string]any, error) {
	if v == nil || v.Keys == nil || v.Issuer == nil {
		return nil, refused("no verifier")
	}
	token = strings.TrimSpace(token)
	if token == "" {
		return nil, refused("no token")
	}
	if limit := v.MaxTokenBytes; limit > 0 && len(token) > limit {
		return nil, refused("the token is longer than %d bytes", limit)
	}
	audience, err := ParseAudience(rawAudience)
	if err != nil {
		return nil, refused("%v", err)
	}
	issuer := strings.TrimRight(strings.TrimSpace(v.Issuer()), "/")
	if issuer == "" {
		return nil, refused("the verifier names no issuer")
	}
	claims, err := v.parse(ctx, token, issuer, audience)
	if err != nil && (errors.Is(err, jwt.ErrTokenUnverifiable) || errors.Is(err, jwt.ErrTokenSignatureInvalid) || errors.Is(err, errNoKey)) {
		if rerr := v.Refresh(ctx); rerr == nil {
			claims, err = v.parse(ctx, token, issuer, audience)
		}
	}
	if err != nil {
		return nil, refused("token verification failed: %v", err)
	}
	return claims, nil
}

func (v *Verifier) parse(ctx context.Context, token, issuer string, audience Audience) (jwt.MapClaims, error) {
	v.mu.Lock()
	set := v.set
	v.mu.Unlock()
	if set == nil {
		if err := v.Refresh(ctx); err != nil {
			return nil, err
		}
		v.mu.Lock()
		set = v.set
		v.mu.Unlock()
	}
	options := []jwt.ParserOption{
		jwt.WithValidMethods(verificationMethods),
		jwt.WithIssuer(issuer),
		jwt.WithAudience(audience.String()),
		jwt.WithExpirationRequired(),
		jwt.WithLeeway(v.ClockTolerance),
	}
	if v.Now != nil {
		options = append(options, jwt.WithTimeFunc(v.Now))
	}
	parser := jwt.NewParser(options...)
	claims := jwt.MapClaims{}
	parsed, err := parser.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
		key, err := selectKey(set, t)
		if err != nil {
			return nil, err
		}
		if _, ok := key.(*ecdsa.PublicKey); ok && t.Method.Alg() == jwt.SigningMethodES256.Alg() {
			t.Method = jwt.SigningMethodES256
		}
		return key, nil
	})
	if err != nil {
		return nil, err
	}
	if !parsed.Valid {
		return nil, errors.New("the token is not valid")
	}
	return claims, nil
}

func selectKey(set map[string]any, t *jwt.Token) (any, error) {
	kid, _ := t.Header["kid"].(string)
	if key, ok := set[kid]; ok {
		return key, nil
	}
	if kid == "" && len(set) == 1 {
		for _, key := range set {
			return key, nil
		}
	}
	return nil, errNoKey
}

func refused(format string, args ...any) error {
	return fmt.Errorf("%w: %s", ErrTokenRefused, fmt.Sprintf(format, args...))
}

type jwk struct {
	Kty string `json:"kty"`
	Kid string `json:"kid"`
	Alg string `json:"alg"`
	Use string `json:"use"`
	N   string `json:"n"`
	E   string `json:"e"`
	Crv string `json:"crv"`
	X   string `json:"x"`
	Y   string `json:"y"`
}

func parseJWKSet(raw []byte) (map[string]any, error) {
	var set struct {
		Keys []jwk `json:"keys"`
	}
	if err := json.Unmarshal(raw, &set); err != nil {
		return nil, fmt.Errorf("guestidentity: the OIDC keys are not a JWK set: %w", err)
	}
	if len(set.Keys) == 0 {
		return nil, errors.New("guestidentity: the OIDC key set is empty")
	}
	out := map[string]any{}
	for _, k := range set.Keys {
		if k.Use != "" && k.Use != "sig" {
			continue
		}
		if k.Alg != "" && !contains(verificationMethods, k.Alg) {
			continue
		}
		key, err := parseJWK(k)
		if err != nil {
			return nil, err
		}
		if _, dup := out[k.Kid]; dup {
			return nil, fmt.Errorf("guestidentity: the OIDC key set names kid %q twice", k.Kid)
		}
		out[k.Kid] = key
	}
	if len(out) == 0 {
		return nil, errors.New("guestidentity: the OIDC key set holds no signing key these methods verify")
	}
	return out, nil
}

func parseJWK(k jwk) (any, error) {
	switch k.Kty {
	case "RSA":
		n, err := base64.RawURLEncoding.DecodeString(k.N)
		if err != nil || len(n) == 0 {
			return nil, fmt.Errorf("guestidentity: the OIDC RSA key %q carries no modulus", k.Kid)
		}
		e, err := base64.RawURLEncoding.DecodeString(k.E)
		if err != nil || len(e) == 0 || len(e) > 4 {
			return nil, fmt.Errorf("guestidentity: the OIDC RSA key %q carries no exponent", k.Kid)
		}
		exponent := 0
		for _, b := range e {
			exponent = exponent<<8 | int(b)
		}
		if exponent < 3 || exponent%2 == 0 {
			return nil, fmt.Errorf("guestidentity: the OIDC RSA key %q carries exponent %d", k.Kid, exponent)
		}
		modulus := new(big.Int).SetBytes(n)
		if modulus.BitLen() < 2048 {
			return nil, fmt.Errorf("guestidentity: the OIDC RSA key %q is %d bits, under 2048", k.Kid, modulus.BitLen())
		}
		return &rsa.PublicKey{N: modulus, E: exponent}, nil
	case "EC":
		if k.Crv != "P-256" {
			return nil, fmt.Errorf("guestidentity: the OIDC EC key %q names curve %q", k.Kid, k.Crv)
		}
		if k.Alg != "" && k.Alg != "ES256" {
			return nil, fmt.Errorf("guestidentity: the OIDC EC key %q names algorithm %q", k.Kid, k.Alg)
		}
		x, err := base64.RawURLEncoding.DecodeString(k.X)
		if err != nil || len(x) != 32 {
			return nil, fmt.Errorf("guestidentity: the OIDC EC key %q carries no 32-byte x", k.Kid)
		}
		y, err := base64.RawURLEncoding.DecodeString(k.Y)
		if err != nil || len(y) != 32 {
			return nil, fmt.Errorf("guestidentity: the OIDC EC key %q carries no 32-byte y", k.Kid)
		}
		point := &ecdsa.PublicKey{Curve: elliptic.P256(), X: new(big.Int).SetBytes(x), Y: new(big.Int).SetBytes(y)}
		if !point.Curve.IsOnCurve(point.X, point.Y) {
			return nil, fmt.Errorf("guestidentity: the OIDC EC key %q is not on P-256", k.Kid)
		}
		return point, nil
	default:
		return nil, fmt.Errorf("guestidentity: the OIDC key %q has kty %q", k.Kid, k.Kty)
	}
}