diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 48f2c056..846a924a 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -11,6 +11,7 @@ import ( "os" "strings" + "github.com/go-jose/go-jose/v4" "golang.org/x/crypto/bcrypt" "github.com/dexidp/dex/pkg/featureflags" @@ -525,6 +526,11 @@ func (s *Signer) UnmarshalJSON(b []byte) error { return fmt.Errorf("parse signer config: %v", err) } } + if localConfig, ok := signerConfig.(*signer.LocalConfig); ok { + if err := normalizeLocalSignerConfig(localConfig); err != nil { + return fmt.Errorf("parse signer config: %v", err) + } + } *s = Signer{ Type: signerData.Type, @@ -533,6 +539,17 @@ func (s *Signer) UnmarshalJSON(b []byte) error { return nil } +func normalizeLocalSignerConfig(c *signer.LocalConfig) error { + if c.Algorithm == "" { + c.Algorithm = jose.RS256 + return nil + } + if c.Algorithm == jose.RS256 || c.Algorithm == jose.ES256 { + return nil + } + return fmt.Errorf("unsupported local signer algorithm %q", c.Algorithm) +} + // Connector is a magical type that can unmarshal YAML dynamically. The // Type field determines the connector type, which is then customized for Config. type Connector struct { diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index 26385f56..2a08ab1e 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -4,9 +4,11 @@ import ( "encoding/json" "log/slog" "os" + "strings" "testing" "github.com/ghodss/yaml" + "github.com/go-jose/go-jose/v4" "github.com/kylelemons/godebug/pretty" "github.com/dexidp/dex/connector/mock" @@ -481,10 +483,11 @@ logger: func TestSignerConfigUnmarshal(t *testing.T) { tests := []struct { - name string - config string - wantErr bool - check func(*Config) error + name string + config string + wantErr bool + errContains string + check func(*Config) error }{ { name: "local signer with rotation period", @@ -507,8 +510,84 @@ enablePasswordDB: true } if localConfig, ok := c.Signer.Config.(*signer.LocalConfig); !ok { t.Error("expected LocalConfig") - } else if localConfig.KeysRotationPeriod != "6h" { - t.Errorf("expected keys rotation period '6h', got %q", localConfig.KeysRotationPeriod) + } else { + if localConfig.KeysRotationPeriod != "6h" { + t.Errorf("expected keys rotation period '6h', got %q", localConfig.KeysRotationPeriod) + } + if localConfig.Algorithm != jose.RS256 { + t.Errorf("expected default algorithm 'RS256', got %q", localConfig.Algorithm) + } + } + return nil + }, + }, + { + name: "local signer with ES256 algorithm", + config: ` +issuer: http://127.0.0.1:5556/dex +storage: + type: memory +web: + http: 0.0.0.0:5556 +signer: + type: local + config: + keysRotationPeriod: 6h + algorithm: ES256 +enablePasswordDB: true +`, + wantErr: false, + check: func(c *Config) error { + localConfig, ok := c.Signer.Config.(*signer.LocalConfig) + if !ok { + t.Error("expected LocalConfig") + return nil + } + if localConfig.Algorithm != jose.ES256 { + t.Errorf("expected algorithm 'ES256', got %q", localConfig.Algorithm) + } + return nil + }, + }, + { + name: "local signer with invalid algorithm", + config: ` +issuer: http://127.0.0.1:5556/dex +storage: + type: memory +web: + http: 0.0.0.0:5556 +signer: + type: local + config: + keysRotationPeriod: 6h + algorithm: ES512 +enablePasswordDB: true +`, + wantErr: true, + errContains: `parse signer config: unsupported local signer algorithm "ES512"`, + }, + { + name: "local signer without config", + config: ` +issuer: http://127.0.0.1:5556/dex +storage: + type: memory +web: + http: 0.0.0.0:5556 +signer: + type: local +enablePasswordDB: true +`, + wantErr: false, + check: func(c *Config) error { + localConfig, ok := c.Signer.Config.(*signer.LocalConfig) + if !ok { + t.Error("expected LocalConfig") + return nil + } + if localConfig.Algorithm != jose.RS256 { + t.Errorf("expected default algorithm 'RS256', got %q", localConfig.Algorithm) } return nil }, @@ -583,6 +662,10 @@ enablePasswordDB: true t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr) return } + if tt.errContains != "" && (err == nil || !strings.Contains(err.Error(), tt.errContains)) { + t.Errorf("Unmarshal() error = %v, want substring %q", err, tt.errContains) + return + } if err == nil && tt.check != nil { if err := tt.check(&c); err != nil { diff --git a/config.docker.yaml b/config.docker.yaml index c5d2a47b..cc3a99ed 100644 --- a/config.docker.yaml +++ b/config.docker.yaml @@ -22,10 +22,15 @@ telemetry: expiry: deviceRequests: {{ getenv "DEX_EXPIRY_DEVICE_REQUESTS" "5m" }} - signingKeys: {{ getenv "DEX_EXPIRY_SIGNING_KEYS" "6h" }} idTokens: {{ getenv "DEX_EXPIRY_ID_TOKENS" "24h" }} authRequests: {{ getenv "DEX_EXPIRY_AUTH_REQUESTS" "24h" }} +signer: + type: local + config: + keysRotationPeriod: {{ getenv "DEX_EXPIRY_SIGNING_KEYS" "6h" }} + algorithm: {{ getenv "DEX_SIGNER_LOCAL_ALGORITHM" "RS256" }} + logger: level: {{ getenv "DEX_LOG_LEVEL" "info" }} format: {{ getenv "DEX_LOG_FORMAT" "text" }} diff --git a/config.yaml.dist b/config.yaml.dist index 917f8d1f..669066c9 100644 --- a/config.yaml.dist +++ b/config.yaml.dist @@ -87,13 +87,19 @@ web: # Expiration configuration for tokens, signing keys, etc. # expiry: # deviceRequests: "5m" -# signingKeys: "6h" +# signingKeys: "6h" # deprecated, use signer.config.keysRotationPeriod # idTokens: "24h" # refreshTokens: # disableRotation: false # reuseInterval: "3s" # validIfNotUsedFor: "2160h" # 90 days # absoluteLifetime: "3960h" # 165 days +# +# signer: +# type: local +# config: +# keysRotationPeriod: "6h" +# algorithm: "RS256" # supported values: "RS256" (default) and "ES256"; changes apply on the next key rotation # OAuth2 configuration # oauth2: diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml index cc15daed..1b9da1f1 100644 --- a/examples/config-dev.yaml +++ b/examples/config-dev.yaml @@ -88,7 +88,7 @@ telemetry: # Is possible to specify units using only s, m and h suffixes. # expiry: # deviceRequests: "5m" -# signingKeys: "6h" +# signingKeys: "6h" # deprecated, use signer.config.keysRotationPeriod # idTokens: "24h" # refreshTokens: # reuseInterval: "3s" @@ -239,13 +239,14 @@ staticPasswords: - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" -# Settings for signing JWT tokens. Available options: -# - "local": use local keys (only RSA keys supported) -# - "vault": use Vault Transit backend (RSA and EC keys supported) +# Configuration for signing JWT tokens. +# - "local": use local keys (supports RS256 (default) and ES256) +# - "vault": use Vault Transit backend (supports RSA, ECDSA, and Ed25519) signer: type: local config: keysRotationPeriod: "6h" + algorithm: "RS256" # changes apply on the next key rotation # signer # type: vault # config: diff --git a/server/handlers_test.go b/server/handlers_test.go index 933e9e4d..dad1ab2a 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -20,12 +20,14 @@ import ( gosundheit "github.com/AppsFlyer/go-sundheit" "github.com/AppsFlyer/go-sundheit/checks" "github.com/coreos/go-oidc/v3/oidc" + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" "golang.org/x/oauth2" "github.com/dexidp/dex/connector" "github.com/dexidp/dex/server/internal" + "github.com/dexidp/dex/server/signer" "github.com/dexidp/dex/storage" ) @@ -112,6 +114,29 @@ func TestHandleDiscovery(t *testing.T) { }, res) } +func TestHandleDiscoveryWithES256LocalSigner(t *testing.T) { + httpServer, server := newTestServer(t, func(c *Config) { + localConfig := signer.LocalConfig{ + KeysRotationPeriod: time.Hour.String(), + Algorithm: jose.ES256, + } + + sig, err := localConfig.Open(context.Background(), c.Storage, time.Hour, time.Now, c.Logger) + require.NoError(t, err) + c.Signer = sig + }) + defer httpServer.Close() + + rr := httptest.NewRecorder() + server.ServeHTTP(rr, httptest.NewRequest("GET", "/.well-known/openid-configuration", nil)) + require.Equal(t, http.StatusOK, rr.Code) + + var res discovery + err := json.NewDecoder(rr.Result().Body).Decode(&res) + require.NoError(t, err) + require.Equal(t, []string{string(jose.ES256)}, res.IDTokenAlgs) +} + func TestHandleHealthFailure(t *testing.T) { httpServer, server := newTestServer(t, func(c *Config) { c.HealthChecker = gosundheit.New() diff --git a/server/oauth2_test.go b/server/oauth2_test.go index 835868a1..6df028e0 100644 --- a/server/oauth2_test.go +++ b/server/oauth2_test.go @@ -1,6 +1,7 @@ package server import ( + "context" "crypto/rand" "crypto/rsa" "encoding/json" @@ -781,6 +782,31 @@ func TestSignerKeySet(t *testing.T) { } } +func TestSignerKeySetWithES256LocalSigner(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := slog.New(slog.DiscardHandler) + store := memory.New(logger) + + localConfig := signer.LocalConfig{ + KeysRotationPeriod: time.Hour.String(), + Algorithm: jose.ES256, + } + sig, err := localConfig.Open(ctx, store, time.Hour, time.Now, logger) + require.NoError(t, err) + + sig.Start(ctx) + + jwt, err := sig.Sign(ctx, []byte("payload")) + require.NoError(t, err) + + keySet := &signerKeySet{signer: sig} + payload, err := keySet.VerifySignature(ctx, jwt) + require.NoError(t, err) + require.Equal(t, []byte("payload"), payload) +} + func TestRedirectedAuthErrHandler(t *testing.T) { tests := []struct { name string @@ -980,6 +1006,99 @@ func TestValidateIDTokenHint(t *testing.T) { }) } +func TestNewIDTokenUsesStoredAlgorithmUntilNextRotation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := slog.New(slog.DiscardHandler) + store := memory.New(logger) + + now := time.Now().UTC() + err := store.UpdateKeys(ctx, func(keys storage.Keys) (storage.Keys, error) { + keys.SigningKey = &jose.JSONWebKey{ + Key: testKey, + KeyID: "legacy-rs256", + Algorithm: string(jose.RS256), + Use: "sig", + } + keys.SigningKeyPub = &jose.JSONWebKey{ + Key: testKey.Public(), + KeyID: "legacy-rs256", + Algorithm: string(jose.RS256), + Use: "sig", + } + keys.NextRotation = now.Add(time.Hour) + return keys, nil + }) + require.NoError(t, err) + + localConfig := signer.LocalConfig{ + KeysRotationPeriod: time.Hour.String(), + Algorithm: jose.ES256, + } + sig, err := localConfig.Open(ctx, store, time.Hour, func() time.Time { return now }, logger) + require.NoError(t, err) + + sig.Start(ctx) + + alg, err := sig.Algorithm(ctx) + require.NoError(t, err) + require.Equal(t, jose.RS256, alg) + + issuerURL, err := url.Parse("https://issuer.example.com") + require.NoError(t, err) + + s := &Server{ + signer: sig, + issuerURL: *issuerURL, + logger: logger, + now: func() time.Time { return now }, + idTokensValidFor: time.Hour, + } + + accessToken := "test-access-token" + code := "test-auth-code" + idToken, _, err := s.newIDToken( + ctx, + "test-client", + storage.Claims{UserID: "1", Username: "jane"}, + []string{"openid"}, + "nonce", + accessToken, + code, + "test", + time.Time{}, + ) + require.NoError(t, err) + + keys, err := sig.ValidationKeys(ctx) + require.NoError(t, err) + require.NotEmpty(t, keys) + + jws, err := jose.ParseSigned(idToken, []jose.SignatureAlgorithm{jose.RS256}) + require.NoError(t, err) + require.Len(t, jws.Signatures, 1) + require.Equal(t, string(jose.RS256), jws.Signatures[0].Protected.Algorithm) + + payload, err := jws.Verify(keys[0]) + require.NoError(t, err) + + var claims struct { + AccessTokenHash string `json:"at_hash"` + CodeHash string `json:"c_hash"` + } + err = json.Unmarshal(payload, &claims) + require.NoError(t, err) + + wantAtHash, err := accessTokenHash(jose.RS256, accessToken) + require.NoError(t, err) + require.Equal(t, wantAtHash, claims.AccessTokenHash) + + wantCodeHash, err := accessTokenHash(jose.RS256, code) + require.NoError(t, err) + require.Equal(t, wantCodeHash, claims.CodeHash) +} + func TestSessionMatchesHint(t *testing.T) { // genSubject("foo", "bar") == "CgNmb28SA2Jhcg" (from TestGetSubject) assert.True(t, sessionMatchesHint(&storage.AuthSession{UserID: "foo", ConnectorID: "bar"}, "CgNmb28SA2Jhcg")) diff --git a/server/signer/local.go b/server/signer/local.go index da9f99fb..30fd37d3 100644 --- a/server/signer/local.go +++ b/server/signer/local.go @@ -2,6 +2,7 @@ package signer import ( "context" + "errors" "fmt" "log/slog" "time" @@ -15,6 +16,21 @@ import ( type LocalConfig struct { // KeysRotationPeriod defines the duration of time after which the signing keys will be rotated. KeysRotationPeriod string `json:"keysRotationPeriod"` + // Algorithm defines the signing algorithm used for newly generated local keys. + // Changing it does not replace the current signing key immediately. The new + // algorithm is applied when Dex generates the next signing key during rotation. + // Supported values are RS256 and ES256. + Algorithm jose.SignatureAlgorithm `json:"algorithm"` +} + +func (c *LocalConfig) signingAlgorithm() (jose.SignatureAlgorithm, error) { + if c.Algorithm == "" { + return jose.RS256, nil + } + if c.Algorithm == jose.RS256 || c.Algorithm == jose.ES256 { + return c.Algorithm, nil + } + return "", fmt.Errorf("unsupported local signer algorithm %q", c.Algorithm) } // Open creates a new local signer. @@ -24,7 +40,15 @@ func (c *LocalConfig) Open(_ context.Context, s storage.Storage, idTokenValidFor return nil, fmt.Errorf("invalid config value %q for local signer rotation period: %v", c.KeysRotationPeriod, err) } - strategy := defaultRotationStrategy(rotateKeysAfter, idTokenValidFor) + alg, err := c.signingAlgorithm() + if err != nil { + return nil, fmt.Errorf("invalid config value %q for local signer algorithm: %v", c.Algorithm, err) + } + + strategy, err := rotationStrategyForAlgorithm(rotateKeysAfter, idTokenValidFor, alg) + if err != nil { + return nil, err + } r := &keyRotator{s, strategy, now, logger} return &localSigner{ storage: s, @@ -48,11 +72,7 @@ type localSigner struct { func (l *localSigner) Start(ctx context.Context) { // Try to rotate immediately so properly configured storages will have keys. if err := l.rotator.rotate(); err != nil { - if err == errAlreadyRotated { - l.logger.Info("key rotation not needed", "err", err) - } else { - l.logger.Error("failed to rotate keys", "err", err) - } + l.logRotateError(err) } go func() { @@ -62,13 +82,21 @@ func (l *localSigner) Start(ctx context.Context) { return case <-time.After(time.Second * 30): if err := l.rotator.rotate(); err != nil { - l.logger.Error("failed to rotate keys", "err", err) + l.logRotateError(err) } } } }() } +func (l *localSigner) logRotateError(err error) { + if errors.Is(err, errAlreadyRotated) { + l.logger.Info("key rotation not needed", "err", err) + return + } + l.logger.Error("failed to rotate keys", "err", err) +} + func (l *localSigner) Sign(ctx context.Context, payload []byte) (string, error) { keys, err := l.storage.GetKeys(ctx) if err != nil { @@ -105,8 +133,13 @@ func (l *localSigner) ValidationKeys(ctx context.Context) ([]*jose.JSONWebKey, e return jwks, nil } -func (l *localSigner) Algorithm(_ context.Context) (jose.SignatureAlgorithm, error) { - // Local signer always uses RSA keys (see rotationStrategy.key). - // TODO(nabokihms): add support for other key types and algorithms in the future. - return jose.RS256, nil +func (l *localSigner) Algorithm(ctx context.Context) (jose.SignatureAlgorithm, error) { + keys, err := l.storage.GetKeys(ctx) + if err != nil && !errors.Is(err, storage.ErrNotFound) { + return "", fmt.Errorf("failed to get keys: %v", err) + } + if keys.SigningKey == nil { + return l.rotator.strategy.algorithm, nil + } + return signatureAlgorithm(keys.SigningKey) } diff --git a/server/signer/local_test.go b/server/signer/local_test.go index 9e0f7e8d..dfd65763 100644 --- a/server/signer/local_test.go +++ b/server/signer/local_test.go @@ -2,6 +2,10 @@ package signer import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" "log/slog" "testing" "time" @@ -10,51 +14,245 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/dexidp/dex/storage" "github.com/dexidp/dex/storage/memory" ) -func newTestLocalSigner(t *testing.T) *localSigner { +func newTestLocalSigner(t *testing.T, config LocalConfig, s storage.Storage, now func() time.Time) *localSigner { t.Helper() logger := slog.New(slog.DiscardHandler) - s := memory.New(logger) - r := &keyRotator{ - Storage: s, - strategy: defaultRotationStrategy(time.Hour, time.Hour), - now: time.Now, - logger: logger, + if s == nil { + s = memory.New(logger) + } + if config.KeysRotationPeriod == "" { + config.KeysRotationPeriod = time.Hour.String() + } + if now == nil { + now = time.Now } - return &localSigner{ - storage: s, - rotator: r, - logger: logger, + sig, err := config.Open(context.Background(), s, time.Hour, now, logger) + require.NoError(t, err) + + ls, ok := sig.(*localSigner) + require.True(t, ok) + return ls +} + +func newTestRSAJWKPair(t *testing.T) (*jose.JSONWebKey, *jose.JSONWebKey) { + t.Helper() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + priv, pub, err := newJWKPair(key, jose.RS256) + require.NoError(t, err) + + return priv, pub +} + +func newTestES256JWKPair(t *testing.T) (*jose.JSONWebKey, *jose.JSONWebKey) { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + priv, pub, err := newJWKPair(key, jose.ES256) + require.NoError(t, err) + + return priv, pub +} + +func requireVerifiedByAnyKey(t *testing.T, token string, alg jose.SignatureAlgorithm, keys []*jose.JSONWebKey, wantPayload []byte) { + t.Helper() + + jws, err := jose.ParseSigned(token, []jose.SignatureAlgorithm{alg}) + require.NoError(t, err) + + for _, key := range keys { + payload, err := jws.Verify(key) + if err == nil { + require.Equal(t, wantPayload, payload) + return + } } + + t.Fatalf("token did not verify with any key for algorithm %s", alg) } func TestLocalSignerAlgorithm(t *testing.T) { - ls := newTestLocalSigner(t) + tests := []struct { + name string + cfg LocalConfig + want jose.SignatureAlgorithm + }{ + { + name: "default RS256 before first rotation", + cfg: LocalConfig{KeysRotationPeriod: time.Hour.String()}, + want: jose.RS256, + }, + { + name: "ES256 before first rotation", + cfg: LocalConfig{KeysRotationPeriod: time.Hour.String(), Algorithm: jose.ES256}, + want: jose.ES256, + }, + } - // Algorithm should return RS256 even before keys are rotated (empty storage). - alg, err := ls.Algorithm(context.Background()) - require.NoError(t, err) - assert.Equal(t, jose.RS256, alg) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ls := newTestLocalSigner(t, tt.cfg, nil, nil) + + alg, err := ls.Algorithm(context.Background()) + require.NoError(t, err) + assert.Equal(t, tt.want, alg) + }) + } } func TestLocalSignerSignAndValidate(t *testing.T) { - ls := newTestLocalSigner(t) - ctx := context.Background() + tests := []struct { + name string + cfg LocalConfig + want jose.SignatureAlgorithm + }{ + { + name: "RS256", + cfg: LocalConfig{KeysRotationPeriod: time.Hour.String()}, + want: jose.RS256, + }, + { + name: "ES256", + cfg: LocalConfig{KeysRotationPeriod: time.Hour.String(), Algorithm: jose.ES256}, + want: jose.ES256, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ls := newTestLocalSigner(t, tt.cfg, nil, nil) + ctx := context.Background() + + require.NoError(t, ls.rotator.rotate()) + + payload := []byte(`{"sub":"test-user"}`) + signed, err := ls.Sign(ctx, payload) + require.NoError(t, err) + assert.NotEmpty(t, signed) + + keys, err := ls.ValidationKeys(ctx) + require.NoError(t, err) + require.Len(t, keys, 1) + assert.Equal(t, string(tt.want), keys[0].Algorithm) + + alg, err := ls.Algorithm(ctx) + require.NoError(t, err) + assert.Equal(t, tt.want, alg) + + requireVerifiedByAnyKey(t, signed, tt.want, keys, payload) + }) + } +} + +func TestLocalSignerAppliesConfiguredAlgorithmOnNextRotation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := slog.New(slog.DiscardHandler) + s := memory.New(logger) + + currentTime := time.Now().UTC() + + rsaPriv, rsaPub := newTestRSAJWKPair(t) + err := s.UpdateKeys(ctx, func(keys storage.Keys) (storage.Keys, error) { + keys.SigningKey = rsaPriv + keys.SigningKeyPub = rsaPub + keys.NextRotation = currentTime.Add(time.Hour) + return keys, nil + }) + require.NoError(t, err) + + ls := newTestLocalSigner( + t, + LocalConfig{KeysRotationPeriod: time.Hour.String(), Algorithm: jose.ES256}, + s, + func() time.Time { return currentTime }, + ) + + beforeAlg, err := ls.Algorithm(ctx) + require.NoError(t, err) + assert.Equal(t, jose.RS256, beforeAlg) + + beforePayload := []byte(`{"sub":"before-rotation"}`) + beforeToken, err := ls.Sign(ctx, beforePayload) + require.NoError(t, err) + + ls.Start(ctx) + + afterStartAlg, err := ls.Algorithm(ctx) + require.NoError(t, err) + assert.Equal(t, jose.RS256, afterStartAlg) + + currentTime = currentTime.Add(time.Hour + time.Second) + require.NoError(t, ls.rotator.rotate()) + + afterRotationAlg, err := ls.Algorithm(ctx) + require.NoError(t, err) + assert.Equal(t, jose.ES256, afterRotationAlg) + + afterPayload := []byte(`{"sub":"after-rotation"}`) + afterToken, err := ls.Sign(ctx, afterPayload) + require.NoError(t, err) + + keys, err := ls.ValidationKeys(ctx) + require.NoError(t, err) + require.Len(t, keys, 2) + assert.Equal(t, string(jose.ES256), keys[0].Algorithm) + assert.Equal(t, string(jose.RS256), keys[1].Algorithm) + + requireVerifiedByAnyKey(t, beforeToken, jose.RS256, keys, beforePayload) + requireVerifiedByAnyKey(t, afterToken, jose.ES256, keys, afterPayload) +} + +func TestLocalSignerDoesNotRevertSharedKeyAlgorithmBeforeNextRotation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := slog.New(slog.DiscardHandler) + s := memory.New(logger) + + currentTime := time.Now().UTC() + + esPriv, esPub := newTestES256JWKPair(t) + err := s.UpdateKeys(ctx, func(keys storage.Keys) (storage.Keys, error) { + keys.SigningKey = esPriv + keys.SigningKeyPub = esPub + keys.NextRotation = currentTime.Add(time.Hour) + return keys, nil + }) + require.NoError(t, err) + + ls := newTestLocalSigner( + t, + LocalConfig{KeysRotationPeriod: time.Hour.String(), Algorithm: jose.RS256}, + s, + func() time.Time { return currentTime }, + ) - // Rotate keys so we have a signing key. require.NoError(t, ls.rotator.rotate()) - payload := []byte(`{"sub":"test-user"}`) - signed, err := ls.Sign(ctx, payload) + alg, err := ls.Algorithm(ctx) require.NoError(t, err) - assert.NotEmpty(t, signed) + assert.Equal(t, jose.ES256, alg) - // Validation keys should be available. keys, err := ls.ValidationKeys(ctx) require.NoError(t, err) - assert.NotEmpty(t, keys) + require.Len(t, keys, 1) + assert.Equal(t, string(jose.ES256), keys[0].Algorithm) + assert.Equal(t, esPub.KeyID, keys[0].KeyID) + + payload := []byte(`{"sub":"shared-key"}`) + token, err := ls.Sign(ctx, payload) + require.NoError(t, err) + requireVerifiedByAnyKey(t, token, jose.ES256, keys, payload) } diff --git a/server/signer/rotation.go b/server/signer/rotation.go index f043ba72..b24fa1db 100644 --- a/server/signer/rotation.go +++ b/server/signer/rotation.go @@ -2,6 +2,9 @@ package signer import ( "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "encoding/hex" @@ -28,21 +31,55 @@ type rotationStrategy struct { // signatures? idTokenValidFor time.Duration - // Keys are always RSA keys. Though cryptopasta recommends ECDSA keys, not every - // client may support these (e.g. github.com/coreos/go-oidc/oidc). - key func() (*rsa.PrivateKey, error) + // Algorithm used for newly generated signing keys. + algorithm jose.SignatureAlgorithm + + // Local signer keys can be RSA or ECDSA depending on the configured algorithm. + key func() (crypto.Signer, error) } -// defaultRotationStrategy returns a strategy which rotates keys every provided period, -// holding onto the public parts for some specified amount of time. -func defaultRotationStrategy(rotationFrequency, idTokenValidFor time.Duration) rotationStrategy { - return rotationStrategy{ +// rotationStrategyForAlgorithm builds a key rotation strategy for the provided +// local signer algorithm. +func rotationStrategyForAlgorithm(rotationFrequency, idTokenValidFor time.Duration, algorithm jose.SignatureAlgorithm) (rotationStrategy, error) { + strategy := rotationStrategy{ rotationFrequency: rotationFrequency, idTokenValidFor: idTokenValidFor, - key: func() (*rsa.PrivateKey, error) { + algorithm: algorithm, + } + // Only RS256 and ES256 are supported for local key rotation; all other algorithms are handled by the default case. + switch algorithm { //nolint:exhaustive + case jose.RS256: + strategy.key = func() (crypto.Signer, error) { return rsa.GenerateKey(rand.Reader, 2048) - }, + } + case jose.ES256: + strategy.key = func() (crypto.Signer, error) { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + } + default: + return rotationStrategy{}, fmt.Errorf("unsupported local signer algorithm %q", algorithm) + } + return strategy, nil +} + +func newJWKPair(key crypto.Signer, algorithm jose.SignatureAlgorithm) (priv, pub *jose.JSONWebKey, err error) { + b := make([]byte, 20) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return nil, nil, fmt.Errorf("generate key id: %v", err) } + keyID := hex.EncodeToString(b) + + return &jose.JSONWebKey{ + Key: key, + KeyID: keyID, + Algorithm: string(algorithm), + Use: "sig", + }, &jose.JSONWebKey{ + Key: key.Public(), + KeyID: keyID, + Algorithm: string(algorithm), + Use: "sig", + }, nil } type keyRotator struct { @@ -54,46 +91,47 @@ type keyRotator struct { logger *slog.Logger } +func (k keyRotator) rotationReason(keys storage.Keys, tNow time.Time) string { + if keys.SigningKey == nil { + return "missing signing key" + } + + if tNow.Before(keys.NextRotation) { + return "" + } + return "expired" +} + func (k keyRotator) rotate() error { keys, err := k.GetKeys(context.Background()) if err != nil && err != storage.ErrNotFound { return fmt.Errorf("get keys: %v", err) } - if k.now().Before(keys.NextRotation) { + + reason := k.rotationReason(keys, k.now()) + if reason == "" { return nil } - k.logger.Info("keys expired, rotating") + k.logger.Info("rotating signing keys", "reason", reason) // Generate the key outside of a storage transaction. key, err := k.strategy.key() if err != nil { return fmt.Errorf("generate key: %v", err) } - b := make([]byte, 20) - if _, err := io.ReadFull(rand.Reader, b); err != nil { - panic(err) - } - keyID := hex.EncodeToString(b) - priv := &jose.JSONWebKey{ - Key: key, - KeyID: keyID, - Algorithm: "RS256", - Use: "sig", - } - pub := &jose.JSONWebKey{ - Key: key.Public(), - KeyID: keyID, - Algorithm: "RS256", - Use: "sig", + priv, pub, err := newJWKPair(key, k.strategy.algorithm) + if err != nil { + return fmt.Errorf("generate JWK pair: %v", err) } var nextRotation time.Time err = k.Storage.UpdateKeys(context.Background(), func(keys storage.Keys) (storage.Keys, error) { tNow := k.now() + reason := k.rotationReason(keys, tNow) // if you are running multiple instances of dex, another instance // could have already rotated the keys. - if tNow.Before(keys.NextRotation) { + if reason == "" { return storage.Keys{}, errAlreadyRotated } @@ -134,6 +172,6 @@ func (k keyRotator) rotate() error { if err != nil { return err } - k.logger.Info("keys rotated", "next_rotation", nextRotation) + k.logger.Info("keys rotated", "reason", reason, "next_rotation", nextRotation) return nil } diff --git a/server/signer/rotation_test.go b/server/signer/rotation_test.go index 1974d996..a4e0f937 100644 --- a/server/signer/rotation_test.go +++ b/server/signer/rotation_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v4" + "github.com/dexidp/dex/storage" "github.com/dexidp/dex/storage/memory" ) @@ -67,10 +69,14 @@ func TestKeyRotator(t *testing.T) { maxVerificationKeys := 5 l := slog.New(slog.DiscardHandler) + strategy, err := rotationStrategyForAlgorithm(rotationFrequency, validFor, jose.RS256) + if err != nil { + t.Fatal(err) + } r := &keyRotator{ Storage: memory.New(l), - strategy: defaultRotationStrategy(rotationFrequency, validFor), + strategy: strategy, now: func() time.Time { return now }, logger: l, } diff --git a/server/signer/utils.go b/server/signer/utils.go index 6d607a10..92926d5b 100644 --- a/server/signer/utils.go +++ b/server/signer/utils.go @@ -2,6 +2,7 @@ package signer import ( "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "errors" @@ -10,39 +11,54 @@ import ( "github.com/go-jose/go-jose/v4" ) +// signatureAlgorithm returns the JOSE signing algorithm declared by the JWK or +// inferred from its key material when the algorithm field is empty. func signatureAlgorithm(jwk *jose.JSONWebKey) (alg jose.SignatureAlgorithm, err error) { if jwk.Key == nil { return alg, errors.New("no signing key") } - switch key := jwk.Key.(type) { - case *rsa.PrivateKey: + if jwk.Algorithm != "" { + return jose.SignatureAlgorithm(jwk.Algorithm), nil + } + return signatureAlgorithmFromKey(jwk.Key) +} + +func signatureAlgorithmFromKey(key any) (alg jose.SignatureAlgorithm, err error) { + switch key := key.(type) { + case *rsa.PublicKey, *rsa.PrivateKey: // Because OIDC mandates that we support RS256, we always return that // value. In the future, we might want to make this configurable on a // per client basis. For example allowing PS256 or ECDSA variants. // // See https://github.com/dexidp/dex/issues/692 return jose.RS256, nil + case *ecdsa.PublicKey: + return signatureAlgorithmFromECDSACurve(key.Curve) case *ecdsa.PrivateKey: - // We don't actually support ECDSA keys yet, but they're tested for - // in case we want to in the future. - // - // These values are prescribed depending on the ECDSA key type. We - // can't return different values. - switch key.Params() { - case elliptic.P256().Params(): - return jose.ES256, nil - case elliptic.P384().Params(): - return jose.ES384, nil - case elliptic.P521().Params(): - return jose.ES512, nil - default: - return alg, errors.New("unsupported ecdsa curve") - } + return signatureAlgorithmFromECDSACurve(key.Curve) + case ed25519.PublicKey, ed25519.PrivateKey: + return jose.EdDSA, nil default: return alg, fmt.Errorf("unsupported signing key type %T", key) } } +func signatureAlgorithmFromECDSACurve(curve elliptic.Curve) (jose.SignatureAlgorithm, error) { + if curve == nil { + return "", errors.New("unsupported ecdsa curve") + } + switch curve.Params() { + case elliptic.P256().Params(): + return jose.ES256, nil + case elliptic.P384().Params(): + return jose.ES384, nil + case elliptic.P521().Params(): + return jose.ES512, nil + default: + return "", errors.New("unsupported ecdsa curve") + } +} + func signPayload(key *jose.JSONWebKey, alg jose.SignatureAlgorithm, payload []byte) (jws string, err error) { signingKey := jose.SigningKey{Key: key, Algorithm: alg} diff --git a/server/signer/utils_test.go b/server/signer/utils_test.go new file mode 100644 index 00000000..7162447c --- /dev/null +++ b/server/signer/utils_test.go @@ -0,0 +1,50 @@ +package signer + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "testing" + + "github.com/go-jose/go-jose/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSignatureAlgorithmFromKey(t *testing.T) { + t.Run("RSA private key", func(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + alg, err := signatureAlgorithmFromKey(key) + require.NoError(t, err) + assert.Equal(t, jose.RS256, alg) + }) + + t.Run("ECDSA P-256 private key", func(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + alg, err := signatureAlgorithmFromKey(key) + require.NoError(t, err) + assert.Equal(t, jose.ES256, alg) + }) + + t.Run("Ed25519 private key", func(t *testing.T) { + _, key, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + alg, err := signatureAlgorithmFromKey(key) + require.NoError(t, err) + assert.Equal(t, jose.EdDSA, alg) + }) + + t.Run("unsupported key type", func(t *testing.T) { + alg, err := signatureAlgorithmFromKey(struct{}{}) + require.Error(t, err) + assert.Empty(t, alg) + assert.EqualError(t, err, "unsupported signing key type struct {}") + }) +} diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go index c81c9484..8a06006c 100644 --- a/storage/conformance/conformance.go +++ b/storage/conformance/conformance.go @@ -2,7 +2,7 @@ package conformance import ( - "context" + "crypto/ecdsa" "reflect" "sort" "testing" @@ -16,6 +16,8 @@ import ( "github.com/dexidp/dex/storage" ) +const keyRoundTripPayload = "storage-keys-round-trip" + // ensure that values being tested on never expire. var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100) @@ -86,6 +88,45 @@ func mustBeErrAlreadyExists(t *testing.T, kind string, err error) { } } +func isES256JWK(jwk *jose.JSONWebKey) bool { + if jwk == nil { + return false + } + + switch jwk.Key.(type) { + case *ecdsa.PrivateKey, *ecdsa.PublicKey: + return jose.SignatureAlgorithm(jwk.Algorithm) == jose.ES256 + default: + return false + } +} + +func requireSigningKeyRoundTripUsable(t *testing.T, keys storage.Keys) { + t.Helper() + + if !isES256JWK(keys.SigningKey) { + return + } + require.NotNil(t, keys.SigningKeyPub) + + alg := jose.SignatureAlgorithm(keys.SigningKey.Algorithm) + signer, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: keys.SigningKey}, nil) + require.NoError(t, err) + + signed, err := signer.Sign([]byte(keyRoundTripPayload)) + require.NoError(t, err) + + compact, err := signed.CompactSerialize() + require.NoError(t, err) + + jws, err := jose.ParseSigned(compact, []jose.SignatureAlgorithm{alg}) + require.NoError(t, err) + + payload, err := jws.Verify(keys.SigningKeyPub) + require.NoError(t, err) + require.Equal(t, []byte(keyRoundTripPayload), payload) +} + func testAuthRequestCRUD(t *testing.T, s storage.Storage) { ctx := t.Context() codeChallenge := storage.PKCE{ @@ -730,54 +771,69 @@ func testConnectorCRUD(t *testing.T, s storage.Storage) { } func testKeysCRUD(t *testing.T, s storage.Storage) { - ctx := context.TODO() - - updateAndCompare := func(k storage.Keys) { - err := s.UpdateKeys(ctx, func(oldKeys storage.Keys) (storage.Keys, error) { - return k, nil - }) - if err != nil { - t.Errorf("failed to update keys: %v", err) - return - } - - if got, err := s.GetKeys(ctx); err != nil { - t.Errorf("failed to get keys: %v", err) - } else { - got.NextRotation = got.NextRotation.UTC() - if diff := pretty.Compare(k, got); diff != "" { - t.Errorf("got keys did not equal expected: %s", diff) - } - } - } + ctx := t.Context() // Postgres isn't as accurate with nano seconds as we'd like n := time.Now().UTC().Round(time.Second) - keys1 := storage.Keys{ - SigningKey: jsonWebKeys[0].Private, - SigningKeyPub: jsonWebKeys[0].Public, - NextRotation: n, - } - - keys2 := storage.Keys{ - SigningKey: jsonWebKeys[2].Private, - SigningKeyPub: jsonWebKeys[2].Public, - NextRotation: n.Add(time.Hour), - VerificationKeys: []storage.VerificationKey{ - { - PublicKey: jsonWebKeys[0].Public, - Expiry: n.Add(time.Hour), + tests := []struct { + name string + keys storage.Keys + }{ + { + name: "rsa signing key", + keys: storage.Keys{ + SigningKey: jsonWebKeys[0].Private, + SigningKeyPub: jsonWebKeys[0].Public, + NextRotation: n, }, - { - PublicKey: jsonWebKeys[1].Public, - Expiry: n.Add(time.Hour * 2), + }, + { + name: "es256 signing key", + keys: storage.Keys{ + SigningKey: jsonWebKeys[5].Private, + SigningKeyPub: jsonWebKeys[5].Public, + NextRotation: n.Add(time.Hour), + }, + }, + { + name: "mixed verification key algorithms", + keys: storage.Keys{ + SigningKey: jsonWebKeys[6].Private, + SigningKeyPub: jsonWebKeys[6].Public, + NextRotation: n.Add(2 * time.Hour), + VerificationKeys: []storage.VerificationKey{ + { + PublicKey: jsonWebKeys[1].Public, + Expiry: n.Add(3 * time.Hour), + }, + { + PublicKey: jsonWebKeys[5].Public, + Expiry: n.Add(4 * time.Hour), + }, + }, }, }, } - updateAndCompare(keys1) - updateAndCompare(keys2) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := s.UpdateKeys(ctx, func(oldKeys storage.Keys) (storage.Keys, error) { + return tt.keys, nil + }) + require.NoError(t, err) + + got, err := s.GetKeys(ctx) + require.NoError(t, err) + + got.NextRotation = got.NextRotation.UTC() + if diff := pretty.Compare(tt.keys, got); diff != "" { + t.Fatalf("got keys did not equal expected: %s", diff) + } + + requireSigningKeyRoundTripUsable(t, got) + }) + } } func testGC(t *testing.T, s storage.Storage) { diff --git a/storage/conformance/gen_jwks.go b/storage/conformance/gen_jwks.go index b5affcc8..990f3c54 100644 --- a/storage/conformance/gen_jwks.go +++ b/storage/conformance/gen_jwks.go @@ -7,6 +7,9 @@ package main import ( "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "encoding/hex" @@ -44,14 +47,14 @@ type keyPair struct { Private *jose.JSONWebKey } -// keys are generated beforehand so we don't have to generate RSA keys for every test. +// keys are generated beforehand so tests don't spend time on crypto key generation. var jsonWebKeys = []keyPair{ - {{ range $i, $pair := .Keys }} +{{- range $i, $pair := .Keys }} { Public: mustLoadJWK({{ $pair.Public }}), Private: mustLoadJWK({{ $pair.Private }}), }, - {{ end }} +{{- end }} } `[1:])) // Remove the first newline. @@ -60,42 +63,73 @@ type keyPair struct { Private string } +type keySpec struct { + count int + algorithm string + generate func() (crypto.Signer, error) +} + +func newKeyPair(algorithm string, key crypto.Signer) keyPair { + priv := jose.JSONWebKey{ + Key: key, + KeyID: newUUID(), + Algorithm: algorithm, + Use: "sig", + } + pub := jose.JSONWebKey{ + Key: key.Public(), + KeyID: newUUID(), + Algorithm: algorithm, + Use: "sig", + } + + privBytes, err := json.MarshalIndent(priv, "\t\t", "\t") + if err != nil { + log.Fatalf("marshal priv: %v", err) + } + pubBytes, err := json.MarshalIndent(pub, "\t\t", "\t") + if err != nil { + log.Fatalf("marshal pub: %v", err) + } + + return keyPair{ + Private: "`" + string(privBytes) + "`", + Public: "`" + string(pubBytes) + "`", + } +} + func main() { var tmplData struct { Keys []keyPair } - for i := 0; i < 5; i++ { - // TODO(ericchiang): Test with ECDSA keys. - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - log.Fatalf("gen rsa key: %v", err) - } - priv := jose.JSONWebKey{ - Key: key, - KeyID: newUUID(), - Algorithm: "RS256", - Use: "sig", - } - pub := jose.JSONWebKey{ - Key: key.Public(), - KeyID: newUUID(), - Algorithm: "RS256", - Use: "sig", - } - privBytes, err := json.MarshalIndent(priv, "\t\t", "\t") - if err != nil { - log.Fatalf("marshal priv: %v", err) - } - pubBytes, err := json.MarshalIndent(pub, "\t\t", "\t") - if err != nil { - log.Fatalf("marshal pub: %v", err) + specs := []keySpec{ + { + count: 5, + algorithm: "RS256", + generate: func() (crypto.Signer, error) { + return rsa.GenerateKey(rand.Reader, 2048) + }, + }, + { + count: 2, + algorithm: "ES256", + generate: func() (crypto.Signer, error) { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + }, + }, + } + + for _, spec := range specs { + for i := 0; i < spec.count; i++ { + key, err := spec.generate() + if err != nil { + log.Fatalf("gen %s key: %v", spec.algorithm, err) + } + tmplData.Keys = append(tmplData.Keys, newKeyPair(spec.algorithm, key)) } - tmplData.Keys = append(tmplData.Keys, keyPair{ - Private: "`" + string(privBytes) + "`", - Public: "`" + string(pubBytes) + "`", - }) } + buff := new(bytes.Buffer) if err := tmpl.Execute(buff, tmplData); err != nil { log.Fatalf("execute tmpl: %v", err) diff --git a/storage/conformance/jwks.go b/storage/conformance/jwks.go index 28ce5f72..afccbc0b 100644 --- a/storage/conformance/jwks.go +++ b/storage/conformance/jwks.go @@ -1,35 +1,38 @@ -// This file was generaged by gen_jwks.go +// This file was generated by gen_jwks.go package conformance -import "github.com/go-jose/go-jose/v4" +import jose "github.com/go-jose/go-jose/v4" type keyPair struct { Public *jose.JSONWebKey Private *jose.JSONWebKey } -// keys are generated beforehand so we don't have to generate RSA keys for every test. +// keys are generated beforehand so tests don't spend time on crypto key generation. var jsonWebKeys = []keyPair{ { Public: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "8145b5b9243c41459a8fdaa12acbd371", + "kid": "10cfa7ef3f1d48d4896fd2fcdd4a064d", "alg": "RS256", - "n": "34ls8E4onyEU_JKcxl8BMu2N6hK_D6aG2tOuCHJ_ka4rom8NmdJGdOQPC_fvKhcAxWeDktdAPislTT76Q4iMCC7DbM1aQhgRMaecKHBagc5ue2kSPM3oZPLqe6X-CxdxGTfXAvFIZM9JZTbQeJPcXFdn28iZ086xWPMdQKY5QTRKtoHQSN6EAQuuiuZsXrAC3lBZmE4tda6NoeYLb0UayGqiiFmtoIFJQ4NecI-EECT-mcjkPGWG0Ll5dCIUhGDl8sQSUrmBuaTDpPEzLGo-UtM3ay7AN0gOVN0mLIk2oyroXcVOA626LYNLVU0mz9PDpdkhWBeUfLL6i4HjUS3RaQ", + "n": "nWwoxydjr1ihXg0jnflEoSwOwyR4VRvHJIkEFnFx8ibt6IQJE0oXdSY1RG0yUddTb0Wfgqwp_-sXHEjG_tQbOTLWQoW2f8wQIw_SF3eq5fhk_uwv_tGB980sMKQA3Er13FEr2qttbWn5xYaoglDgjNC6DmxFGjSgjJaRKZGeQ0zXmt38GSHmA3IIWps2GftWPRnSKXj27NPIhw-ZKLkAibCdm2zcqyzy5YCXitymhnGCnlT6LQUCbKaZ0ocbnAY4Ttb-pg5toYMq-jps9tdn58ETT9hBsoagY7x8fy-nZZgdg_CNwqfBnCRbqLeeFJRUR00fbSaxjRIsf1RtMz54Sw", "e": "AQAB" }`), Private: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "f547defc90b34ec08caeb8b294591216", + "kid": "a8481527703a43d8aa8f818b80ae23ba", "alg": "RS256", - "n": "34ls8E4onyEU_JKcxl8BMu2N6hK_D6aG2tOuCHJ_ka4rom8NmdJGdOQPC_fvKhcAxWeDktdAPislTT76Q4iMCC7DbM1aQhgRMaecKHBagc5ue2kSPM3oZPLqe6X-CxdxGTfXAvFIZM9JZTbQeJPcXFdn28iZ086xWPMdQKY5QTRKtoHQSN6EAQuuiuZsXrAC3lBZmE4tda6NoeYLb0UayGqiiFmtoIFJQ4NecI-EECT-mcjkPGWG0Ll5dCIUhGDl8sQSUrmBuaTDpPEzLGo-UtM3ay7AN0gOVN0mLIk2oyroXcVOA626LYNLVU0mz9PDpdkhWBeUfLL6i4HjUS3RaQ", + "n": "nWwoxydjr1ihXg0jnflEoSwOwyR4VRvHJIkEFnFx8ibt6IQJE0oXdSY1RG0yUddTb0Wfgqwp_-sXHEjG_tQbOTLWQoW2f8wQIw_SF3eq5fhk_uwv_tGB980sMKQA3Er13FEr2qttbWn5xYaoglDgjNC6DmxFGjSgjJaRKZGeQ0zXmt38GSHmA3IIWps2GftWPRnSKXj27NPIhw-ZKLkAibCdm2zcqyzy5YCXitymhnGCnlT6LQUCbKaZ0ocbnAY4Ttb-pg5toYMq-jps9tdn58ETT9hBsoagY7x8fy-nZZgdg_CNwqfBnCRbqLeeFJRUR00fbSaxjRIsf1RtMz54Sw", "e": "AQAB", - "d": "3rABHsQ-I4jJZ3SHSfeLMjkFj5JtVCIJZiZK0Y9_Fpn0TjVjz0Fzfy9S7hFo6P1Rf1bH9JkLHuPMnU-H8Y8uMVikxtcse3uOZXEcWAzVnUsRNVBPItPeF_MHNXb_xfzsZrsCL6Q_Am6eJ36b4AMtG7DXflQxKphWhM5s7eKqVxDrkhaDPnALLRFjCvUZ_myQQ3Upn7gMgAbvfIY1fn9rXW_4CfxbxhcPJW5IOcu6bPvpQlfuFkXjF-gGCiNf5kv6Db0lpDOKX5l5T-KFGQ0dIOdasm8vL2GxCKZf55rKRCt0a28fwwH2p94ja-1qtPTc34V8F26LyVRgQgD3e-0aoQ", - "p": "_WoAr3sgL5yfaqBL38yqx4hqSPZGdR6xTS64rhgZaVg14_W6xYmlPI7PmVBRW45Fk4tXhXjv9oMZH9HGrH2v4yqXLEq0gJr4VAPvRaN6p_kb_eCfLHCbNCYBAPNVUdFpOTvOmh7m0zYPrku7DZDnZQEN_A9hYcufjy0em-lV6Tc", - "q": "4dFfwyYQmns1xwVEPABxpazk6nAluS-7yYSAc9A8D25nqm0mNWdPJvmpJS02xSDjIGfe0FtMr1XlPm3XHdUlIu2Z9Ex-J-kcs3lfs2UKmleQqJRXK4MahAEIV3vp0zG47hAJyzE3Oh4sVLFr3ZK9_-SenolCFv5eIikWa3Xg6l8" + "d": "B61Kn8uv9NEhrXB-mCmbyzBW1_VjWOMt5v43kNCfeeukFn654pLuaewfFOMuXQdfLkH68Whr8-sHCX4TFkJJwZRVFpFmwPy2nL8cw8A2OVjPtA7YmFinOepriUO2NwgAzWm39cX-ZCOS5qsWdKR1Duju6d0l0Y1XdwOwuJI9YZK-LE9JusdPHO4ME7JR5NHFmop1VWnt27QPs_VijW6V-_jnSqD-w1p_gnc0tEi3r7cFGjpbOh_JbxWG3WrX8CTmbl7T-j3kCkMkzE9mYAPTxMmHE80ooy4fBXyd4CkoIGeZe8r4JNowUneSCyicI_1t5PlmUAniEv51azq_bM5IIQ", + "p": "yn_xAS2PH7Ko6YLbhNrVowAeLBaujYfJFiJxSqYjMlyDwSiLZ_6rhULNFKT6GRI6UPLDUEwH4B8U8ACcSRg8Vpy0N8wQRuT71NBWYUPL39oc3Jp7ajdT_dv8kcRB_hc5u2XPdP84BFkH-sUfl1CzbaBZ5MAA0jqnjYNFanRImu8", + "q": "xwNrYABQ8OjiB_RkkjycX-i2c8fgQdrLoCDtPpW30IjmHJD5sdeom7Iu7pfp7JQJ87uh4zEq_JYQEMDMmJp4SyqBbV_FQfVOD2YcOJoyozz09ymmLaRO-WPk0GUjdzLWD9LMhBykMN_LmL2S9MZQ4-V8GOtKOpDaFlvYVHvcKGU", + "dp": "eF8XNvkLA--iwrP7o3yl06_lP5X4Cr91hAfTSml5sJ0X4MEmJRpYgO2VlLkAxdh0-9tiyJ95avtu6b-jJzwV2fJqmmReJJZHMFjrkAPJ8_XmhCf8RsY-0j9rYobEJ7NMqR269TQk60i1GpcE0WCyV-8ioHyVwGIHaXJn2ADpi9c", + "dq": "eMk6gimu9ohhpzJNV_QxdIRpOBw3n8CLlcRmOXXk-kwcXXogEjAxxU1_7o-FCnFPGP7oYluVG3h4h7J8eqJGIFjL9PYLYtzfY7k1p6Tu1uRISTqeAVWYGQTn-xNnUxEnmrL2Lbi0bnVlvG5Sov06WxcNGpKzmpgPxUI5Kg2CsYE", + "qi": "BeF87zRr0j7Ff-pEJMDAYINJou2YcCBsZoyA0COKIohVOh-pkTaKfVkJ4ndWShuGOgatjqSbALxn3Z0NDX6VvTgpnMJSBuMNtkzyzeVHahRA8PftfShar8i2l_xb__gSZmr57-YgNe7ditoDu-EngcaebxmuFV6xQn9YkG1Irkc" }`), }, @@ -37,21 +40,24 @@ var jsonWebKeys = []keyPair{ Public: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "3a9365e41b114ec1b9288b214196e158", + "kid": "21fd15ddab654e3cab72d24f94925ea9", "alg": "RS256", - "n": "t3TrxLN5_z-x5X9kebkoPnoYnGAPqAXOVCGBTxcAqev_P8t6SyyeeITDiePhCctYp5dO-WHRkB7_BkUeHZOgoyCBarDkDifQSG7MCtlYDm0yiSij_0vqzJQx-6zlXb5ypwO0P1sAXrO_nO87u69w5yaKf0yEJMpSjU8BDKQ__nskZP2QJJsYwOeAI9aAM2oP8r7Im8KzLy9-mnFSqypxBnL24hFNzKOS_GyHs0tPLjVY7JNDtDOkwPQIQFzsdZSY88n6uYvV-MGu3O-Y3-xLwUqMlJOXFskhmp1AOUnb4JgQ9wEaZ7088PY3Ak0eZkrg2FQ3XRHSWhUCOb2xL5iTvw", + "n": "nv9zOaDskza-NEBerrj_TSgbWx_slrra2UXXNcMFLkZASwU0K81G8iipM5cikbSHAd_4z2Y31NLpIz_-mOHPdyqKRCC9kWcWB9bfrZ06ey47Wvf-_wnCuLKXn9wPYZJwhSSPPrvy0146hVEaZOEpDYqMvRHxY5uU_gl_VLkAGqQKrP2YBNf8ubrulkzMEkuWHySHVrBD1KkJorw4gno22vtszRPjPapsdhpfz1Wm9W4NVOTdxyPAHcyQV5j0Tkmj9xPM6BBWHMpl5iLZpXpMoPAGlGCIX26WNZjsrNRoPDTB478g-SrVzuxR4w78HJ56rbazfseqpFLvNMMlXtWmtw", "e": "AQAB" }`), Private: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "c79418aaf8ee439bb2b0e28672d71584", + "kid": "d7ea68c557754ae19cb40fa114067137", "alg": "RS256", - "n": "t3TrxLN5_z-x5X9kebkoPnoYnGAPqAXOVCGBTxcAqev_P8t6SyyeeITDiePhCctYp5dO-WHRkB7_BkUeHZOgoyCBarDkDifQSG7MCtlYDm0yiSij_0vqzJQx-6zlXb5ypwO0P1sAXrO_nO87u69w5yaKf0yEJMpSjU8BDKQ__nskZP2QJJsYwOeAI9aAM2oP8r7Im8KzLy9-mnFSqypxBnL24hFNzKOS_GyHs0tPLjVY7JNDtDOkwPQIQFzsdZSY88n6uYvV-MGu3O-Y3-xLwUqMlJOXFskhmp1AOUnb4JgQ9wEaZ7088PY3Ak0eZkrg2FQ3XRHSWhUCOb2xL5iTvw", + "n": "nv9zOaDskza-NEBerrj_TSgbWx_slrra2UXXNcMFLkZASwU0K81G8iipM5cikbSHAd_4z2Y31NLpIz_-mOHPdyqKRCC9kWcWB9bfrZ06ey47Wvf-_wnCuLKXn9wPYZJwhSSPPrvy0146hVEaZOEpDYqMvRHxY5uU_gl_VLkAGqQKrP2YBNf8ubrulkzMEkuWHySHVrBD1KkJorw4gno22vtszRPjPapsdhpfz1Wm9W4NVOTdxyPAHcyQV5j0Tkmj9xPM6BBWHMpl5iLZpXpMoPAGlGCIX26WNZjsrNRoPDTB478g-SrVzuxR4w78HJ56rbazfseqpFLvNMMlXtWmtw", "e": "AQAB", - "d": "T7-y0dIXQV8l7RbAza0wkmAvHKMhiy_i7m2WMZRVRIiDb-77HXyq8sb73ZBC_if4RPogaYYdPCJNSCN5oO_Qz7jMqV119bVW9HW9myW6AqNzaW5SRCNzUTVGuRoCpwqn-nRAwZ3EfmZy8DyK4d61HLaDVC0l8HxHAIiMcztfWjbfD2LjwWF2hF5VRG2-haDfT6Kwtz0zEXblvYxyPqVyKOFtuWDlzX8iP8_ryWaChpR-jTmwtm7663wcu4M9teMkdgubCIqkz0LLtd-97ZUM2ti70WO7AEqE6p1evnjfYt4HZpQlsn0psrgGLvX2oCIvmPQMfTjzmtsEC51F5CU-yQ", - "p": "4xi5OdCP9n1ivD3CuMhcaoMrwkC1yVdYnJwaNXjIyuSUT0i_QmuRpViydpZsfiYEoNNczL_PwxlDNdl2ccbelBuoEDbrvAfz0G0-YVYuLJoEKQs_OjenIn_6AZlmn7zSQ0LjoZ1tTjOaKuueB2b8RVtF2pbZ_o1ApyWd3q6QjyU", - "q": "zs5SF-jdzP9xThPTEmAa2yh6SI48KuwVwWXGjOQZThXVEfwo-iZNevPjg3b6gwY9fKi71-J75c1ng0QrgdDuRIackHFpSLaWgcIpN31-uyZl5X-uxpBZON1HeiYT8J2JhgbA9ZJ0_SUq3j4YSrFEGKSpBi741mqwS9CZ6NSN5BM" + "d": "Cevjy0lM6eTFGqqXneyCVdS2XEnSMBep11wB0WOBe8E7etLwzfjpv-ukn0kDibcLo6TPuFVnD3s3vBDeuLiCgyue5CBs49lcaRHR9Fn2z7_zSTpnaY-43GCckIehWBPUvKIq5B3DScg7-5yzem8IakVmgdfQSchoFDAQh39hz8eqhaz0BsaOhSZKKqDXypkbxEwF4KnTIbqej0CILImjHLW8dTKsQRVuW2t4SRruvn-fQJMTicRbG1CnL4FSuDRY1t41HQDMASYdjEDKxz3IVRW-6LHbR_Lwkz5fR_Fttws2wC-fwXDaruIzs_nDNuv5r9YosnHZpN7ajgtt7NYiWQ", + "p": "0jDXyRVaG32RbN3AqeYmLk-Btt-pq7wwLl3ethjfNMT3gMYuExchAsiS86DCoMsS_SV65gyBqsl1jmvVCqR9HZl0vIQ9fuUxtVDVy-EWf5Lx5NxSSzNU4WC4vvbkkRxaN73fKbbIfj-k61BuNU8xL67Dd_eZS3oNNiXGT_6XES8", + "q": "waZnXlYWOmUVLGcy7zhj-2Ua5nT6hnhz1Jw3iCrUhw57SdYP4gGvehaU3lvB451r8whs3xsi-7czA0nJ-Idle83j7Knww0BaVBnVdK2pmDqnBm3r35kyfoPgHUo4ujSMFZAdIQpRztvwYdXcpBRfE3toQoheGSJKhDMjxa2dEPk", + "dp": "qhC-54IRMCD_I1ig5FbFGb9WssJxI_TV_w4QfsHNB4M-xsCS0vtpyDjFPb4a-8KTkKNctvnziF4Mcbmp3DyOxv48x7MvlpaXC59l_NoYPAyHD6d8JkBYMyrxEAHvmcGY9XT5NWUg_5U7OPIQ2a1DnNMIcO6y57DPM7qIpHzXeh0", + "dq": "NGsi1_pdkfkCvj5BH1gYWFiJ65AGsJeyYv7WWVmepeBZpyb2rarfISEzsu0Lkt1t9x90uP_TfGeeu2kh7tBXvCeDZvCpZ3DoApPrn-XSXo6h36-phaEmdfCknckVifHnaX7VI7hzZJdMfm8xhoitI8zn7_qR3iPWH-rn5_6S4aE", + "qi": "XMIySQMcYQBr1u8J_X97CJnknxelXWmux5B7hR9jtwvT7r4d0rzzupqNMCApdC3fCsCQrrdJ94JNaOPHOrM5lL_1UL4_OU2n1PYwpxyqj4KPvEfoewGGyTXj-_2NP8uhzW-nATVgX65ZIg_LyxcJt6ym6qVdv-4cdfMJ30jNNIQ" }`), }, @@ -59,21 +65,24 @@ var jsonWebKeys = []keyPair{ Public: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "4c267fc23c7b44d6973a1722b7201849", + "kid": "67c85e968f3240f59a62084fadc8b5ae", "alg": "RS256", - "n": "yeZexEF1gOXd71iz9jRQR3EhgM2-o3mVO4O1fJYYQTh5APfrrbMhOGLvgK06vytREiY9_1awL7YfEnZzQynq9WTZpkwlAhYujHYf1RbGPeoXJS2cXKThfIhbeITEyhfepqzwU_f-RhvaLS3bydDi7F74oTO9njtLkGV2qNHH3B2uTFBy2G8VmDeHNQrUa868LQ9omrmWFkLnoZOoVPiLZD-5aZXOKJ0In5sg9B1EX1oaF-xejCTBX_8EJvvvKXH-GUZnHc3g3Rf3k4iXCJi8VMyjA8we3fgP8jp2P3Ofv6VOKG3vh8j5lI3ys_rctc2fu6CaNWNNZs9wbjpDVPuc0w", + "n": "tUKibQUIXBTKnBS7MryiW6VPDcAszUwYvKZcVw0eVtQa1wDUITBo1-_VJ6fRXd5KLvKOgDxSg9wJgcN2Qm94HCK_JRvEiNQ-bruVVrQ4rk1V4Y9QkG3wJ3osh-srrO551mAocpKKS4tQ6Dojh9wq7y76TRu__r2bGcylR3Ilotzdp9TckC-6TeW2LT7gkevDZwuJWGojzZEmbmhH-Jt1FTtq2oKP_ZtPbHoyJsJ_HrqluDi8EuiT48j2CwpGnfPOkaKkVtHSpsG39_bcgxTMzwDnEVtALDHTIB2LMAEiKycSvppLaH_lCx6IACJLE3LkJEO4O59JxpNGOfggEKU3Yw", "e": "AQAB" }`), Private: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "eec6ee158cb34d699be4baff419da383", + "kid": "10e9bda24041486e83c06e8b804523b5", "alg": "RS256", - "n": "yeZexEF1gOXd71iz9jRQR3EhgM2-o3mVO4O1fJYYQTh5APfrrbMhOGLvgK06vytREiY9_1awL7YfEnZzQynq9WTZpkwlAhYujHYf1RbGPeoXJS2cXKThfIhbeITEyhfepqzwU_f-RhvaLS3bydDi7F74oTO9njtLkGV2qNHH3B2uTFBy2G8VmDeHNQrUa868LQ9omrmWFkLnoZOoVPiLZD-5aZXOKJ0In5sg9B1EX1oaF-xejCTBX_8EJvvvKXH-GUZnHc3g3Rf3k4iXCJi8VMyjA8we3fgP8jp2P3Ofv6VOKG3vh8j5lI3ys_rctc2fu6CaNWNNZs9wbjpDVPuc0w", + "n": "tUKibQUIXBTKnBS7MryiW6VPDcAszUwYvKZcVw0eVtQa1wDUITBo1-_VJ6fRXd5KLvKOgDxSg9wJgcN2Qm94HCK_JRvEiNQ-bruVVrQ4rk1V4Y9QkG3wJ3osh-srrO551mAocpKKS4tQ6Dojh9wq7y76TRu__r2bGcylR3Ilotzdp9TckC-6TeW2LT7gkevDZwuJWGojzZEmbmhH-Jt1FTtq2oKP_ZtPbHoyJsJ_HrqluDi8EuiT48j2CwpGnfPOkaKkVtHSpsG39_bcgxTMzwDnEVtALDHTIB2LMAEiKycSvppLaH_lCx6IACJLE3LkJEO4O59JxpNGOfggEKU3Yw", "e": "AQAB", - "d": "IOFck5eZfElzMFSA0lrIrCnXa_OV1WeqjwuvFcAX6R86TZcSkbI3echa-ti7VYDHbi4-MIQ8oziErOwPb2O3OQmYjIWgDUvxfryKCJjx5glmhY59BXVwp2hJhUISDlt-ziQh63ratS46BNuQDLjxC8-XrCESA1_iuXxcq7emVclRKN2DpGehf2bZyjcZy-OEwvL1jLsvoY2jmY_2JOT4nFLqoelg5vENj69p8IR9Bpdzp0urngLZJ4-HqFGyfx3tEo4ZUF1M5xnoycBc5LMZjmElK66rjBRWPq9UwZwfqaeQh6wEA9siYw1V9yrNRUkq3Q6BErbXNDKBV36bRIiaIQ", - "p": "_YirCr3Sfs9FkEFFMNsTZ2Wv8e5napONPtg1WUYOxG36k65EkPtlmZLWmiwmBk6592oND_S5WvbW4BbX5lRbEvNiRy9coVPst6lOOnLe69GJoI_GxoRyu_94qIS-VNPSQkyw4gfA1M-lMdfKpaTMv7fvVolvmDs5xN_fmXpl06M", - "q": "y90gdyUcYzDX1u3-fCINzXbDcr80QEO3bjuG8p7feaYY2MP51t6j6MisNsQqcGKY7xFhpc-z8_cEIg1HJ3FSly-yejPj8RGavPX6NVGVHDNGwxxnm_i3kf-4MuDxwRSSHMlgVNAXuoH-3iicz-bNTVYM-5bYucZMvZHC6Ur2JRE" + "d": "FCIoJ4LhGFVI4ghdeLfCMvrMKqNXdqBGuSGjbRnsbkmWB560cVFOu_mMTCDUXSBVThysHmtU4QeeWLcM2jlGdp2XbLhGXspwet1EK8LN7vJxISJJmRlVDRWBf8Fr8wP0LY81vvAvA1AgtwLKMouOi03FCK6V52ZJJZb5HtP7gTjmT5omV6KFsIjJK758In498PfoQcGEp028sl9i3mh3NN6U7Q20UGQ2rQ2adhsECc9RLfKZ5-1axOJG39R41Udqa0jzd-WxySaB3Rds1B4QQBI9KS188dTmXoATiLSd3DtbeQQ7qkWv4g4ToYp9a2EhKMFqsKQREfnG5stsVxjWFQ", + "p": "zXwRx1uxexVs_Y5K5CLZmnE0NPIHiC4pOUCnmcfbYemLZUcUyEUGNxa2_-R87cMca93Tumc67NBzKtpTLAfHC_XtFBZ7iv_h72wP3Q9EJMPobnqbV5dv_pFzMnGLQk7Rnql7Rg7wF_XTJm15vzHo9MfMxINUQgj0WiZeQ5pGkSc", + "q": "4dIIwEihlCFDFXqerLFiFHNOPWUTBxCc-exJMPMSoD_4DS9SFzIVk4Q55yJgODKMPQJDrFGWl_Z6kc-otvdwTzwqFM1RGpcpY1BoIoM9XPBebZoe1pTwPneLfHbIx2g9MsyHWxHg5C33OCjIestYt5KIGRVEKkqBlFTuK4AEVWU", + "dp": "YUKUd7Qi8YtWpz0unYURUlS6zUSx4_dVc8_yhItgf5u3axyV7aUeJ_0MaVqQ90kerr-0c8pgza5slJ_6NiEISdUBfjyWBVjQbC7N42hSbMR9w60s0ezQCcJ2sC2mKKB9_4dg3ew8IFsuskWnFqRdC192wJ0YyJjMb88xVvYy6Ik", + "dq": "f2v4rCdPpTovlEsghemBonB90X7bAb2KFiwSEKDX_byaxzXQaG5GZnrSAW4QRSWgpDxG21EeXJkkCGrYlIWCxfL4-8vu28Y4AGY6nF7ZnTu0zLuOcx6PgXALMSwFlx9miOeH6QdpktetC-9XUJK1eMAiM6UgVYy9GWnjCoCt8VE", + "qi": "vgxFfr9PFlh9zB2FjvLX8ev03Vo-o0G3CXgJC8fTH-RkljzYyvD96PT9MWKjEAZqhG5sMs6oLwh0SNf7arkm50forZGJkVV8cTLrTDPk5ZBqE4YcVsMcmAhC3VdH8ujcFbDbTocO4o5sRiaK3Iyqw-bM8zOOnRxhHQh8cbOjCmI" }`), }, @@ -81,21 +90,24 @@ var jsonWebKeys = []keyPair{ Public: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "e10385c5384046f395fc6d9027db2f35", + "kid": "5942c970c64842d39306c9a359be8ab0", "alg": "RS256", - "n": "299cgJgPiu9CK8hGgQw3j8e-Y_u4-Tm6WXKOFHdjCUPV5EAWMOa34cQNt75KN8pxlIcnujnU6TpH4OPRCw1gA44rrk_uczIEULsTnt6UFuMtUY2r-2UW2BWg5rEHyLcNX_QCA80T9DVSxsWeN8S23YcVk9fVputIRU7ee7auOx3b6K3pkoQJBVUk-_ndaqwlX-JU2CQG52CH91CrDzN0WGUPrhMZOdL7ybv94l5ztBrnjaQupkt0FxTA1_m_tXTvxIgzzegaqXrJ1mJM-z2TxPUJUc_04JaGilPUkxU780jk_03d46Op-pdElgbZ52C9JT9b8nRnA-vHq4e2whY8Yw", + "n": "3D1Z0EEqUux9HyngYQjFjC09sP53KIt1PG2hXUZJqn3IIsR3UPcUVvpOARlfWOsDss9SlZiInvPLXtTsL3K89AalT9Au34yLYVzr1JnkC1W8rU_ej0EYm3QnqT0EH_6nV4bvupIzip4rB4hx6oDu3EDCb2a83FLmntyKgV7HTFH7HUWoGcIQkZ-X3NczRWAfVnMnsgMIUzSVOtIMdcbJZjgvQoCZYUzMljjd0HKPnxZzrzDp76Syr7cLnkjuApHLUwi2sDSaL38oa4jsYseqCOAq-ItltuyLE42LoS1Hwdz4NI30cDkyvK5dFFu7PI8W-bH0rkNqEUO59kJGJtStFw", "e": "AQAB" }`), Private: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "8165cc507cd1492394be64575dfa8261", + "kid": "fe85223c6d054fc4ae0e8cfa38c3eca6", "alg": "RS256", - "n": "299cgJgPiu9CK8hGgQw3j8e-Y_u4-Tm6WXKOFHdjCUPV5EAWMOa34cQNt75KN8pxlIcnujnU6TpH4OPRCw1gA44rrk_uczIEULsTnt6UFuMtUY2r-2UW2BWg5rEHyLcNX_QCA80T9DVSxsWeN8S23YcVk9fVputIRU7ee7auOx3b6K3pkoQJBVUk-_ndaqwlX-JU2CQG52CH91CrDzN0WGUPrhMZOdL7ybv94l5ztBrnjaQupkt0FxTA1_m_tXTvxIgzzegaqXrJ1mJM-z2TxPUJUc_04JaGilPUkxU780jk_03d46Op-pdElgbZ52C9JT9b8nRnA-vHq4e2whY8Yw", + "n": "3D1Z0EEqUux9HyngYQjFjC09sP53KIt1PG2hXUZJqn3IIsR3UPcUVvpOARlfWOsDss9SlZiInvPLXtTsL3K89AalT9Au34yLYVzr1JnkC1W8rU_ej0EYm3QnqT0EH_6nV4bvupIzip4rB4hx6oDu3EDCb2a83FLmntyKgV7HTFH7HUWoGcIQkZ-X3NczRWAfVnMnsgMIUzSVOtIMdcbJZjgvQoCZYUzMljjd0HKPnxZzrzDp76Syr7cLnkjuApHLUwi2sDSaL38oa4jsYseqCOAq-ItltuyLE42LoS1Hwdz4NI30cDkyvK5dFFu7PI8W-bH0rkNqEUO59kJGJtStFw", "e": "AQAB", - "d": "xh587o6WKr2uZV8gUHXettroLpWKtl-TD7hOWBi_j4ClgfdRR50NggwzxCZeH-l18LzcSkyEEefnDriZC5lws6NurrHtjbU6-Dep1VSAIiNwGXVLy8nqDKlog5ZvCigPkC-BhUVMPpexz9QP3faORAzNn5szNCX7yB_qD5WrZy20AUEoWtGPgxGW6xf5Lgu6zg2uQEEB1Z0hKjHV9seIiuQooMrSzpS1D7BLSTHOvM2Y2lXvQQokc3uQXnyT_soHPjHl00bcuJLJaRCmyHRTol7uh9MNe67eMy7pHYmmlwOvTDfW6meKCgoEXd1wKIrS9VRY7WP36ZRpJH6qv8vceQ", - "p": "7sWEsknUaSlAJ-bGhsuFr_j15zupV9O-DLnLobASm4Z7Ylt1HhtPN1NCVzYFTCtltPBE_CXGaAPqw3wiERK3tgYSLV8yk57sU1H28Zsq65A1B-vdlO69-F_6djiGegYKTOO4CXt0VYB4hJ6Trwx_BNJmrAD_Ykjqsp5sR0gOrqU", - "q": "67y_hzbi81IH2DxmHTQOfHgcLYe-TnrEQLGLQtfx8J0J_REf_fLBDL-pt_jy6WIvTAb-LgUIcieiXfhni1nPUw0f_I1SDNv02EYvP0vkfyQdJBR6sLi4jv0mpqyQxvGif9B4eM9Qjngm2Jclj3-el-RkMZOUyf3zGTNGLI3MmGc" + "d": "N03spMAIacRa8x1n69XxDW8645wyveqvKNp6v9prmzV0sU5Wi-1PnTvDpMEDUWfKiPdIzKn9_LRJqGmdWZjdXAXjMzSa4KYBRhS4X625NyNzeq3ovF5jWH4Fi5j31TT0PqVyw439BV0MfxrDSFjSIlrcuG67FDR5FwFYgn4TW9Vu59EaULbno6U53BQHv_a2fuSTIH06zsI1y2Pp3AfCUdxjEviRVbmY-Zdd_qqdJV5b3h8henpABWDhdvMAyXdpOlxJqyTxs9-cud0vUl1MWgR14OUhWOMhGeGbuMK-EMTkaQlYP_-3teaKEzu4sc2YDmlstePBXydYjGSMQzHnEQ", + "p": "-jp9GP9Rqk3eB0KISm7sbXganiIXbpgaSFN7o9rYFgWnjMhAHizluRCX_Ffba-oIDPPXgQK9VKTtrmmrz7mNWC2eYuSVzgPfl24TUA8c1xGoUvISBD_Y6lN0HmljoLapx02u_18BEgKvTcskK09aZLN4YSaMf2FOwA-QvNtASg8", + "q": "4VHJ6QiX39Lm1BQgZpMOtzXUeB-OVutbMK0h92xBd5JNE8uRKfPo6KHLYqUd6roem012wQ3yww0PKjd0AjsjfPchD0ATg6cRgKI2DBX-sHFVTtWJKn22J2p0Ry2zTTuYKQQmikWocccTJHn0yGK1g-cYKSfKd1fikBhsbYBPlHk", + "dp": "qB6j7iects-aXUZWS6w7LVGEkLpXA_ctsWplp7FhfWpPKEdizONddPmxI9njkp2qywkJwaQVkMk-5_AXf3krfhMoV0k9XS09DIDoeOGuftFeRlxdvn0nQEjRu50TUudnKoEHEozCK1eicKILqw2lrgLm3l8IBo1aW7AZjsnAaAE", + "dq": "qA1IevZd0CKM50DbqaWlJCCSB8YBc_K2YOfAnbmwWm5T7p-19YRmApwqBbkBR0Dp8wsjt3mfxrrGxMX3UHKrVVk83xL7ewXwl_BkPz9oZlEhP1skovyAD4Xmk_AR-1indHAUDq1FpRbFwD71wz9cArUp4Ag9iiWSZcbRmBpLQIE", + "qi": "m0Km_G1VJu47Np_OLubdi16Db--C0np_ILetAQ8uZjePtfE96JQ11n2sl3BY8nj2n_A0Al8C8GAzzwHVn47Yw_l2YncS0SFkyuQuOwt83WyseiUXZqSOIq6aY2UvgeBtsNMJznfBFN0Jo8OrjNpQMyzDy04XxXFgucbYcN_elZ0" }`), }, @@ -103,21 +115,68 @@ var jsonWebKeys = []keyPair{ Public: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "941861b40500430da0d09ec213e00832", + "kid": "e8525274b94b466e87236c8288f3888d", "alg": "RS256", - "n": "ub3SiNK-uIvSrUTyIPm1cITzuqPX_CIa6nZTDTP1tJ6PP_KufYz2eGLj9jppWLo_J7XQfKfIAKvET8Mq4HEcLQpNRN90KNyGML17JJtSgYJeLuB38BnalVUxpnycPKeGgoNJMu6t8tKYOtOfxtqTA6x8MnqMeify1cvEc5Tr4QmKjcLLHKcr1yMR7kG48i586bLdchtIBYeB298WXbQaKrgsEjZA0E1exfMnYHyvN12lMBxwhOJtcFu3mngZ7vTh179UKsP3yD8IdO5ITe_RIOmnUKuynW3PdkRUzCK5gS-xuqueGqEzJVIKBv0Hfom3eyDW5DjxpIZxlqkGhGyeNw", + "n": "-OaIVgMSRhDe5KZTiTmElFVqlRaUS_pOt4RB6DAQWubMQBxsRktN5ooBZTWJ8uI4yqnrsMoookuf2uFSFfMKQMNi3YBLCorNkrapV6kfwDaX1yD26MbHz8upetBD3452gRMFHOZznbDAOlZOObtNFp2BCVTpzU3FKhN3fK-ejrQfUdHqsjszjazbiDXLLvjnjByGmlCE7olicvUZRQ2ydr7L3HxUEwZ5_U3FxnWQBAzEkERBiwnIB_7LbIUHzt2GaoqkLRoSnXrcJ26yIj0EneRJ9gCAGzvvzFAKhG7eMvaW5nbF5yJ5PnUEBZVC0N0AnoBqr2yXdYn0yQNvwmVzvw", "e": "AQAB" }`), Private: mustLoadJWK(`{ "use": "sig", "kty": "RSA", - "kid": "c4c09817da9a42ae8d850aaba7b7cd82", + "kid": "62a3f70927f04df6a23ee08a6d826276", "alg": "RS256", - "n": "ub3SiNK-uIvSrUTyIPm1cITzuqPX_CIa6nZTDTP1tJ6PP_KufYz2eGLj9jppWLo_J7XQfKfIAKvET8Mq4HEcLQpNRN90KNyGML17JJtSgYJeLuB38BnalVUxpnycPKeGgoNJMu6t8tKYOtOfxtqTA6x8MnqMeify1cvEc5Tr4QmKjcLLHKcr1yMR7kG48i586bLdchtIBYeB298WXbQaKrgsEjZA0E1exfMnYHyvN12lMBxwhOJtcFu3mngZ7vTh179UKsP3yD8IdO5ITe_RIOmnUKuynW3PdkRUzCK5gS-xuqueGqEzJVIKBv0Hfom3eyDW5DjxpIZxlqkGhGyeNw", + "n": "-OaIVgMSRhDe5KZTiTmElFVqlRaUS_pOt4RB6DAQWubMQBxsRktN5ooBZTWJ8uI4yqnrsMoookuf2uFSFfMKQMNi3YBLCorNkrapV6kfwDaX1yD26MbHz8upetBD3452gRMFHOZznbDAOlZOObtNFp2BCVTpzU3FKhN3fK-ejrQfUdHqsjszjazbiDXLLvjnjByGmlCE7olicvUZRQ2ydr7L3HxUEwZ5_U3FxnWQBAzEkERBiwnIB_7LbIUHzt2GaoqkLRoSnXrcJ26yIj0EneRJ9gCAGzvvzFAKhG7eMvaW5nbF5yJ5PnUEBZVC0N0AnoBqr2yXdYn0yQNvwmVzvw", "e": "AQAB", - "d": "29bQWSEWm1bjBDGWY3EqTwMNdtp1yPaU5O0nX3kgV6dT5VxXKkKtdc-WANkh1uKZ3WZUXTY4gpLKx504Im2965FF4z6XPcXFDes21R0BikfDMbh8PLJdBGLRYTwbr66YheDdwmq9d6nKg9X2RmZtmuuMFDL4EZ02zdVfr22TwcSCghC2gnV6CpHHeEatJBWbK1yE6cHqCeY9UTc_QnXmbZ0TYsQi4qCV1HqTJKZDtkzqZMPvMB5EP_my_SCxcfcIzt6qqujmuXCFiS658Up-Z4W5s0RINLoPmePG8zJVFBmWrQ8xiykCeL8z9XSvXoEo6ZJJC-KSjI6s-KsCfQqZ", - "p": "8LzUJM2YgP7zG618rrFTav3gB2t1yMwFJy9d3J-pOkVFUq-4-74qEZz6H2RTUw7Ae5XEYdVIbRRQInpo0qO2MfLW8vtRexUNFFt1pBiVykq-KdkWcwPETyRD-huEEqswBhg33lFTUrY7BXRukbfNmVY7YfdagIJ5LZU0I-nGMqs", - "q": "xYRoIFTTiXitKBFo0vvHAadqVV8gJq8bCxJZ4lFMpADlU-S8Me7aPmkhPmCDaw-ii940S46bTp9ueh6EJCttmG3cJm8r4YzjK-H1dnqeF_3dpq2pimVFlFILBKWojUHHWC4n0d1IVwdf8-xnDSiUzl9roFZV5IPy4mW1HMTZ4qU" + "d": "E9C6oupcrpJSRG7PcLnRCcQhF5x0EvZrBOY1HwiQjMQaF7KLli8RlRyAju-ru1fyQIQ1nN4DTSRrJhBRKikutEg52zuG_eBeDGOZaL6wpetSvFBGB_MnXi4vIHVrKeWcHpuGiS35n2C-dQaA9MyqDvZcGwOVF4CtBTQGi7kpjN-2-vbaBiqvy9MWMPxBJc6lAjqn6d8U4hqzvRcl2Fgh9gykjRX5RhuuqcbFBn-W5iHT2JeQtCUh2tBiBje8S8d2eN_m3B4JdIvdVTX6ONjLYlcm7yaQ2w5yA4inLVkHq3FkrOwWkwcVheMIUOAWUllBe65wQpl5v_7Dg_I0jHaOnQ", + "p": "_oXrxZUp99Ym-fqYym6130IFeNT5druZyKpdSUcjec8RaDLXiSeGWECRGdL5S9X09o9cz0wvpravIjemIbuiraCe-dSNd3MOKTI6PezDzmeIJzhAa02xJET0ZGHzLDIH_6uKnGHSiUMxKn6XmXg4gCR10gsAMMzBrfSWKU25xnU", + "q": "-lhCcCJOl3gWOz3cGEZkmjE68DUcoVy8VbcVB_nkg8P7qyolOpbdS0xB3r7VFIbEnW63pE6DFnPEpDJnmE4rZ0JCn22HpZzUepPkevCCyvK-5DEzp7MOdZEW34RVnpl5V7L_a64aQdMNkrroNczArH3fUr8UPMiAqjOzlieVUuM", + "dp": "m3J7Ts8NNLpsT5xHmMsI9PPDl8qrGWL2R2IVW13BQvG-dd8nWDL1kAzPEfCSJUjlNXjVfB3RtIY6bDGEm3GXp2dD8N1qERwJ-AsfGxbxd6wvsZRfc6mdmMMsZ-qSs9lSnUnNfDkado69BnEOccLE7y5VbxUc5rEHURDibRN1dMk", + "dq": "XlcOSmWTSl9VuWN5Yqh8VuMAe-736BMjYgXJZiUUqVccqwcJ2odpw2tsUz2E3ORgiZdkmwV_PuHHk7zu7yVeE96TbrL-8DkmlT7QVkf2cfaCW0nzVloSs9lGTlr3TPo3EQaCXJjSikW3krjesw-C-D3C_9LEFJfWbM1o-sfV7NM", + "qi": "otibsKrvyertgnuuxepfzTpPcQ3an31YTdyHwDc32-kaMyvd7WH5SYA_sqZwA2b9IRUNarZ6doEbebcwfwMNJV3I_QRRUNMOs2SDp5XH7cDWUGwtx3SYdziqK56Xt23orNHx95uIfqrkWkYRk1EOo6lRyVREtPwk8HCV_tl5eYY" + }`), + }, + + { + Public: mustLoadJWK(`{ + "use": "sig", + "kty": "EC", + "kid": "bc23e2a3e81f4a17b751ce423c69a3a4", + "crv": "P-256", + "alg": "ES256", + "x": "bIkaBnOXHj-TYlPLZ4cjZernfP6P73KCEZ6JlXQosz4", + "y": "YNyxfwaqsrkAXLIvSRJVsfrJvk9CqvQAJPDIqJ96cNU" + }`), + Private: mustLoadJWK(`{ + "use": "sig", + "kty": "EC", + "kid": "14c1bb757c7940b49ec1401c36a315c7", + "crv": "P-256", + "alg": "ES256", + "x": "bIkaBnOXHj-TYlPLZ4cjZernfP6P73KCEZ6JlXQosz4", + "y": "YNyxfwaqsrkAXLIvSRJVsfrJvk9CqvQAJPDIqJ96cNU", + "d": "VGryC3cIOaJqzgti9FsWMbSi4LsbZ_3BUmOBdsd8-Xo" + }`), + }, + + { + Public: mustLoadJWK(`{ + "use": "sig", + "kty": "EC", + "kid": "e0d6ff6492bd4ab39aa68a9e16f99960", + "crv": "P-256", + "alg": "ES256", + "x": "VCg2WFqMoeULMRr2cMgfqlNvORPI0YyIsjFzEHMGBZI", + "y": "QyWgq1jIfPUDlQPhA8sT_VWk7Z9-uPek0FJJ285G40s" + }`), + Private: mustLoadJWK(`{ + "use": "sig", + "kty": "EC", + "kid": "df1241b64dba40c18a10aff4a806083d", + "crv": "P-256", + "alg": "ES256", + "x": "VCg2WFqMoeULMRr2cMgfqlNvORPI0YyIsjFzEHMGBZI", + "y": "QyWgq1jIfPUDlQPhA8sT_VWk7Z9-uPek0FJJ285G40s", + "d": "Eu1dSBbvVRdZwU8wPSWUq_xhSzxFfx43NqSWFAx4Yas" }`), }, } diff --git a/storage/conformance/transactions.go b/storage/conformance/transactions.go index f018224e..3924e52c 100644 --- a/storage/conformance/transactions.go +++ b/storage/conformance/transactions.go @@ -1,12 +1,12 @@ package conformance import ( - "context" "strconv" "sync" "testing" "time" + "github.com/kylelemons/godebug/pretty" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" @@ -154,34 +154,67 @@ func testPasswordConcurrentUpdate(t *testing.T, s storage.Storage) { } func testKeysConcurrentUpdate(t *testing.T, s storage.Storage) { - // Test twice. Once for a create, once for an update. - for i := 0; i < 2; i++ { - n := time.Now().UTC().Round(time.Second) - keys1 := storage.Keys{ - SigningKey: jsonWebKeys[i].Private, - SigningKeyPub: jsonWebKeys[i].Public, - NextRotation: n, - } - - keys2 := storage.Keys{ - SigningKey: jsonWebKeys[2].Private, - SigningKeyPub: jsonWebKeys[2].Public, - NextRotation: n.Add(time.Hour), - VerificationKeys: []storage.VerificationKey{ - { - PublicKey: jsonWebKeys[0].Public, - Expiry: n.Add(time.Hour), + tests := []struct { + name string + keys1 storage.Keys + keys2 storage.Keys + }{ + { + name: "create with rsa and es256 payloads", + keys1: storage.Keys{ + SigningKey: jsonWebKeys[0].Private, + SigningKeyPub: jsonWebKeys[0].Public, + }, + keys2: storage.Keys{ + SigningKey: jsonWebKeys[5].Private, + SigningKeyPub: jsonWebKeys[5].Public, + VerificationKeys: []storage.VerificationKey{ + { + PublicKey: jsonWebKeys[1].Public, + }, + { + PublicKey: jsonWebKeys[6].Public, + }, }, - { - PublicKey: jsonWebKeys[1].Public, - Expiry: n.Add(time.Hour * 2), + }, + }, + { + name: "update with es256 and rsa payloads", + keys1: storage.Keys{ + SigningKey: jsonWebKeys[6].Private, + SigningKeyPub: jsonWebKeys[6].Public, + }, + keys2: storage.Keys{ + SigningKey: jsonWebKeys[2].Private, + SigningKeyPub: jsonWebKeys[2].Public, + VerificationKeys: []storage.VerificationKey{ + { + PublicKey: jsonWebKeys[5].Public, + }, + { + PublicKey: jsonWebKeys[3].Public, + }, }, }, + }, + } + + // Run twice against the same storage. The first case exercises row creation, + // the second runs with an existing keys row and therefore exercises updates. + for _, tt := range tests { + n := time.Now().UTC().Round(time.Second) + keys1 := tt.keys1 + keys1.NextRotation = n + keys2 := tt.keys2 + keys2.NextRotation = n.Add(time.Hour) + + for i := range keys2.VerificationKeys { + keys2.VerificationKeys[i].Expiry = n.Add(time.Duration(i+1) * time.Hour) } var err1, err2 error - ctx := context.TODO() + ctx := t.Context() err1 = s.UpdateKeys(ctx, func(old storage.Keys) (storage.Keys, error) { err2 = s.UpdateKeys(ctx, func(old storage.Keys) (storage.Keys, error) { return keys1, nil @@ -190,8 +223,23 @@ func testKeysConcurrentUpdate(t *testing.T, s storage.Storage) { }) if (err1 == nil) == (err2 == nil) { - t.Errorf("update keys: concurrent updates both returned no error") + t.Errorf("%s: concurrent updates both returned no error", tt.name) + } + + got, err := s.GetKeys(ctx) + require.NoError(t, err) + + got.NextRotation = got.NextRotation.UTC() + + diff1 := pretty.Compare(keys1, got) + diff2 := pretty.Compare(keys2, got) + match1 := diff1 == "" + match2 := diff2 == "" + if match1 == match2 { + t.Fatalf("%s: final stored keys did not match an expected winner\nkeys1 diff:\n%s\nkeys2 diff:\n%s", tt.name, diff1, diff2) } + + requireSigningKeyRoundTripUsable(t, got) } }