Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): handle RSA key rollover #6154

Merged
merged 3 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions engine/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,13 @@ type Configuration struct {
InsecureSkipVerifyTLS bool `toml:"insecureSkipVerifyTLS" json:"insecureSkipVerifyTLS" default:"false"`
} `toml:"internalServiceMesh" json:"internalServiceMesh"`
Auth struct {
TokenDefaultDuration int64 `toml:"tokenDefaultDuration" default:"30" comment:"The default duration of a token (in days)" json:"tokenDefaultDuration"`
TokenOverlapDefaultDuration string `toml:"tokenOverlapDefaultDuration" default:"24h" comment:"The default overlap duration when a token is regen" json:"tokenOverlapDefaultDuration"`
DefaultGroup string `toml:"defaultGroup" default:"" comment:"The default group is the group in which every new user will be granted at signup" json:"defaultGroup"`
DisableAddUserInDefaultGroup bool `toml:"disableAddUserInDefaultGroup" default:"false" comment:"If false, user are automatically added in the default group" json:"disableAddUserInDefaultGroup"`
RSAPrivateKey string `toml:"rsaPrivateKey" default:"" comment:"The RSA Private Key used to sign and verify the JWT Tokens issued by the API \nThis is mandatory." json:"-"`
AllowedOrganizations sdk.StringSlice `toml:"allowedOrganizations" default:"" comment:"The list of allowed organizations for CDS users, let empty to authorize all organizations." json:"allowedOrganizations"`
TokenDefaultDuration int64 `toml:"tokenDefaultDuration" default:"30" comment:"The default duration of a token (in days)" json:"tokenDefaultDuration"`
TokenOverlapDefaultDuration string `toml:"tokenOverlapDefaultDuration" default:"24h" comment:"The default overlap duration when a token is regen" json:"tokenOverlapDefaultDuration"`
DefaultGroup string `toml:"defaultGroup" default:"" comment:"The default group is the group in which every new user will be granted at signup" json:"defaultGroup"`
DisableAddUserInDefaultGroup bool `toml:"disableAddUserInDefaultGroup" default:"false" comment:"If false, user are automatically added in the default group" json:"disableAddUserInDefaultGroup"`
RSAPrivateKey string `toml:"rsaPrivateKey" default:"" comment:"The RSA Private Key used to sign and verify the JWT Tokens issued by the API \nThis is mandatory." json:"-"`
RSAPrivateKeys []authentication.KeyConfig `toml:"rsaPrivateKeys" default:"" comment:"RSA Private Keys used to sign and verify the JWT Tokens issued by the API \nThis is mandatory." json:"-" mapstructure:"rsaPrivateKeys"`
AllowedOrganizations sdk.StringSlice `toml:"allowedOrganizations" default:"" comment:"The list of allowed organizations for CDS users, let empty to authorize all organizations." json:"allowedOrganizations"`
LDAP struct {
Enabled bool `toml:"enabled" default:"false" json:"enabled"`
SignupDisabled bool `toml:"signupDisabled" default:"false" json:"signupDisabled"`
Expand Down Expand Up @@ -363,7 +364,7 @@ func (a *API) CheckConfiguration(config interface{}) error {
return fmt.Errorf("You can't specify just defaultArch without defaultOS in your configuration and vice versa")
}

if aConfig.Auth.RSAPrivateKey == "" {
if aConfig.Auth.RSAPrivateKey == "" && len(aConfig.Auth.RSAPrivateKeys) == 0 {
return errors.New("invalid given authentication rsa private key")
}

Expand Down Expand Up @@ -420,7 +421,17 @@ func (a *API) Serve(ctx context.Context) error {
}

// Initialize the jwt layer
if err := authentication.Init(a.ServiceName, []byte(a.Config.Auth.RSAPrivateKey)); err != nil {
var RSAKeyConfigs []authentication.KeyConfig
if a.Config.Auth.RSAPrivateKey != "" {
RSAKeyConfigs = append(RSAKeyConfigs, authentication.KeyConfig{
Key: a.Config.Auth.RSAPrivateKey,
Timestamp: 0,
})
}
if len(a.Config.Auth.RSAPrivateKeys) > 0 {
RSAKeyConfigs = append(RSAKeyConfigs, a.Config.Auth.RSAPrivateKeys...)
}
if err := authentication.Init(ctx, a.ServiceName, RSAKeyConfigs); err != nil {
return sdk.WrapError(err, "unable to initialize the JWT Layer")
}

Expand Down
57 changes: 47 additions & 10 deletions engine/api/authentication/authentication.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package authentication

import (
"context"
"crypto/rsa"
"sort"
"time"

jwt "github.com/golang-jwt/jwt"
Expand All @@ -10,24 +12,39 @@ import (
)

var (
signer *authentication.Signer
signers []authentication.Signer
)

type KeyConfig struct {
Timestamp int64 `toml:"timestamp" mapstructure:"timestamp"`
Key string `toml:"key" mapstructure:"key"`
}

// Init the package by passing the signing key
func Init(issuer string, k []byte) error {
s, err := authentication.NewSigner(issuer, k)
if err != nil {
return err
func Init(ctx context.Context, issuer string, keys []KeyConfig) error {
// sort the keys to set the most recent signer at the end
sort.Slice(keys, func(i, j int) bool {
return keys[i].Timestamp < keys[j].Timestamp
})

signers = make([]authentication.Signer, len(keys))

for i := range keys {
s, err := authentication.NewSigner(issuer, []byte(keys[i].Key))
if err != nil {
return err
}
signers[i] = s
}
signer = &s

return nil
}

func getSigner() authentication.Signer {
if signer == nil {
if len(signers) == 0 {
panic("signer is not set")
}
return *signer
return signers[len(signers)-1] // return the most recent signer
}

func GetIssuerName() string {
Expand All @@ -43,13 +60,33 @@ func SignJWT(jwtToken *jwt.Token) (string, error) {
}

func VerifyJWT(token *jwt.Token) (interface{}, error) {
return getSigner().VerifyJWT(token)
var lastError error
// Check with the most recent signer first
for i := len(signers) - 1; i >= 0; i-- {
s := signers[i]
res, err := s.VerifyJWT(token)
if err == nil && res != nil {
return res, nil
}
lastError = err
}
return nil, lastError
}

func SignJWS(content interface{}, now time.Time, duration time.Duration) (string, error) {
return getSigner().SignJWS(content, now, duration)
}

func VerifyJWS(signature string, content interface{}) error {
return getSigner().VerifyJWS(signature, content)
var lastError error
// Check with the most recent signer first
for i := len(signers) - 1; i >= 0; i-- {
s := signers[i]
err := s.VerifyJWS(signature, content)
if err == nil {
return nil
}
lastError = err
}
return lastError
}
3 changes: 2 additions & 1 deletion engine/api/test/test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package test

import (
"context"
"testing"

"github.com/rockbears/log"
Expand All @@ -26,7 +27,7 @@ func SetupPGWithFactory(t *testing.T, bootstrapFunc ...test.Bootstrapf) (*test.F
db, factory, cache, cancel := test.SetupPGToCancel(t, gorpmapping.Mapper, sdk.TypeAPI, bootstrapFunc...)
t.Cleanup(cancel)

err := authentication.Init("cds-api-test", test.SigningKey)
err := authentication.Init(context.TODO(), "cds-api-test", []authentication.KeyConfig{{Key: string(test.SigningKey)}})
require.NoError(t, err, "unable to init authentication layer")

return db, factory, cache
Expand Down
24 changes: 20 additions & 4 deletions engine/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"context"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -306,12 +307,17 @@ func configSetStartupData(conf *Configuration) (string, error) {
validityPediod := sdk.NewAuthConsumerValidityPeriod(time.Now(), 0)
startupCfg := api.StartupConfig{IAT: validityPediod.Latest().IssuedAt.Unix()}

if err := authentication.Init("cds-api", apiPrivateKeyPEM); err != nil {
if err := authentication.Init(context.TODO(), "cds-api", []authentication.KeyConfig{{Key: string(apiPrivateKeyPEM)}}); err != nil {
return "", err
}

if conf.API != nil {
conf.API.Auth.RSAPrivateKey = string(apiPrivateKeyPEM)
conf.API.Auth.RSAPrivateKeys = []authentication.KeyConfig{
{
Timestamp: time.Now().Unix(),
Key: string(apiPrivateKeyPEM),
},
}

key, _ := keyloader.GenerateKey("hmac", gorpmapper.KeySignIdentifier, false, time.Now())
conf.API.Database.SignatureKey = database.RollingKeyConfig{Cipher: "hmac"}
Expand Down Expand Up @@ -658,13 +664,23 @@ func getInitTokenFromExistingConfiguration(conf Configuration) (string, error) {
if conf.API == nil {
return "", fmt.Errorf("cannot load configuration")
}
apiPrivateKeyPEM := []byte(conf.API.Auth.RSAPrivateKey)

now := time.Now()
globalIAT := now.Unix()
startupCfg := api.StartupConfig{}

if err := authentication.Init("cds-api", apiPrivateKeyPEM); err != nil {
var RSAKeyConfigs []authentication.KeyConfig
if conf.API.Auth.RSAPrivateKey != "" {
RSAKeyConfigs = append(RSAKeyConfigs, authentication.KeyConfig{
Key: conf.API.Auth.RSAPrivateKey,
Timestamp: 0,
})
}
if len(conf.API.Auth.RSAPrivateKeys) > 0 {
RSAKeyConfigs = append(RSAKeyConfigs, conf.API.Auth.RSAPrivateKeys...)
}

if err := authentication.Init(context.TODO(), "cds-api", RSAKeyConfigs); err != nil {
return "", err
}

Expand Down
2 changes: 1 addition & 1 deletion engine/hatchery/hatchery_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func InitMock(t *testing.T, url string) {
privKeyPEM, _ := jws.ExportPrivateKey(privKey)
pubKey, _ := jws.ExportPublicKey(privKey)

require.NoError(t, authentication.Init("cds-api-test", privKeyPEM))
require.NoError(t, authentication.Init(context.TODO(), "cds-api-test", []authentication.KeyConfig{{Key: string(privKeyPEM)}}))
id := sdk.UUID()
consumerID := sdk.UUID()
hatcheryAuthenticationToken, _ := authentication.NewSessionJWT(&sdk.AuthSession{
Expand Down