Skip to content
Snippets Groups Projects
rotation.go 4.76 KiB
Newer Older
  • Learn to ignore specific revisions
  • Eric Chiang's avatar
    Eric Chiang committed
    package server
    
    import (
    
    Eric Chiang's avatar
    Eric Chiang committed
    	"crypto/rand"
    	"crypto/rsa"
    	"encoding/hex"
    	"fmt"
    	"io"
    	"time"
    
    	"gopkg.in/square/go-jose.v2"
    
    
    	"github.com/Sirupsen/logrus"
    
    Eric Chiang's avatar
    Eric Chiang committed
    	"github.com/coreos/dex/storage"
    
    Eric Chiang's avatar
    Eric Chiang committed
    )
    
    // 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
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    
    	// After being rotated how long should the key be kept around for validating
    	// signatues?
    	idTokenValidFor time.Duration
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    	// 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)
    }
    
    // staticRotationStrategy returns a strategy which never rotates keys.
    func staticRotationStrategy(key *rsa.PrivateKey) rotationStrategy {
    	return rotationStrategy{
    		// Setting these values to 100 years is easier than having a flag indicating no rotation.
    
    		rotationFrequency: time.Hour * 8760 * 100,
    
    		idTokenValidFor:   time.Hour * 8760 * 100,
    
    		key:               func() (*rsa.PrivateKey, error) { return key, nil },
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    }
    
    // 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 {
    
    Eric Chiang's avatar
    Eric Chiang committed
    	return rotationStrategy{
    
    		rotationFrequency: rotationFrequency,
    
    		idTokenValidFor:   idTokenValidFor,
    
    Eric Chiang's avatar
    Eric Chiang committed
    		key: func() (*rsa.PrivateKey, error) {
    			return rsa.GenerateKey(rand.Reader, 2048)
    		},
    	}
    }
    
    type keyRotater struct {
    	storage.Storage
    
    	strategy rotationStrategy
    
    
    	logger logrus.FieldLogger
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    
    // startKeyRotation begins key rotation in a new goroutine, closing once the context is canceled.
    //
    // The method blocks until after the first attempt to rotate keys has completed. That way
    // healthy storages will return from this call with valid keys.
    
    func (s *Server) startKeyRotation(ctx context.Context, strategy rotationStrategy, now func() time.Time) {
    	rotater := keyRotater{s.storage, strategy, now, s.logger}
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    
    	// Try to rotate immediately so properly configured storages will have keys.
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err := rotater.rotate(); err != nil {
    
    		s.logger.Errorf("failed to rotate keys: %v", err)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    
    	go func() {
    
    		for {
    			select {
    			case <-ctx.Done():
    				return
    
    			case <-time.After(time.Second * 30):
    
    				if err := rotater.rotate(); err != nil {
    
    					s.logger.Errorf("failed to rotate keys: %v", err)
    
    Eric Chiang's avatar
    Eric Chiang committed
    			}
    		}
    	}()
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    func (k keyRotater) rotate() error {
    	keys, err := k.GetKeys()
    	if err != nil && err != storage.ErrNotFound {
    		return fmt.Errorf("get keys: %v", err)
    	}
    	if k.now().Before(keys.NextRotation) {
    		return nil
    	}
    
    	k.logger.Infof("keys expired, rotating")
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    	// 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(func(keys storage.Keys) (storage.Keys, error) {
    		tNow := k.now()
    
    
    		// if you are running multiple instances of dex, another instance
    		// could have already rotated the keys.
    
    Eric Chiang's avatar
    Eric Chiang committed
    		if tNow.Before(keys.NextRotation) {
    
    Eric Chiang's avatar
    Eric Chiang committed
    		}
    
    
    		expired := func(key storage.VerificationKey) bool {
    			return tNow.After(key.Expiry)
    		}
    
    		// Remove any verification keys that have expired.
    		i := 0
    
    Eric Chiang's avatar
    Eric Chiang committed
    		for _, key := range keys.VerificationKeys {
    
    Eric Chiang's avatar
    Eric Chiang committed
    				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.
    
    Eric Chiang's avatar
    Eric Chiang committed
    			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),
    
    Eric Chiang's avatar
    Eric Chiang committed
    			}
    			keys.VerificationKeys = append(keys.VerificationKeys, verificationKey)
    		}
    
    
    		nextRotation = k.now().Add(k.strategy.rotationFrequency)
    
    Eric Chiang's avatar
    Eric Chiang committed
    		keys.SigningKey = priv
    		keys.SigningKeyPub = pub
    		keys.NextRotation = nextRotation
    		return keys, nil
    	})
    	if err != nil {
    		return err
    	}
    
    	k.logger.Infof("keys rotated, next rotation: %s", nextRotation)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	return nil
    }