docker/swarmkit

View on GitHub
manager/deks.go

Summary

Maintainability
A
35 mins
Test Coverage
package manager

import (
    "crypto/subtle"
    "encoding/base64"
    "fmt"

    "github.com/moby/swarmkit/v2/ca"
    "github.com/moby/swarmkit/v2/manager/encryption"
    "github.com/moby/swarmkit/v2/manager/state/raft"
)

// This module contains the data structures and control flow to manage rotating the raft
// DEK and also for interacting with KeyReadWriter to maintain the raft DEK information in
// the PEM headers fo the TLS key for the node.

const (
    // the raft DEK (data encryption key) is stored in the TLS key as a header
    // these are the header values
    pemHeaderRaftDEK              = "raft-dek"
    pemHeaderRaftPendingDEK       = "raft-dek-pending"
    pemHeaderRaftDEKNeedsRotation = "raft-dek-needs-rotation"
)

// RaftDEKData contains all the data stored in TLS pem headers.
type RaftDEKData struct {

    // EncryptionKeys contain the current and pending raft DEKs
    raft.EncryptionKeys

    // NeedsRotation indicates whether another rotation needs to be happen after
    // the current one.
    NeedsRotation bool

    // The FIPS boolean is not serialized, but is internal state which indicates how
    // the raft DEK headers should be encrypted (e.g. using FIPS compliant algorithms)
    FIPS bool
}

// RaftDEKData should implement the PEMKeyHeaders interface
var _ ca.PEMKeyHeaders = RaftDEKData{}

// UnmarshalHeaders loads the current state of the DEKs into a new RaftDEKData object (which is returned) given the
// current TLS headers and the current KEK.
func (r RaftDEKData) UnmarshalHeaders(headers map[string]string, kekData ca.KEKData) (ca.PEMKeyHeaders, error) {
    var (
        currentDEK, pendingDEK []byte
        err                    error
    )

    if currentDEKStr, ok := headers[pemHeaderRaftDEK]; ok {
        currentDEK, err = decodePEMHeaderValue(currentDEKStr, kekData.KEK, r.FIPS)
        if err != nil {
            return nil, err
        }
    }
    if pendingDEKStr, ok := headers[pemHeaderRaftPendingDEK]; ok {
        pendingDEK, err = decodePEMHeaderValue(pendingDEKStr, kekData.KEK, r.FIPS)
        if err != nil {
            return nil, err
        }
    }

    if pendingDEK != nil && currentDEK == nil {
        return nil, fmt.Errorf("there is a pending DEK, but no current DEK")
    }

    _, ok := headers[pemHeaderRaftDEKNeedsRotation]
    return RaftDEKData{
        NeedsRotation: ok,
        EncryptionKeys: raft.EncryptionKeys{
            CurrentDEK: currentDEK,
            PendingDEK: pendingDEK,
        },
        FIPS: r.FIPS,
    }, nil
}

// MarshalHeaders returns new PEM headers given the current KEK - it uses the current KEK to
// serialize/encrypt the current DEK state that is maintained in the current RaftDEKData object.
func (r RaftDEKData) MarshalHeaders(kekData ca.KEKData) (map[string]string, error) {
    headers := make(map[string]string)
    for headerKey, contents := range map[string][]byte{
        pemHeaderRaftDEK:        r.CurrentDEK,
        pemHeaderRaftPendingDEK: r.PendingDEK,
    } {
        if contents != nil {
            dekStr, err := encodePEMHeaderValue(contents, kekData.KEK, r.FIPS)
            if err != nil {
                return nil, err
            }
            headers[headerKey] = dekStr
        }
    }

    if r.NeedsRotation {
        headers[pemHeaderRaftDEKNeedsRotation] = "true"
    }

    // return a function that updates the dek data on write success
    return headers, nil
}

// UpdateKEK sets NeedRotation to true if we go from unlocked to locked.
func (r RaftDEKData) UpdateKEK(oldKEK, candidateKEK ca.KEKData) ca.PEMKeyHeaders {
    if _, unlockedToLocked, err := compareKEKs(oldKEK, candidateKEK); err == nil && unlockedToLocked {
        return RaftDEKData{
            EncryptionKeys: r.EncryptionKeys,
            NeedsRotation:  true,
            FIPS:           r.FIPS,
        }
    }
    return r
}

// Returns whether the old KEK should be replaced with the new KEK, whether we went from
// unlocked to locked, and whether there was an error (the versions are the same, but the
// keks are different)
func compareKEKs(oldKEK, candidateKEK ca.KEKData) (bool, bool, error) {
    keksEqual := subtle.ConstantTimeCompare(oldKEK.KEK, candidateKEK.KEK) == 1
    switch {
    case oldKEK.Version == candidateKEK.Version && !keksEqual:
        return false, false, fmt.Errorf("candidate KEK has the same version as the current KEK, but a different KEK value")
    case oldKEK.Version >= candidateKEK.Version || keksEqual:
        return false, false, nil
    default:
        return true, oldKEK.KEK == nil, nil
    }
}

// RaftDEKManager manages the raft DEK keys by interacting with KeyReadWriter, calling the necessary functions
// to update the TLS headers when the raft DEK needs to change, or to re-encrypt everything when the KEK changes.
type RaftDEKManager struct {
    kw         ca.KeyWriter
    rotationCh chan struct{}
    FIPS       bool
}

var errNoUpdateNeeded = fmt.Errorf("don't need to rotate or update")

// this error is returned if the KeyReadWriter's PEMKeyHeaders object is no longer a RaftDEKData object -
// this can happen if the node is no longer a manager, for example
var errNotUsingRaftDEKData = fmt.Errorf("RaftDEKManager can no longer store and manage TLS key headers")

