Skip to content

Commit

Permalink
feat(api): handle RSA key rollover (#6154)
Browse files Browse the repository at this point in the history
* feat(api): handler RSA key rollover

Signed-off-by: francois  samin <[email protected]>
  • Loading branch information
fsamin authored Apr 26, 2022
1 parent 156c3cf commit a28b9d2
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 24 deletions.
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

0 comments on commit a28b9d2

Please sign in to comment.