Browse Source

feat: support ES256 local signer (#4682)

Signed-off-by: Ilia Andreev <ilia.andreev@palark.com>
Co-authored-by: Ilia Andreev <ilia.andreev@palark.com>
pull/4668/merge
iliaandreevde 2 days ago committed by GitHub
parent
commit
098ab6036e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 17
      cmd/dex/config.go
  2. 95
      cmd/dex/config_test.go
  3. 7
      config.docker.yaml
  4. 8
      config.yaml.dist
  5. 9
      examples/config-dev.yaml
  6. 25
      server/handlers_test.go
  7. 119
      server/oauth2_test.go
  8. 55
      server/signer/local.go
  9. 246
      server/signer/local_test.go
  10. 96
      server/signer/rotation.go
  11. 8
      server/signer/rotation_test.go
  12. 50
      server/signer/utils.go
  13. 50
      server/signer/utils_test.go
  14. 136
      storage/conformance/conformance.go
  15. 98
      storage/conformance/gen_jwks.go
  16. 135
      storage/conformance/jwks.go
  17. 94
      storage/conformance/transactions.go

17
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 {

95
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 {

7
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" }}

8
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:

9
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:

25
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()

119
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"))

55
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)
}

246
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)
}

96
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
}

8
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,
}

50
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}

50
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 {}")
})
}

136
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) {

98
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)

135
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"
}`),
},
}

94
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)
}
}

Loading…
Cancel
Save