mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 05:03:30 +08:00
445 lines
12 KiB
Go
445 lines
12 KiB
Go
/*
|
|
Copyright 2020 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package jwt is used to sign and verify JWT tokens used by application access.
|
|
package jwt
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"gopkg.in/square/go-jose.v2"
|
|
"gopkg.in/square/go-jose.v2/cryptosigner"
|
|
"gopkg.in/square/go-jose.v2/jwt"
|
|
|
|
"github.com/gravitational/teleport/api/constants"
|
|
"github.com/gravitational/teleport/api/types/wrappers"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
// Config defines the clock and PEM encoded bytes of a public and private
|
|
// key that form a *jwt.Key.
|
|
type Config struct {
|
|
// Clock is used to control expiry time.
|
|
Clock clockwork.Clock
|
|
|
|
// PublicKey is used to verify a signed token.
|
|
PublicKey crypto.PublicKey
|
|
|
|
// PrivateKey is used to sign and verify tokens.
|
|
PrivateKey crypto.Signer
|
|
|
|
// Algorithm is algorithm used to sign JWT tokens.
|
|
Algorithm jose.SignatureAlgorithm
|
|
|
|
// ClusterName is the name of the cluster that will be signing the JWT tokens.
|
|
ClusterName string
|
|
}
|
|
|
|
// CheckAndSetDefaults validates the values of a *Config.
|
|
func (c *Config) CheckAndSetDefaults() error {
|
|
if c.Clock == nil {
|
|
c.Clock = clockwork.NewRealClock()
|
|
}
|
|
if c.PrivateKey != nil {
|
|
c.PublicKey = c.PrivateKey.Public()
|
|
}
|
|
|
|
if c.PrivateKey == nil && c.PublicKey == nil {
|
|
return trace.BadParameter("public or private key is required")
|
|
}
|
|
if c.Algorithm == "" {
|
|
return trace.BadParameter("algorithm is required")
|
|
}
|
|
if c.ClusterName == "" {
|
|
return trace.BadParameter("cluster name is required")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Key is a JWT key that can be used to sign and/or verify a token.
|
|
type Key struct {
|
|
config *Config
|
|
}
|
|
|
|
// New creates a JWT key that can be used to sign and verify tokens.
|
|
func New(config *Config) (*Key, error) {
|
|
if err := config.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &Key{
|
|
config: config,
|
|
}, nil
|
|
}
|
|
|
|
// SignParams are the claims to be embedded within the JWT token.
|
|
type SignParams struct {
|
|
// Username is the Teleport identity.
|
|
Username string
|
|
|
|
// Roles are the roles assigned to the user within Teleport.
|
|
Roles []string
|
|
|
|
// Traits are the traits assigned to the user within Teleport.
|
|
Traits wrappers.Traits
|
|
|
|
// Expiry is time to live for the token.
|
|
Expires time.Time
|
|
|
|
// URI is the URI of the recipient application.
|
|
URI string
|
|
}
|
|
|
|
// Check verifies all the values are valid.
|
|
func (p *SignParams) Check() error {
|
|
if p.Username == "" {
|
|
return trace.BadParameter("username missing")
|
|
}
|
|
if len(p.Roles) == 0 {
|
|
return trace.BadParameter("roles missing")
|
|
}
|
|
if p.Expires.IsZero() {
|
|
return trace.BadParameter("expires missing")
|
|
}
|
|
if p.URI == "" {
|
|
return trace.BadParameter("uri missing")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// sign will return a signed JWT with the passed in claims embedded within.
|
|
func (k *Key) sign(claims Claims) (string, error) {
|
|
return k.signAny(claims)
|
|
}
|
|
|
|
// signAny will return a signed JWT with the passed in claims embedded within; unlike sign it allows more flexibility in the claim data.
|
|
func (k *Key) signAny(claims any) (string, error) {
|
|
if k.config.PrivateKey == nil {
|
|
return "", trace.BadParameter("can not sign token with non-signing key")
|
|
}
|
|
|
|
// Create a signer with configured private key and algorithm.
|
|
var signer interface{}
|
|
switch k.config.PrivateKey.(type) {
|
|
case *rsa.PrivateKey:
|
|
signer = k.config.PrivateKey
|
|
default:
|
|
signer = cryptosigner.Opaque(k.config.PrivateKey)
|
|
}
|
|
signingKey := jose.SigningKey{
|
|
Algorithm: k.config.Algorithm,
|
|
Key: signer,
|
|
}
|
|
sig, err := jose.NewSigner(signingKey, (&jose.SignerOptions{}).WithType("JWT"))
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
|
|
token, err := jwt.Signed(sig).Claims(claims).CompactSerialize()
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
func (k *Key) Sign(p SignParams) (string, error) {
|
|
if err := p.Check(); err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
|
|
// Sign the claims and create a JWT token.
|
|
claims := Claims{
|
|
Claims: jwt.Claims{
|
|
Subject: p.Username,
|
|
Issuer: k.config.ClusterName,
|
|
Audience: jwt.Audience{p.URI},
|
|
NotBefore: jwt.NewNumericDate(k.config.Clock.Now().Add(-10 * time.Second)),
|
|
IssuedAt: jwt.NewNumericDate(k.config.Clock.Now()),
|
|
Expiry: jwt.NewNumericDate(p.Expires),
|
|
},
|
|
Username: p.Username,
|
|
Roles: p.Roles,
|
|
Traits: p.Traits,
|
|
}
|
|
|
|
return k.sign(claims)
|
|
}
|
|
|
|
func (k *Key) SignSnowflake(p SignParams, issuer string) (string, error) {
|
|
// Sign the claims and create a JWT token.
|
|
claims := Claims{
|
|
Claims: jwt.Claims{
|
|
Subject: p.Username,
|
|
Issuer: issuer,
|
|
NotBefore: jwt.NewNumericDate(k.config.Clock.Now().Add(-10 * time.Second)),
|
|
Expiry: jwt.NewNumericDate(p.Expires),
|
|
IssuedAt: jwt.NewNumericDate(k.config.Clock.Now().Add(-10 * time.Second)),
|
|
},
|
|
}
|
|
|
|
return k.sign(claims)
|
|
}
|
|
|
|
// AzureTokenClaims represent a minimal set of claims that will be encoded as JWT in Azure access token and passed back to az CLI.
|
|
type AzureTokenClaims struct {
|
|
// TenantID represents TenantID; this is read by az CLI.
|
|
TenantID string `json:"tid"`
|
|
// Resource records the resource requested by az CLI. This will be used in backend to request real token with appropriate scope.
|
|
Resource string `json:"resource"`
|
|
}
|
|
|
|
// SignAzureToken signs AzureTokenClaims
|
|
func (k *Key) SignAzureToken(claims AzureTokenClaims) (string, error) {
|
|
return k.signAny(claims)
|
|
}
|
|
|
|
type PROXYSignParams struct {
|
|
ClusterName string
|
|
SourceAddress string
|
|
DestinationAddress string
|
|
}
|
|
|
|
const expirationPROXY = time.Second * 60
|
|
|
|
// SignPROXYJwt will create short lived signed JWT that is used in signed PROXY header
|
|
func (k *Key) SignPROXYJWT(p PROXYSignParams) (string, error) {
|
|
claims := Claims{
|
|
Claims: jwt.Claims{
|
|
Subject: p.SourceAddress,
|
|
Audience: []string{p.DestinationAddress},
|
|
Issuer: p.ClusterName,
|
|
NotBefore: jwt.NewNumericDate(k.config.Clock.Now().Add(-10 * time.Second)),
|
|
Expiry: jwt.NewNumericDate(k.config.Clock.Now().Add(expirationPROXY)),
|
|
IssuedAt: jwt.NewNumericDate(k.config.Clock.Now()),
|
|
},
|
|
}
|
|
|
|
return k.sign(claims)
|
|
}
|
|
|
|
// VerifyParams are the parameters needed to pass the token and data needed to verify.
|
|
type VerifyParams struct {
|
|
// Username is the Teleport identity.
|
|
Username string
|
|
|
|
// RawToken is the JWT token.
|
|
RawToken string
|
|
|
|
// URI is the URI of the recipient application.
|
|
URI string
|
|
}
|
|
|
|
// Check verifies all the values are valid.
|
|
func (p *VerifyParams) Check() error {
|
|
if p.Username == "" {
|
|
return trace.BadParameter("username missing")
|
|
}
|
|
if p.RawToken == "" {
|
|
return trace.BadParameter("raw token missing")
|
|
}
|
|
if p.URI == "" {
|
|
return trace.BadParameter("uri missing")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type SnowflakeVerifyParams struct {
|
|
AccountName string
|
|
LoginName string
|
|
RawToken string
|
|
}
|
|
|
|
func (p *SnowflakeVerifyParams) Check() error {
|
|
if p.AccountName == "" {
|
|
return trace.BadParameter("account name missing")
|
|
}
|
|
|
|
if p.LoginName == "" {
|
|
return trace.BadParameter("login name is missing")
|
|
}
|
|
|
|
if p.RawToken == "" {
|
|
return trace.BadParameter("raw token missing")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type PROXYVerifyParams struct {
|
|
ClusterName string
|
|
SourceAddress string
|
|
DestinationAddress string
|
|
RawToken string
|
|
}
|
|
|
|
func (p *PROXYVerifyParams) Check() error {
|
|
if p.ClusterName == "" {
|
|
return trace.BadParameter("cluster name missing")
|
|
}
|
|
if p.SourceAddress == "" {
|
|
return trace.BadParameter("source address missing")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (k *Key) verify(rawToken string, expectedClaims jwt.Expected) (*Claims, error) {
|
|
if k.config.PublicKey == nil {
|
|
return nil, trace.BadParameter("can not verify token without public key")
|
|
}
|
|
// Parse the token.
|
|
tok, err := jwt.ParseSigned(rawToken)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Validate the signature on the JWT token.
|
|
var out Claims
|
|
if err := tok.Claims(k.config.PublicKey, &out); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Validate the claims on the JWT token.
|
|
if err = out.Validate(expectedClaims); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
// Verify will validate the passed in JWT token.
|
|
func (k *Key) Verify(p VerifyParams) (*Claims, error) {
|
|
if err := p.Check(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
expectedClaims := jwt.Expected{
|
|
Issuer: k.config.ClusterName,
|
|
Subject: p.Username,
|
|
Audience: jwt.Audience{p.URI},
|
|
Time: k.config.Clock.Now(),
|
|
}
|
|
|
|
return k.verify(p.RawToken, expectedClaims)
|
|
}
|
|
|
|
// VerifyPROXY will validate the passed JWT for signed PROXY header
|
|
func (k *Key) VerifyPROXY(p PROXYVerifyParams) (*Claims, error) {
|
|
if err := p.Check(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
expectedClaims := jwt.Expected{
|
|
Issuer: p.ClusterName,
|
|
Subject: p.SourceAddress,
|
|
Audience: []string{p.DestinationAddress},
|
|
Time: k.config.Clock.Now(),
|
|
}
|
|
|
|
return k.verify(p.RawToken, expectedClaims)
|
|
}
|
|
|
|
// VerifySnowflake will validate the passed in JWT token.
|
|
func (k *Key) VerifySnowflake(p SnowflakeVerifyParams) (*Claims, error) {
|
|
if err := p.Check(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
pubKey, err := x509.MarshalPKIXPublicKey(k.config.PublicKey)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
keyFp := sha256.Sum256(pubKey)
|
|
keyFpStr := base64.StdEncoding.EncodeToString(keyFp[:])
|
|
|
|
accName := strings.ToUpper(p.AccountName)
|
|
loginName := strings.ToUpper(p.LoginName)
|
|
|
|
// Generate issuer name in the Snowflake required format.
|
|
issuer := fmt.Sprintf("%s.%s.SHA256:%s", accName, loginName, keyFpStr)
|
|
|
|
// Validate the claims on the JWT token.
|
|
expectedClaims := jwt.Expected{
|
|
Issuer: issuer,
|
|
Subject: fmt.Sprintf("%s.%s", accName, loginName),
|
|
Time: k.config.Clock.Now(),
|
|
}
|
|
return k.verify(p.RawToken, expectedClaims)
|
|
}
|
|
|
|
func (k *Key) VerifyAzureToken(rawToken string) (*AzureTokenClaims, error) {
|
|
if k.config.PublicKey == nil {
|
|
return nil, trace.BadParameter("can not verify token without public key")
|
|
}
|
|
// Parse the token.
|
|
tok, err := jwt.ParseSigned(rawToken)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Validate the signature on the JWT token.
|
|
var out AzureTokenClaims
|
|
if err := tok.Claims(k.config.PublicKey, &out); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
// Claims represents public and private claims for a JWT token.
|
|
type Claims struct {
|
|
// Claims represents public claim values (as specified in RFC 7519).
|
|
jwt.Claims
|
|
|
|
// Username returns the Teleport identity of the user.
|
|
Username string `json:"username"`
|
|
|
|
// Roles returns the list of roles assigned to the user within Teleport.
|
|
Roles []string `json:"roles"`
|
|
|
|
// Traits returns the traits assigned to the user within Teleport.
|
|
Traits wrappers.Traits `json:"traits"`
|
|
}
|
|
|
|
// GenerateKeyPair generates and return a PEM encoded private and public
|
|
// key in the format used by this package.
|
|
func GenerateKeyPair() ([]byte, []byte, error) {
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, constants.RSAKeySize)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
public, private, err := utils.MarshalPrivateKey(privateKey)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
return public, private, nil
|
|
}
|