Skip to content

Commit

Permalink
feat(kuma-cp) user token with RSA256 (#2992)
Browse files Browse the repository at this point in the history
Signed-off-by: Jakub Dyszkiewicz <jakub.dyszkiewicz@gmail.com>
  • Loading branch information
jakubdyszkiewicz authored Oct 28, 2021
1 parent b805788 commit 4c0f561
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 95 deletions.
3 changes: 0 additions & 3 deletions pkg/plugins/authn/api-server/tokens/admin_token_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

kuma_cp "github.com/kumahq/kuma/pkg/config/app/kuma-cp"
config_core "github.com/kumahq/kuma/pkg/config/core"
"github.com/kumahq/kuma/pkg/core"
"github.com/kumahq/kuma/pkg/core/resources/apis/system"
"github.com/kumahq/kuma/pkg/core/resources/manager"
"github.com/kumahq/kuma/pkg/core/resources/model"
Expand All @@ -21,8 +20,6 @@ import (
"github.com/kumahq/kuma/pkg/plugins/authn/api-server/tokens/issuer"
)

var log = core.Log.WithName("plugins").WithName("authn").WithName("tokens")

var AdminTokenKey = model.ResourceKey{
Name: "admin-user-token",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ var _ = Describe("Admin Token Bootstrap", func() {
// given
resManager := manager.NewResourceManager(memory.NewStore())
signingKeyManager := issuer.NewSigningKeyManager(resManager)
tokenIssuer := issuer.NewUserTokenIssuer(signingKeyManager, issuer.NewTokenRevocations(resManager))
tokenIssuer := issuer.NewUserTokenIssuer(signingKeyManager)
tokenValidator := issuer.NewUserTokenValidator(issuer.NewSigningKeyAccessor(resManager), issuer.NewTokenRevocations(resManager))
component := tokens.NewAdminTokenBootstrap(tokenIssuer, resManager, kuma_cp.DefaultConfig())
err := signingKeyManager.CreateDefaultSigningKey()
Expect(err).ToNot(HaveOccurred())
Expand All @@ -37,7 +38,7 @@ var _ = Describe("Admin Token Bootstrap", func() {
globalSecret := system.NewGlobalSecretResource()
err = resManager.Get(context.Background(), globalSecret, core_store.GetBy(tokens.AdminTokenKey))
g.Expect(err).ToNot(HaveOccurred())
user, err := tokenIssuer.Validate(string(globalSecret.Spec.Data.Value))
user, err := tokenValidator.Validate(string(globalSecret.Spec.Data.Value))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(user.Name).To(Equal("mesh-system:admin"))
g.Expect(user.Groups).To(Equal([]string{"mesh-system:admin"}))
Expand Down
10 changes: 7 additions & 3 deletions pkg/plugins/authn/api-server/tokens/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,27 @@ import (
"github.com/emicklei/go-restful"

"github.com/kumahq/kuma/pkg/api-server/authn"
"github.com/kumahq/kuma/pkg/core"
rest_errors "github.com/kumahq/kuma/pkg/core/rest/errors"
"github.com/kumahq/kuma/pkg/core/user"
"github.com/kumahq/kuma/pkg/plugins/authn/api-server/tokens/issuer"
)

const bearerPrefix = "Bearer "

func UserTokenAuthenticator(issuer issuer.UserTokenIssuer) authn.Authenticator {
var log = core.Log.WithName("plugins").WithName("authn").WithName("api-server").WithName("tokens")

func UserTokenAuthenticator(validator issuer.UserTokenValidator) authn.Authenticator {
return func(request *restful.Request, response *restful.Response, chain *restful.FilterChain) {
authnHeader := request.Request.Header.Get("authorization")
if user.FromCtx(request.Request.Context()).Name == user.Anonymous.Name && // do not overwrite existing user
authnHeader != "" &&
strings.HasPrefix(authnHeader, bearerPrefix) {
token := strings.TrimPrefix(authnHeader, bearerPrefix)
u, err := issuer.Validate(token)
u, err := validator.Validate(token)
if err != nil {
rest_errors.HandleError(response, &rest_errors.Unauthenticated{}, "invalid authentication data: "+err.Error())
rest_errors.HandleError(response, &rest_errors.Unauthenticated{}, "Invalid authentication data")
log.Info("authentication rejected", "reason", err.Error())
return
}
request.Request = request.Request.WithContext(user.Ctx(request.Request.Context(), u.Authenticated()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (d *defaultSigningKeyComponent) createDefaultSigningKeyIfNotExist() error {
log.V(1).Info("user token's signing key already exists. Skip creating.")
return nil
}
if err != SigningKeyNotFound {
if _, ok := err.(*SigningKeyNotFound); !ok {
return err
}
log.Info("trying to create user token's signing key")
Expand Down
42 changes: 2 additions & 40 deletions pkg/plugins/authn/api-server/tokens/issuer/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const KeyIDHeader = "kid" // standard JWT header that indicates which signing ke

type UserTokenIssuer interface {
Generate(identity user.User, validFor time.Duration) (Token, error)
Validate(token Token) (user.User, error)
}

// jwtTokenIssuer generates and validates User Tokens.
Expand All @@ -27,13 +26,11 @@ type UserTokenIssuer interface {
// A new token is always generated by using the latest signing key.
type jwtTokenIssuer struct {
signingKeyManager SigningKeyManager
revocations TokenRevocations
}

func NewUserTokenIssuer(signingKeyAccessor SigningKeyManager, revocations TokenRevocations) UserTokenIssuer {
func NewUserTokenIssuer(signingKeyAccessor SigningKeyManager) UserTokenIssuer {
return &jwtTokenIssuer{
signingKeyManager: signingKeyAccessor,
revocations: revocations,
}
}

Expand Down Expand Up @@ -61,46 +58,11 @@ func (j *jwtTokenIssuer) Generate(identity user.User, validFor time.Duration) (T
},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
token := jwt.NewWithClaims(jwt.SigningMethodRS256, c)
token.Header[KeyIDHeader] = strconv.Itoa(serialNumber)
tokenString, err := token.SignedString(signingKey)
if err != nil {
return "", errors.Wrap(err, "could not sign a token")
}
return tokenString, nil
}

func (j *jwtTokenIssuer) Validate(rawToken Token) (user.User, error) {
c := &claims{}
token, err := jwt.ParseWithClaims(rawToken, c, func(token *jwt.Token) (interface{}, error) {
serialNumberRaw := token.Header["kid"]
if serialNumberRaw == nil {
return nil, errors.New("kid header not found")
}
serialNumber, err := strconv.Atoi(serialNumberRaw.(string))
if err != nil {
return nil, err
}
signingKey, err := j.signingKeyManager.GetSigningKey(serialNumber)
if err != nil {
return nil, errors.Wrapf(err, "could not get signing key with serial number %d. The signing key most likely has been rotated, regenerate the token", serialNumber)
}
return signingKey, nil
})
if err != nil {
return user.User{}, errors.Wrap(err, "could not parse token")
}
if !token.Valid {
return user.User{}, errors.New("token is not valid")
}

revoked, err := j.revocations.IsRevoked(c.ID)
if err != nil {
return user.User{}, errors.Wrap(err, "could not check if the token is revoked")
}
if revoked {
return user.User{}, errors.New("token is revoked")
}

return c.User, nil
}
22 changes: 12 additions & 10 deletions pkg/plugins/authn/api-server/tokens/issuer/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
var _ = Describe("User token issuer", func() {

var issuer UserTokenIssuer
var validator UserTokenValidator
var store core_store.ResourceStore
var signingKeyManager SigningKeyManager

Expand All @@ -33,7 +34,8 @@ var _ = Describe("User token issuer", func() {
store = memory.NewStore()
secretManager := secret_manager.NewGlobalSecretManager(secret_store.NewSecretStore(store), cipher.None())
signingKeyManager = NewSigningKeyManager(secretManager)
issuer = NewUserTokenIssuer(signingKeyManager, NewTokenRevocations(secretManager))
issuer = NewUserTokenIssuer(signingKeyManager)
validator = NewUserTokenValidator(NewSigningKeyAccessor(secretManager), NewTokenRevocations(secretManager))

Expect(signingKeyManager.CreateDefaultSigningKey()).To(Succeed())
core.Now = func() time.Time {
Expand Down Expand Up @@ -61,7 +63,7 @@ var _ = Describe("User token issuer", func() {
Expect(err).ToNot(HaveOccurred())

// then
_, err = issuer.Validate(token1)
_, err = validator.Validate(token1)
Expect(err).ToNot(HaveOccurred())

// when new signing key with higher serial number is created
Expand All @@ -73,21 +75,21 @@ var _ = Describe("User token issuer", func() {
Expect(err).ToNot(HaveOccurred())

// then all tokens are valid because 2 signing keys are present in the system
_, err = issuer.Validate(token1)
_, err = validator.Validate(token1)
Expect(err).ToNot(HaveOccurred())
_, err = issuer.Validate(token2)
_, err = validator.Validate(token2)
Expect(err).ToNot(HaveOccurred())

// when first signing key is deleted
err = store.Delete(context.Background(), system.NewGlobalSecretResource(), core_store.DeleteBy(SigningKeyResourceKey(DefaultSerialNumber)))
Expect(err).ToNot(HaveOccurred())

// then old tokens are no longer valid
_, err = issuer.Validate(token1)
Expect(err).To(MatchError("could not parse token: could not get signing key with serial number 1. The signing key most likely has been rotated, regenerate the token: there is no signing key in the Control Plane"))
_, err = validator.Validate(token1)
Expect(err).To(MatchError(`there is no signing key with serial number 1. GlobalSecret of name "user-token-signing-key-1" is not found. If signing key was rotated, regenerate the token`))

// and new token is valid because new signing key is present
_, err = issuer.Validate(token2)
_, err = validator.Validate(token2)
Expect(err).ToNot(HaveOccurred())
})

Expand All @@ -102,7 +104,7 @@ var _ = Describe("User token issuer", func() {

// when
now = now.Add(60*time.Second + 1*time.Second)
_, err = issuer.Validate(token)
_, err = validator.Validate(token)

// then
Expect(err.Error()).To(ContainSubstring("could not parse token: token is expired"))
Expand All @@ -117,7 +119,7 @@ var _ = Describe("User token issuer", func() {

token, err := issuer.Generate(id, 60*time.Second)
Expect(err).ToNot(HaveOccurred())
_, err = issuer.Validate(token)
_, err = validator.Validate(token)
Expect(err).ToNot(HaveOccurred())

// when id of the token is added to revocation list
Expand All @@ -136,7 +138,7 @@ var _ = Describe("User token issuer", func() {
Expect(err).ToNot(HaveOccurred())

// then
_, err = issuer.Validate(token)
_, err = validator.Validate(token)
Expect(err).To(MatchError("token is revoked"))
})
})
51 changes: 28 additions & 23 deletions pkg/plugins/authn/api-server/tokens/issuer/signing_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package issuer

import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"sort"
"strconv"
Expand All @@ -15,34 +17,36 @@ import (
"github.com/kumahq/kuma/pkg/core/resources/manager"
"github.com/kumahq/kuma/pkg/core/resources/model"
"github.com/kumahq/kuma/pkg/core/resources/store"
"github.com/kumahq/kuma/pkg/tokens/builtin/issuer"
util_rsa "github.com/kumahq/kuma/pkg/util/rsa"
)

const (
defaultRsaBits = 2048
signingKeyPrefix = "user-token-signing-key-"

DefaultSerialNumber = 1
)

var SigningKeyNotFound = errors.New("there is no signing key in the Control Plane")
type SigningKeyNotFound struct {
SerialNumber int
}

func (s *SigningKeyNotFound) Error() string {
return fmt.Sprintf("there is no signing key with serial number %d. GlobalSecret of name %q is not found. If signing key was rotated, regenerate the token", s.SerialNumber, SigningKeyResourceKey(s.SerialNumber).Name)
}

func SigningKeyResourceKey(serialNumber int) model.ResourceKey {
return model.ResourceKey{
Name: fmt.Sprintf("%s%d", signingKeyPrefix, serialNumber),
}
}

func IsSigningKeyResource(resKey model.ResourceKey) bool {
return strings.HasPrefix(resKey.Name, signingKeyPrefix) && resKey.Mesh == ""
}

// SigningKeyManager manages User Token's signing keys.
// We can have many signing keys in the system.
// Example: "user-token-signing-key-1", "user-token-signing-key-2" etc.
// The latest key is a key with a higher serial number (number at the end of the name)
type SigningKeyManager interface {
GetSigningKey(serialNumber int) ([]byte, error)
GetLatestSigningKey() ([]byte, int, error)
GetLatestSigningKey() (*rsa.PrivateKey, int, error)
CreateDefaultSigningKey() error
CreateSigningKey(serialNumber int) error
}
Expand All @@ -59,18 +63,7 @@ type signingKeyManager struct {

var _ SigningKeyManager = &signingKeyManager{}

func (s *signingKeyManager) GetSigningKey(serialNumber int) ([]byte, error) {
resource := system.NewGlobalSecretResource()
if err := s.manager.Get(context.Background(), resource, store.GetBy(SigningKeyResourceKey(serialNumber))); err != nil {
if store.IsResourceNotFound(err) {
return nil, SigningKeyNotFound
}
return nil, errors.Wrap(err, "could not retrieve signing key from secret manager")
}
return resource.Spec.GetData().GetValue(), nil
}

func (s *signingKeyManager) GetLatestSigningKey() ([]byte, int, error) {
func (s *signingKeyManager) GetLatestSigningKey() (*rsa.PrivateKey, int, error) {
resources := system.GlobalSecretResourceList{}
if err := s.manager.List(context.Background(), &resources); err != nil {
return nil, 0, errors.Wrap(err, "could not retrieve signing key from secret manager")
Expand All @@ -84,7 +77,7 @@ func (s *signingKeyManager) GetLatestSigningKey() ([]byte, int, error) {
}

if len(signingKeys) == 0 {
return nil, 0, SigningKeyNotFound
return nil, 0, &SigningKeyNotFound{SerialNumber: DefaultSerialNumber}
}

sort.Stable(GlobalSecretsBySerial(signingKeys))
Expand All @@ -94,15 +87,19 @@ func (s *signingKeyManager) GetLatestSigningKey() ([]byte, int, error) {
return nil, 0, err
}

return signingKeys[0].Spec.GetData().GetValue(), serialNumber, nil
key, err := util_rsa.FromPEMBytes(signingKeys[0].Spec.GetData().GetValue())
if err != nil {
return nil, 0, err
}
return key, serialNumber, nil
}

func (s *signingKeyManager) CreateDefaultSigningKey() error {
return s.CreateSigningKey(DefaultSerialNumber)
}

func (s *signingKeyManager) CreateSigningKey(serialNumber int) error {
key, err := issuer.NewSigningKey()
key, err := NewSigningKey()
if err != nil {
return errors.Wrap(err, "could not construct signing key")
}
Expand All @@ -128,6 +125,14 @@ func signingKeySerialNumber(secretName string) (int, error) {
return serialNumber, nil
}

func NewSigningKey() ([]byte, error) {
key, err := rsa.GenerateKey(rand.Reader, defaultRsaBits)
if err != nil {
return nil, errors.Wrap(err, "failed to generate RSA key")
}
return util_rsa.ToPEMBytes(key)
}

type GlobalSecretsBySerial []*system.GlobalSecretResource

func (a GlobalSecretsBySerial) Len() int { return len(a) }
Expand Down
47 changes: 47 additions & 0 deletions pkg/plugins/authn/api-server/tokens/issuer/signing_key_accessor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package issuer

import (
"context"
"crypto/rsa"

"github.com/pkg/errors"

"github.com/kumahq/kuma/pkg/core/resources/apis/system"
"github.com/kumahq/kuma/pkg/core/resources/manager"
"github.com/kumahq/kuma/pkg/core/resources/store"
util_rsa "github.com/kumahq/kuma/pkg/util/rsa"
)

type SigningKeyAccessor interface {
GetSigningPublicKey(serialNumber int) (*rsa.PublicKey, error)
}

type signingKeyAccessor struct {
resManager manager.ResourceManager
}

var _ SigningKeyAccessor = &signingKeyAccessor{}

func NewSigningKeyAccessor(resManager manager.ResourceManager) SigningKeyAccessor {
return &signingKeyAccessor{
resManager: resManager,
}
}

func (s *signingKeyAccessor) GetSigningPublicKey(serialNumber int) (*rsa.PublicKey, error) {
resource := system.NewGlobalSecretResource()
if err := s.resManager.Get(context.Background(), resource, store.GetBy(SigningKeyResourceKey(serialNumber))); err != nil {
if store.IsResourceNotFound(err) {
return nil, &SigningKeyNotFound{
SerialNumber: serialNumber,
}
}
return nil, errors.Wrap(err, "could not retrieve signing key")
}

key, err := util_rsa.FromPEMBytes(resource.Spec.GetData().GetValue())
if err != nil {
return nil, err
}
return &key.PublicKey, nil
}
Loading

0 comments on commit 4c0f561

Please sign in to comment.