| 1 | package auth |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "strings" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/decred/dcrd/dcrec/secp256k1/v4" |
| 9 | jose "github.com/go-jose/go-jose/v4" |
| 10 | "github.com/go-jose/go-jose/v4/jwt" |
| 11 | |
| 12 | "github.com/gosuda/portal-tunnel/v2/portal/identity" |
| 13 | "github.com/gosuda/portal-tunnel/v2/types" |
| 14 | "github.com/gosuda/portal-tunnel/v2/utils" |
| 15 | ) |
| 16 | |
| 17 | const ( |
| 18 | leaseAccessTokenAudience = "portal-sdk" |
| 19 | leaseTokenAlgorithm = jose.SignatureAlgorithm("ES256K") |
| 20 | ) |
| 21 | |
| 22 | type LeaseAccessTokenClaims struct { |
| 23 | jwt.Claims |
| 24 | Identity types.Identity `json:"identity"` |
| 25 | } |
| 26 | |
| 27 | type es256kOpaqueSigner struct { |
| 28 | authority identity.Authority |
| 29 | } |
| 30 | |
| 31 | func (s *es256kOpaqueSigner) Public() *jose.JSONWebKey { |
| 32 | return &jose.JSONWebKey{} |
| 33 | } |
| 34 | |
| 35 | func (s *es256kOpaqueSigner) Algs() []jose.SignatureAlgorithm { |
| 36 | return []jose.SignatureAlgorithm{leaseTokenAlgorithm} |
| 37 | } |
| 38 | |
| 39 | func (s *es256kOpaqueSigner) SignPayload(payload []byte, alg jose.SignatureAlgorithm) ([]byte, error) { |
| 40 | if alg != leaseTokenAlgorithm { |
| 41 | return nil, jose.ErrUnsupportedAlgorithm |
| 42 | } |
| 43 | if s == nil || s.authority == nil { |
| 44 | return nil, errors.New("signing key is required") |
| 45 | } |
| 46 | signature, err := s.authority.SignSHA256Secp256k1(payload) |
| 47 | if err != nil { |
| 48 | return nil, err |
| 49 | } |
| 50 | return signature.Raw64() |
| 51 | } |
| 52 | |
| 53 | type es256kOpaqueVerifier struct { |
| 54 | publicKey *secp256k1.PublicKey |
| 55 | } |
| 56 | |
| 57 | func (v *es256kOpaqueVerifier) VerifyPayload(payload []byte, signature []byte, alg jose.SignatureAlgorithm) error { |
| 58 | if alg != leaseTokenAlgorithm { |
| 59 | return jose.ErrUnsupportedAlgorithm |
| 60 | } |
| 61 | if v == nil || v.publicKey == nil { |
| 62 | return errors.New("verification key is required") |
| 63 | } |
| 64 | if err := identity.VerifySHA256Secp256k1Raw64(payload, signature, v.publicKey); err != nil { |
| 65 | if errors.Is(err, identity.ErrSecp256k1SignatureInvalid) { |
| 66 | return errors.New("token signature is invalid") |
| 67 | } |
| 68 | return err |
| 69 | } |
| 70 | return nil |
| 71 | } |
| 72 | |
| 73 | func IssueLeaseAccessToken(authority identity.Authority, issuer string, leaseIdentity types.Identity, ttl time.Duration) (string, LeaseAccessTokenClaims, error) { |
| 74 | if authority == nil { |
| 75 | return "", LeaseAccessTokenClaims{}, errors.New("lease token signing authority is required") |
| 76 | } |
| 77 | normalizedIdentity, err := identity.NormalizeIdentity(leaseIdentity) |
| 78 | if err != nil { |
| 79 | return "", LeaseAccessTokenClaims{}, err |
| 80 | } |
| 81 | |
| 82 | signer, err := jose.NewSigner(jose.SigningKey{ |
| 83 | Algorithm: leaseTokenAlgorithm, |
| 84 | Key: &es256kOpaqueSigner{ |
| 85 | authority: authority, |
| 86 | }, |
| 87 | }, (&jose.SignerOptions{}).WithType("JWT")) |
| 88 | if err != nil { |
| 89 | return "", LeaseAccessTokenClaims{}, err |
| 90 | } |
| 91 | |
| 92 | now := time.Now().UTC() |
| 93 | expiresAt := now.Add(ttl) |
| 94 | claims := LeaseAccessTokenClaims{ |
| 95 | Claims: jwt.Claims{ |
| 96 | Issuer: strings.TrimSpace(issuer), |
| 97 | Subject: normalizedIdentity.Key(), |
| 98 | Audience: jwt.Audience{leaseAccessTokenAudience}, |
| 99 | ID: utils.RandomID("tok_"), |
| 100 | IssuedAt: jwt.NewNumericDate(now), |
| 101 | NotBefore: jwt.NewNumericDate(now), |
| 102 | Expiry: jwt.NewNumericDate(expiresAt), |
| 103 | }, |
| 104 | Identity: normalizedIdentity, |
| 105 | } |
| 106 | |
| 107 | token, err := jwt.Signed(signer).Claims(claims).Serialize() |
| 108 | if err != nil { |
| 109 | return "", LeaseAccessTokenClaims{}, err |
| 110 | } |
| 111 | return token, claims, nil |
| 112 | } |
| 113 | |
| 114 | func VerifyLeaseAccessToken(token, publicKeyHex, issuer string, now time.Time) (LeaseAccessTokenClaims, error) { |
| 115 | publicKey, err := identity.ParseSecp256k1PublicKeyHex(publicKeyHex) |
| 116 | if err != nil { |
| 117 | return LeaseAccessTokenClaims{}, err |
| 118 | } |
| 119 | |
| 120 | parsed, err := jwt.ParseSigned(strings.TrimSpace(token), []jose.SignatureAlgorithm{leaseTokenAlgorithm}) |
| 121 | if err != nil { |
| 122 | return LeaseAccessTokenClaims{}, err |
| 123 | } |
| 124 | |
| 125 | var claims LeaseAccessTokenClaims |
| 126 | if err := parsed.Claims(&es256kOpaqueVerifier{publicKey: publicKey}, &claims); err != nil { |
| 127 | return LeaseAccessTokenClaims{}, err |
| 128 | } |
| 129 | normalizedClaimsIdentity, err := identity.NormalizeIdentity(claims.Identity) |
| 130 | if err != nil { |
| 131 | return LeaseAccessTokenClaims{}, err |
| 132 | } |
| 133 | if normalizedClaimsIdentity.Key() != claims.Subject { |
| 134 | return LeaseAccessTokenClaims{}, errors.New("lease access token identity does not match subject") |
| 135 | } |
| 136 | claims.Identity = normalizedClaimsIdentity |
| 137 | if err := claims.ValidateWithLeeway(jwt.Expected{ |
| 138 | Issuer: strings.TrimSpace(issuer), |
| 139 | AnyAudience: jwt.Audience{leaseAccessTokenAudience}, |
| 140 | Time: now.UTC(), |
| 141 | }, 0); err != nil { |
| 142 | return LeaseAccessTokenClaims{}, err |
| 143 | } |
| 144 | return claims, nil |
| 145 | } |