diff --git a/cmd/zitadel/system-defaults.yaml b/cmd/zitadel/system-defaults.yaml index 23aefaa57..b8938827f 100644 --- a/cmd/zitadel/system-defaults.yaml +++ b/cmd/zitadel/system-defaults.yaml @@ -137,4 +137,5 @@ SystemDefaults: PublicKeyLifetime: 30h EncryptionConfig: EncryptionKeyID: $ZITADEL_OIDC_KEYS_ID - SigningKeyRotation: 10s \ No newline at end of file + SigningKeyRotationCheck: 10s + SigningKeyGracefulPeriod: 10m \ No newline at end of file diff --git a/go.mod b/go.mod index baa42a005..e8f4c9fc4 100644 --- a/go.mod +++ b/go.mod @@ -16,14 +16,14 @@ require ( github.com/allegro/bigcache v1.2.1 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc github.com/caos/logging v0.0.2 - github.com/caos/oidc v0.14.0 + github.com/caos/oidc v0.14.1 github.com/caos/orbos v1.5.14-0.20210205131708-6dc812182dc0 github.com/cockroachdb/cockroach-go/v2 v2.1.0 github.com/duo-labs/webauthn v0.0.0-20200714211715-1daaee874e43 github.com/envoyproxy/protoc-gen-validate v0.1.0 github.com/ghodss/yaml v1.0.0 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b - github.com/golang/mock v1.4.4 + github.com/golang/mock v1.5.0 github.com/golang/protobuf v1.4.3 github.com/golang/snappy v0.0.2 // indirect github.com/google/go-cmp v0.5.3 // indirect diff --git a/go.sum b/go.sum index 0e642d25f..8e4f12dec 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,8 @@ github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a/go.mod h1:9LKiDE2ChuG github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo= github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= github.com/caos/oidc v0.6.2/go.mod h1:ozoi3b+aY33gzdvjz4w90VZShIHGsmDa0goruuV0arQ= -github.com/caos/oidc v0.14.0 h1:l7mTqYDpqNRZF9Vwzq5KAQd1wQCThdceL5HpsEMGoao= -github.com/caos/oidc v0.14.0/go.mod h1:CPsubVrA110OyLnCKwVZjTdsAVwq67DTbYIvux7UgbY= +github.com/caos/oidc v0.14.1 h1:MZ1wKomUnGeOjNWClf3dtcxqSKd2o37WXf0fpshSb0k= +github.com/caos/oidc v0.14.1/go.mod h1:fSLPGlxZhjSMP2LYKZ5QMaM/YYmLHfj/Fce+ji48kYY= github.com/caos/orbos v1.5.14-0.20210205131708-6dc812182dc0 h1:N+KYBwuQO3QPr/nTUaNwjAetjp3NU4MP8Nv9Iue53UE= github.com/caos/orbos v1.5.14-0.20210205131708-6dc812182dc0/go.mod h1:ZLxNgPuYIlSvr80trezGGUIXng9gY2hHEdky/m0B/P0= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= @@ -338,6 +338,8 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -598,6 +600,8 @@ github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= +github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -783,8 +787,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= +github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index ea471a4b1..5932cd582 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -120,8 +120,8 @@ func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID strin return o.command.HumansSignOut(ctx, userAgentID, userIDs) } -func (o *OPStorage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, timer <-chan time.Time) { - o.repo.GetSigningKey(ctx, keyCh, errCh, timer) +func (o *OPStorage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) { + o.repo.GetSigningKey(ctx, keyCh, o.signingKeyAlgorithm) } func (o *OPStorage) GetKeySet(ctx context.Context) (_ *jose.JSONWebKeySet, err error) { @@ -130,10 +130,6 @@ func (o *OPStorage) GetKeySet(ctx context.Context) (_ *jose.JSONWebKeySet, err e return o.repo.GetKeySet(ctx) } -func (o *OPStorage) SaveNewKeyPair(ctx context.Context) error { - return o.command.GenerateSigningKeyPair(ctx, o.signingKeyAlgorithm) -} - func (o *OPStorage) assertProjectRoleScopes(app *proj_model.ApplicationView, scopes []string) ([]string, error) { if !app.ProjectRoleAssertion { return scopes, nil diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 1de8f8a20..b57fe78ea 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -79,7 +79,6 @@ func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.C op.WithCustomUserinfoEndpoint(op.NewEndpointWithURL(config.Endpoints.Userinfo.Path, config.Endpoints.Userinfo.URL)), op.WithCustomEndSessionEndpoint(op.NewEndpointWithURL(config.Endpoints.EndSession.Path, config.Endpoints.EndSession.URL)), op.WithCustomKeysEndpoint(op.NewEndpointWithURL(config.Endpoints.Keys.Path, config.Endpoints.Keys.URL)), - op.WithRetry(3, time.Duration(30*time.Second)), ) logging.Log("OIDC-asf13").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("cannot create provider") return provider diff --git a/internal/auth/repository/eventsourcing/eventstore/key.go b/internal/auth/repository/eventsourcing/eventstore/key.go index fea6f596a..1c608b975 100644 --- a/internal/auth/repository/eventsourcing/eventstore/key.go +++ b/internal/auth/repository/eventsourcing/eventstore/key.go @@ -2,27 +2,58 @@ package eventstore import ( "context" + "os" "time" + "github.com/caos/logging" "gopkg.in/square/go-jose.v2" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/spooler" + "github.com/caos/zitadel/internal/eventstore/v2" + "github.com/caos/zitadel/internal/id" + "github.com/caos/zitadel/internal/key/model" + key_view "github.com/caos/zitadel/internal/key/repository/view" + "github.com/caos/zitadel/internal/v2/command" ) type KeyRepository struct { - View *view.View - SigningKeyRotation time.Duration + Commands *command.CommandSide + Eventstore *eventstore.Eventstore + View *view.View + SigningKeyRotationCheck time.Duration + SigningKeyGracefulPeriod time.Duration + KeyAlgorithm crypto.EncryptionAlgorithm + KeyChan <-chan *model.KeyView + Locker spooler.Locker + lockID string + currentKeyID string + currentKeyExpiration time.Time } -func (k *KeyRepository) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, renewTimer <-chan time.Time) { +const ( + signingKey = "signing_key" +) + +func (k *KeyRepository) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, algorithm string) { + renewTimer := time.After(0) go func() { for { select { case <-ctx.Done(): return + case key := <-k.KeyChan: + refreshed, err := k.refreshSigningKey(ctx, key, keyCh, algorithm) + logging.Log("KEY-asd5g").OnError(err).Error("could not refresh signing key on key channel push") + k.setRenewTimer(renewTimer, refreshed) case <-renewTimer: - k.refreshSigningKey(keyCh, errCh) - renewTimer = time.After(k.SigningKeyRotation) + key, err := k.latestSigningKey() + logging.Log("KEY-DAfh4").OnError(err).Error("could not check for latest signing key") + refreshed, err := k.refreshSigningKey(ctx, key, keyCh, algorithm) + logging.Log("KEY-DAfh4").OnError(err).Error("could not refresh signing key when ensuring key") + k.setRenewTimer(renewTimer, refreshed) } } }() @@ -40,17 +71,98 @@ func (k *KeyRepository) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, err return &jose.JSONWebKeySet{Keys: webKeys}, nil } -func (k *KeyRepository) refreshSigningKey(keyCh chan<- jose.SigningKey, errCh chan<- error) { - key, err := k.View.GetSigningKey() - if err != nil { - errCh <- err - return +func (k *KeyRepository) setRenewTimer(timer <-chan time.Time, refreshed bool) { + duration := k.SigningKeyRotationCheck + if refreshed { + duration = k.currentKeyExpiration.Sub(time.Now().Add(k.SigningKeyGracefulPeriod + k.SigningKeyRotationCheck*2)) } + timer = time.After(duration) +} + +func (k *KeyRepository) latestSigningKey() (shortRefresh *model.KeyView, err error) { + key, errView := k.View.GetActivePrivateKeyForSigning(time.Now().UTC().Add(k.SigningKeyGracefulPeriod)) + if errView != nil && !errors.IsNotFound(errView) { + logging.Log("EVENT-GEd4h").WithError(errView).Warn("could not get signing key") + return nil, errView + } + return key, nil +} + +func (k *KeyRepository) ensureIsLatestKey(ctx context.Context) (bool, error) { + sequence, err := k.View.GetLatestKeySequence() + if err != nil { + return false, err + } + events, err := k.getKeyEvents(ctx, sequence.CurrentSequence) + if err != nil { + logging.Log("EVENT-der5g").WithError(err).Warn("error retrieving new events") + return false, err + } + if len(events) > 0 { + logging.Log("EVENT-GBD23").Warn("view not up to date, retrying later") + return false, nil + } + return true, nil +} + +func (k *KeyRepository) refreshSigningKey(ctx context.Context, key *model.KeyView, keyCh chan<- jose.SigningKey, algorithm string) (refreshed bool, err error) { + if key == nil { + if k.currentKeyExpiration.Before(time.Now().UTC()) { + keyCh <- jose.SigningKey{} + } + if ok, err := k.ensureIsLatestKey(ctx); !ok && err == nil { + return false, err + } + err = k.lockAndGenerateSigningKeyPair(ctx, algorithm) + logging.Log("EVENT-B4d21").OnError(err).Warn("could not create signing key") + return false, err + } + if k.currentKeyID == key.ID { + return false, nil + } + if ok, err := k.ensureIsLatestKey(ctx); !ok && err == nil { + return false, err + } + signingKey, err := model.SigningKeyFromKeyView(key, k.KeyAlgorithm) + if err != nil { + return false, err + } + k.currentKeyID = signingKey.ID + k.currentKeyExpiration = key.Expiry keyCh <- jose.SigningKey{ - Algorithm: jose.SignatureAlgorithm(key.Algorithm), + Algorithm: jose.SignatureAlgorithm(signingKey.Algorithm), Key: jose.JSONWebKey{ - KeyID: key.ID, - Key: key.Key, + KeyID: signingKey.ID, + Key: signingKey.Key, }, } + return true, nil +} + +func (k *KeyRepository) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm string) error { + err := k.Locker.Renew(k.lockerID(), signingKey, k.SigningKeyRotationCheck*2) + if err != nil { + if errors.IsErrorAlreadyExists(err) { + return nil + } + return err + } + return k.Commands.GenerateSigningKeyPair(ctx, algorithm) +} + +func (k *KeyRepository) lockerID() string { + if k.lockID == "" { + var err error + k.lockID, err = os.Hostname() + if err != nil || k.lockID == "" { + k.lockID, err = id.SonyFlakeGenerator.Next() + logging.Log("EVENT-bsdf6").OnError(err).Panic("unable to generate lockID") + } + context.TODO() + } + return k.lockID +} + +func (k *KeyRepository) getKeyEvents(ctx context.Context, sequence uint64) ([]eventstore.EventReader, error) { + return k.Eventstore.FilterEvents(ctx, key_view.KeyPairQuery(sequence)) } diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 828061970..927a1f3c4 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -8,6 +8,7 @@ import ( "github.com/caos/zitadel/internal/config/types" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/query" + key_model "github.com/caos/zitadel/internal/key/model" ) type Configs map[string]*Config @@ -29,7 +30,7 @@ func (h *handler) Eventstore() eventstore.Eventstore { return h.es } -func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es eventstore.Eventstore, systemDefaults sd.SystemDefaults) []query.Handler { +func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es eventstore.Eventstore, systemDefaults sd.SystemDefaults, keyChan chan<- *key_model.KeyView) []query.Handler { return []query.Handler{ newUser( handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es}, @@ -41,7 +42,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es newToken( handler{view, bulkLimit, configs.cycleDuration("Token"), errorCount, es}), newKey( - handler{view, bulkLimit, configs.cycleDuration("Key"), errorCount, es}), + handler{view, bulkLimit, configs.cycleDuration("Key"), errorCount, es}, + keyChan), newApplication(handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount, es}), newOrg( handler{view, bulkLimit, configs.cycleDuration("Org"), errorCount, es}), diff --git a/internal/auth/repository/eventsourcing/handler/key.go b/internal/auth/repository/eventsourcing/handler/key.go index ded3ce768..e598dbfd1 100644 --- a/internal/auth/repository/eventsourcing/handler/key.go +++ b/internal/auth/repository/eventsourcing/handler/key.go @@ -4,10 +4,12 @@ import ( "time" "github.com/caos/logging" + "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/models" "github.com/caos/zitadel/internal/eventstore/query" "github.com/caos/zitadel/internal/eventstore/spooler" + "github.com/caos/zitadel/internal/key/model" "github.com/caos/zitadel/internal/key/repository/eventsourcing" es_model "github.com/caos/zitadel/internal/key/repository/eventsourcing/model" view_model "github.com/caos/zitadel/internal/key/repository/view/model" @@ -20,11 +22,13 @@ const ( type Key struct { handler subscription *eventstore.Subscription + keyChan chan<- *model.KeyView } -func newKey(handler handler) *Key { +func newKey(handler handler, keyChan chan<- *model.KeyView) *Key { h := &Key{ handler: handler, + keyChan: keyChan, } h.subscribe() @@ -75,7 +79,12 @@ func (k *Key) Reduce(event *models.Event) error { if privateKey.Expiry.Before(time.Now()) && publicKey.Expiry.Before(time.Now()) { return k.view.ProcessedKeySequence(event) } - return k.view.PutKeys(privateKey, publicKey, event) + err = k.view.PutKeys(privateKey, publicKey, event) + if err != nil { + return err + } + k.keyChan <- view_model.KeyViewToModel(privateKey) + return nil default: return k.view.ProcessedKeySequence(event) } diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 171954464..c7abff4dc 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -15,6 +15,7 @@ import ( es_int "github.com/caos/zitadel/internal/eventstore" es_spol "github.com/caos/zitadel/internal/eventstore/spooler" "github.com/caos/zitadel/internal/id" + key_model "github.com/caos/zitadel/internal/key/model" "github.com/caos/zitadel/internal/v2/command" "github.com/caos/zitadel/internal/v2/query" ) @@ -75,7 +76,9 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co return nil, err } - spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, systemDefaults) + keyChan := make(chan *key_model.KeyView) + spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, systemDefaults, keyChan) + locker := spooler.NewLocker(sqlClient) userRepo := eventstore.UserRepo{ SearchLimit: conf.SearchLimit, @@ -108,12 +111,15 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co IAMID: systemDefaults.IamID, }, eventstore.TokenRepo{ - Eventstore: es, View: view, }, eventstore.KeyRepository{ - View: view, - SigningKeyRotation: systemDefaults.KeyConfig.SigningKeyRotation.Duration, + View: view, + SigningKeyRotationCheck: systemDefaults.KeyConfig.SigningKeyRotationCheck.Duration, + SigningKeyGracefulPeriod: systemDefaults.KeyConfig.SigningKeyGracefulPeriod.Duration, + KeyAlgorithm: keyAlgorithm, + Locker: locker, + KeyChan: keyChan, }, eventstore.ApplicationRepo{ Commands: command, diff --git a/internal/auth/repository/eventsourcing/spooler/lock.go b/internal/auth/repository/eventsourcing/spooler/lock.go index 5b69f3ae0..1ba6656c3 100644 --- a/internal/auth/repository/eventsourcing/spooler/lock.go +++ b/internal/auth/repository/eventsourcing/spooler/lock.go @@ -15,6 +15,10 @@ type locker struct { dbClient *sql.DB } +func NewLocker(client *sql.DB) *locker { + return &locker{dbClient: client} +} + func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error { return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime) } diff --git a/internal/auth/repository/eventsourcing/spooler/spooler.go b/internal/auth/repository/eventsourcing/spooler/spooler.go index 2080d65f6..4f4967c9f 100644 --- a/internal/auth/repository/eventsourcing/spooler/spooler.go +++ b/internal/auth/repository/eventsourcing/spooler/spooler.go @@ -3,13 +3,12 @@ package spooler import ( "database/sql" - sd "github.com/caos/zitadel/internal/config/systemdefaults" - "github.com/caos/zitadel/internal/auth/repository/eventsourcing/handler" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" - + sd "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/spooler" + key_model "github.com/caos/zitadel/internal/key/model" ) type SpoolerConfig struct { @@ -19,12 +18,12 @@ type SpoolerConfig struct { Handlers handler.Configs } -func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, client *sql.DB, systemDefaults sd.SystemDefaults) *spooler.Spooler { +func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, client *sql.DB, systemDefaults sd.SystemDefaults, keyChan chan<- *key_model.KeyView) *spooler.Spooler { spoolerConfig := spooler.Config{ Eventstore: es, Locker: &locker{dbClient: client}, ConcurrentWorkers: c.ConcurrentWorkers, - ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, systemDefaults), + ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, systemDefaults, keyChan), } spool := spoolerConfig.New() spool.Start() diff --git a/internal/auth/repository/eventsourcing/view/key.go b/internal/auth/repository/eventsourcing/view/key.go index 574ff8433..e9cd8adcd 100644 --- a/internal/auth/repository/eventsourcing/view/key.go +++ b/internal/auth/repository/eventsourcing/view/key.go @@ -1,6 +1,8 @@ package view import ( + "time" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore/models" key_model "github.com/caos/zitadel/internal/key/model" @@ -17,12 +19,21 @@ func (v *View) KeyByIDAndType(keyID string, private bool) (*model.KeyView, error return view.KeyByIDAndType(v.Db, keyTable, keyID, private) } -func (v *View) GetSigningKey() (*key_model.SigningKey, error) { - key, err := view.GetSigningKey(v.Db, keyTable) +func (v *View) GetActivePrivateKeyForSigning(expiry time.Time) (*key_model.KeyView, error) { + key, err := view.GetSigningKey(v.Db, keyTable, expiry) if err != nil { return nil, err } - return key_model.SigningKeyFromKeyView(model.KeyViewToModel(key), v.keyAlgorithm) + return model.KeyViewToModel(key), nil +} + +func (v *View) GetSigningKey(expiry time.Time) (*key_model.SigningKey, time.Time, error) { + key, err := view.GetSigningKey(v.Db, keyTable, expiry) + if err != nil { + return nil, time.Time{}, err + } + signingKey, err := key_model.SigningKeyFromKeyView(model.KeyViewToModel(key), v.keyAlgorithm) + return signingKey, key.Expiry, err } func (v *View) GetActiveKeySet() ([]*key_model.PublicKey, error) { diff --git a/internal/auth/repository/key.go b/internal/auth/repository/key.go index 1598041e5..ed8cef3a8 100644 --- a/internal/auth/repository/key.go +++ b/internal/auth/repository/key.go @@ -2,12 +2,11 @@ package repository import ( "context" - "time" "gopkg.in/square/go-jose.v2" ) type KeyRepository interface { - GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, timer <-chan time.Time) + GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, algorithm string) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) } diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index cd3aedf6e..02653688a 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -101,9 +101,10 @@ type WebAuthN struct { } type KeyConfig struct { - Size int - PrivateKeyLifetime types.Duration - PublicKeyLifetime types.Duration - EncryptionConfig *crypto.KeyConfig - SigningKeyRotation types.Duration + Size int + PrivateKeyLifetime types.Duration + PublicKeyLifetime types.Duration + EncryptionConfig *crypto.KeyConfig + SigningKeyRotationCheck types.Duration + SigningKeyGracefulPeriod types.Duration } diff --git a/internal/key/model/key_view.go b/internal/key/model/key_view.go index ac8507f8d..a6640354a 100644 --- a/internal/key/model/key_view.go +++ b/internal/key/model/key_view.go @@ -22,6 +22,7 @@ type SigningKey struct { ID string Algorithm string Key interface{} + Sequence uint64 } type PublicKey struct { @@ -84,6 +85,7 @@ func SigningKeyFromKeyView(key *KeyView, alg crypto.EncryptionAlgorithm) (*Signi ID: key.ID, Algorithm: key.Algorithm, Key: privateKey, + Sequence: key.Sequence, }, nil } diff --git a/internal/key/repository/view/key.go b/internal/key/repository/view/key.go index 3fdc089c3..dab41b0c8 100644 --- a/internal/key/repository/view/key.go +++ b/internal/key/repository/view/key.go @@ -3,6 +3,7 @@ package view import ( "time" + caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/view/repository" "github.com/jinzhu/gorm" @@ -22,15 +23,30 @@ func KeyByIDAndType(db *gorm.DB, table, keyID string, private bool) (*model.KeyV return key, err } -func GetSigningKey(db *gorm.DB, table string) (*model.KeyView, error) { - key := new(model.KeyView) - query := repository.PrepareGetByQuery(table, - model.KeySearchQuery{Key: key_model.KeySearchKeyPrivate, Method: global_model.SearchMethodEquals, Value: true}, - model.KeySearchQuery{Key: key_model.KeySearchKeyUsage, Method: global_model.SearchMethodEquals, Value: key_model.KeyUsageSigning}, - model.KeySearchQuery{Key: key_model.KeySearchKeyExpiry, Method: global_model.SearchMethodGreaterThan, Value: time.Now().UTC()}, +func GetSigningKey(db *gorm.DB, table string, expiry time.Time) (*model.KeyView, error) { + if expiry.IsZero() { + expiry = time.Now().UTC() + } + keys := make([]*model.KeyView, 0) + query := repository.PrepareSearchQuery(table, + model.KeySearchRequest{ + Queries: []*key_model.KeySearchQuery{ + {Key: key_model.KeySearchKeyPrivate, Method: global_model.SearchMethodEquals, Value: true}, + {Key: key_model.KeySearchKeyUsage, Method: global_model.SearchMethodEquals, Value: key_model.KeyUsageSigning}, + {Key: key_model.KeySearchKeyExpiry, Method: global_model.SearchMethodGreaterThan, Value: time.Now().UTC()}, + }, + SortingColumn: key_model.KeySearchKeyExpiry, + Limit: 1, + }, ) - err := query(db, key) - return key, err + _, err := query(db, &keys) + if err != nil { + return nil, err + } + if len(keys) != 1 { + return nil, caos_errs.ThrowNotFound(err, "VIEW-BGD41", "key not found") + } + return keys[0], nil } func GetActivePublicKeys(db *gorm.DB, table string) ([]*model.KeyView, error) { diff --git a/internal/key/repository/view/model/key.go b/internal/key/repository/view/model/key.go index 8cae30b6a..f65769ad9 100644 --- a/internal/key/repository/view/model/key.go +++ b/internal/key/repository/view/model/key.go @@ -45,7 +45,7 @@ func KeysFromPairEvent(event *models.Event) (*KeyView, *KeyView, error) { Algorithm: pair.Algorithm, Usage: pair.Usage, Key: pair.PrivateKey.Key, - Sequence: pair.Sequence, + Sequence: event.Sequence, } publicKey := &KeyView{ ID: event.AggregateID, @@ -54,7 +54,7 @@ func KeysFromPairEvent(event *models.Event) (*KeyView, *KeyView, error) { Algorithm: pair.Algorithm, Usage: pair.Usage, Key: pair.PublicKey.Key, - Sequence: pair.Sequence, + Sequence: event.Sequence, } return privateKey, publicKey, nil } diff --git a/internal/key/repository/view/query.go b/internal/key/repository/view/query.go new file mode 100644 index 000000000..172527c6e --- /dev/null +++ b/internal/key/repository/view/query.go @@ -0,0 +1,11 @@ +package view + +import ( + "github.com/caos/zitadel/internal/eventstore/v2" + "github.com/caos/zitadel/internal/v2/repository/iam" +) + +func KeyPairQuery(latestSequence uint64) *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, iam.AggregateType). + SequenceGreater(latestSequence) +}