OpenID Connect (OIDC) identity and OAuth 2.0 provider with pluggable connectors
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

140 lines
3.7 KiB

package signer
10 years ago
import (
"context"
10 years ago
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"errors"
10 years ago
"fmt"
"io"
"log/slog"
10 years ago
"time"
"github.com/go-jose/go-jose/v4"
10 years ago
"github.com/dexidp/dex/storage"
10 years ago
)
var errAlreadyRotated = errors.New("keys already rotated by another server instance")
10 years ago
// rotationStrategy describes a strategy for generating cryptographic keys, how
// often to rotate them, and how long they can validate signatures after rotation.
type rotationStrategy struct {
// Time between rotations.
rotationFrequency time.Duration
10 years ago
// After being rotated how long should the key be kept around for validating
// signatures?
idTokenValidFor time.Duration
10 years ago
// 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)
}
// 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 {
10 years ago
return rotationStrategy{
rotationFrequency: rotationFrequency,
idTokenValidFor: idTokenValidFor,
10 years ago
key: func() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, 2048)
},
}
}
type keyRotator struct {
10 years ago
storage.Storage
strategy rotationStrategy
now func() time.Time
logger *slog.Logger
10 years ago
}
func (k keyRotator) rotate() error {
keys, err := k.GetKeys(context.Background())
10 years ago
if err != nil && err != storage.ErrNotFound {
return fmt.Errorf("get keys: %v", err)
}
if k.now().Before(keys.NextRotation) {
return nil
}
k.logger.Info("keys expired, rotating")
10 years ago
// 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",
}
var nextRotation time.Time
err = k.Storage.UpdateKeys(context.Background(), func(keys storage.Keys) (storage.Keys, error) {
10 years ago
tNow := k.now()
// if you are running multiple instances of dex, another instance
// could have already rotated the keys.
10 years ago
if tNow.Before(keys.NextRotation) {
return storage.Keys{}, errAlreadyRotated
10 years ago
}
expired := func(key storage.VerificationKey) bool {
return tNow.After(key.Expiry)
}
// Remove any verification keys that have expired.
i := 0
10 years ago
for _, key := range keys.VerificationKeys {
if !expired(key) {
10 years ago
keys.VerificationKeys[i] = key
i++
}
}
keys.VerificationKeys = keys.VerificationKeys[:i]
if keys.SigningKeyPub != nil {
// Move current signing key to a verification only key, throwing
// away the private part.
10 years ago
verificationKey := storage.VerificationKey{
PublicKey: keys.SigningKeyPub,
// After demoting the signing key, keep the token around for at least
// the amount of time an ID Token is valid for. This ensures the
// verification key won't expire until all ID Tokens it's signed
// expired as well.
Expiry: tNow.Add(k.strategy.idTokenValidFor),
10 years ago
}
keys.VerificationKeys = append(keys.VerificationKeys, verificationKey)
}
nextRotation = k.now().Add(k.strategy.rotationFrequency)
10 years ago
keys.SigningKey = priv
keys.SigningKeyPub = pub
keys.NextRotation = nextRotation
return keys, nil
})
if err != nil {
return err
}
k.logger.Info("keys rotated", "next_rotation", nextRotation)
10 years ago
return nil
}