// NewRaftDEKManager returns a RaftDEKManager that uses the current key writer
// and header manager
func NewRaftDEKManager(kw ca.KeyWriter, fips bool) (*RaftDEKManager, error) {
    // If there is no current DEK, generate one and write it to disk
    err := kw.ViewAndUpdateHeaders(func(h ca.PEMKeyHeaders) (ca.PEMKeyHeaders, error) {
        dekData, ok := h.(RaftDEKData)
        // it wasn't a raft DEK manager before - just replace it
        if !ok || dekData.CurrentDEK == nil {
            return RaftDEKData{
                EncryptionKeys: raft.EncryptionKeys{
                    CurrentDEK: encryption.GenerateSecretKey(),
                },
                FIPS: fips,
            }, nil
        }
        return nil, errNoUpdateNeeded
    })
    if err != nil && err != errNoUpdateNeeded {
        return nil, err
    }
    return &RaftDEKManager{
        kw:         kw,
        FIPS:       fips,
        rotationCh: make(chan struct{}, 1),
    }, nil
}

// NeedsRotation returns a boolean about whether we should do a rotation
func (r *RaftDEKManager) NeedsRotation() bool {
    h, _ := r.kw.GetCurrentState()
    data, ok := h.(RaftDEKData)
    if !ok {
        return false
    }
    return data.NeedsRotation || data.EncryptionKeys.PendingDEK != nil
}

// GetKeys returns the current set of DEKs.  If NeedsRotation is true, and there
// is no existing PendingDEK, it will try to create one.  If it successfully creates
// and writes a PendingDEK, it sets NeedRotation to false.  If there are any errors
// doing so, just return the original set of keys.
func (r *RaftDEKManager) GetKeys() raft.EncryptionKeys {
    var newKeys, originalKeys raft.EncryptionKeys
    err := r.kw.ViewAndUpdateHeaders(func(h ca.PEMKeyHeaders) (ca.PEMKeyHeaders, error) {
        data, ok := h.(RaftDEKData)
        if !ok {
            return nil, errNotUsingRaftDEKData
        }
        originalKeys = data.EncryptionKeys
        if !data.NeedsRotation || data.PendingDEK != nil {
            return nil, errNoUpdateNeeded
        }
        newKeys = raft.EncryptionKeys{
            CurrentDEK: data.CurrentDEK,
            PendingDEK: encryption.GenerateSecretKey(),
        }
        return RaftDEKData{
            EncryptionKeys: newKeys,
            FIPS:           data.FIPS,
        }, nil
    })
    if err != nil {
        return originalKeys
    }
    return newKeys
}

// RotationNotify the channel used to notify subscribers as to whether there
// should be a rotation done
func (r *RaftDEKManager) RotationNotify() chan struct{} {
    return r.rotationCh
}

// UpdateKeys will set the updated encryption keys in the headers.  This finishes
// a rotation, and is expected to set the CurrentDEK to the previous PendingDEK.
func (r *RaftDEKManager) UpdateKeys(newKeys raft.EncryptionKeys) error {
    return r.kw.ViewAndUpdateHeaders(func(h ca.PEMKeyHeaders) (ca.PEMKeyHeaders, error) {
        data, ok := h.(RaftDEKData)
        if !ok {
            return nil, errNotUsingRaftDEKData
        }
        // If there is no current DEK, we are basically wiping out all DEKs (no header object)
        if newKeys.CurrentDEK == nil {
            return nil, nil
        }
        return RaftDEKData{
            EncryptionKeys: newKeys,
            NeedsRotation:  data.NeedsRotation,
            FIPS:           data.FIPS,
        }, nil
    })
}

// MaybeUpdateKEK does a KEK rotation if one is required.  Returns whether
// the kek was updated, whether it went from unlocked to locked, and any errors.
func (r *RaftDEKManager) MaybeUpdateKEK(candidateKEK ca.KEKData) (bool, bool, error) {
    var updated, unlockedToLocked bool
    err := r.kw.ViewAndRotateKEK(func(currentKEK ca.KEKData, h ca.PEMKeyHeaders) (ca.KEKData, ca.PEMKeyHeaders, error) {
        var err error
        updated, unlockedToLocked, err = compareKEKs(currentKEK, candidateKEK)
        if err == nil && !updated { // if we don't need to rotate the KEK, don't bother updating
            err = errNoUpdateNeeded
        }
        if err != nil {
            return ca.KEKData{}, nil, err
        }

        data, ok := h.(RaftDEKData)
        if !ok {
            return ca.KEKData{}, nil, errNotUsingRaftDEKData
        }

        if unlockedToLocked {
            data.NeedsRotation = true
        }
        return candidateKEK, data, nil
    })
    if err == errNoUpdateNeeded {
        err = nil
    }

    if err == nil && unlockedToLocked {
        r.rotationCh <- struct{}{}
    }
    return updated, unlockedToLocked, err
}

func decodePEMHeaderValue(headerValue string, kek []byte, fips bool) ([]byte, error) {
    var decrypter encryption.Decrypter = encryption.NoopCrypter
    if kek != nil {
        _, decrypter = encryption.Defaults(kek, fips)
    }
    valueBytes, err := base64.StdEncoding.DecodeString(headerValue)
    if err != nil {
        return nil, err
    }
    result, err := encryption.Decrypt(valueBytes, decrypter)
    if err != nil {
        return nil, ca.ErrInvalidKEK{Wrapped: err}
    }
    return result, nil
}

func encodePEMHeaderValue(headerValue []byte, kek []byte, fips bool) (string, error) {
    var encrypter encryption.Encrypter = encryption.NoopCrypter
    if kek != nil {
        encrypter, _ = encryption.Defaults(kek, fips)
    }
    encrypted, err := encryption.Encrypt(headerValue, encrypter)
    if err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(encrypted), nil
}