blob: 0911440976049f8d5239bf19ff492843da5419e6 [file] [log] [blame]
package ca
import (
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"crypto/tls"
"github.com/docker/swarmkit/ca/keyutils"
"github.com/docker/swarmkit/ca/pkcs8"
"github.com/docker/swarmkit/ioutils"
"github.com/pkg/errors"
)
const (
// keyPerms are the permissions used to write the TLS keys
keyPerms = 0600
// certPerms are the permissions used to write TLS certificates
certPerms = 0644
// versionHeader is the TLS PEM key header that contains the KEK version
versionHeader = "kek-version"
)
// PEMKeyHeaders is an interface for something that needs to know about PEM headers
// when reading or writing TLS keys in order to keep them updated with the latest
// KEK.
type PEMKeyHeaders interface {
// UnmarshalHeaders loads the headers map given the current KEK
UnmarshalHeaders(map[string]string, KEKData) (PEMKeyHeaders, error)
// MarshalHeaders returns a header map given the current KEK
MarshalHeaders(KEKData) (map[string]string, error)
// UpdateKEK gets called whenever KeyReadWriter gets a KEK update. This allows the
// PEMKeyHeaders to optionally update any internal state. It should return an
// updated (if needed) versino of itself.
UpdateKEK(KEKData, KEKData) PEMKeyHeaders
}
// KeyReader reads a TLS cert and key from disk
type KeyReader interface {
// Read reads and returns the certificate and the key PEM bytes that are on disk
Read() ([]byte, []byte, error)
// Target returns a string representation of where the cert data is being read from
Target() string
}
// KeyWriter writes a TLS key and cert to disk
type KeyWriter interface {
// Write accepts a certificate and key in PEM format, as well as an optional KEKData object.
// If there is a current KEK, the key is encrypted using the current KEK. If the KEKData object
// is provided (not nil), the key will be encrypted using the new KEK instead, and the current
// KEK in memory will be replaced by the provided KEKData. The reason to allow changing key
// material and KEK in a single step, as opposed to two steps, is to prevent the key material
// from being written unencrypted or with an old KEK in the first place (when a node gets a
// certificate from the CA, it will also request the current KEK so it won't have to immediately
// do a KEK rotation after getting the key).
Write([]byte, []byte, *KEKData) error
// ViewAndUpdateHeaders is a function that reads and updates the headers of the key in a single
// transaction (e.g. within a lock). It accepts a callback function which will be passed the
// current header management object, and which must return a new, updated, or same header
// management object. KeyReadWriter then performs the following actions:
// - uses the old header management object and the current KEK to deserialize/decrypt
// the existing PEM headers
// - uses the new header management object and the current KEK to to reserialize/encrypt
// the PEM headers
// - writes the new PEM headers, as well as the key material, unchanged, to disk
ViewAndUpdateHeaders(func(PEMKeyHeaders) (PEMKeyHeaders, error)) error
// ViewAndRotateKEK is a function that just re-encrypts the TLS key and headers in a single
// transaction (e.g. within a lock). It accepts a callback unction which will be passed the
// current KEK and the current headers management object, and which should return a new
// KEK and header management object. KeyReadWriter then performs the following actions:
// - uses the old KEK and header management object to deserialize/decrypt the
// TLS key and PEM headers
// - uses the new KEK and header management object to serialize/encrypt the TLS key
// and PEM headers
// - writes the new PEM headers and newly encrypted TLS key to disk
ViewAndRotateKEK(func(KEKData, PEMKeyHeaders) (KEKData, PEMKeyHeaders, error)) error
// GetCurrentState returns the current header management object and the current KEK.
GetCurrentState() (PEMKeyHeaders, KEKData)
// Target returns a string representation of where the cert data is being read from
Target() string
}
// KEKData provides an optional update to the kek when writing. The structure
// is needed so that we can tell the difference between "do not encrypt anymore"
// and there is "no update".
type KEKData struct {
KEK []byte
Version uint64
}
// ErrInvalidKEK means that we cannot decrypt the TLS key for some reason
type ErrInvalidKEK struct {
Wrapped error
}
func (e ErrInvalidKEK) Error() string {
return e.Wrapped.Error()
}
// KeyReadWriter is an object that knows how to read and write TLS keys and certs to disk,
// optionally encrypted and optionally updating PEM headers. It should be the only object which
// can write the TLS key, to ensure that writes are serialized and that the TLS key, the
// KEK (key encrypting key), and any headers which need to be written are never out of sync.
// It accepts a PEMKeyHeaders object, which is used to serialize/encrypt and deserialize/decrypt
// the PEM headers when given the current headers and the current KEK.
type KeyReadWriter struct {
// This lock is held whenever a key is read from or written to disk, or whenever the internal
// state of the KeyReadWriter (such as the KEK, the key formatter, or the PEM header management
// object changes.)
mu sync.Mutex
kekData KEKData
paths CertPaths
headersObj PEMKeyHeaders
keyFormatter keyutils.Formatter
}
// NewKeyReadWriter creates a new KeyReadWriter
func NewKeyReadWriter(paths CertPaths, kek []byte, headersObj PEMKeyHeaders) *KeyReadWriter {
return &KeyReadWriter{
kekData: KEKData{KEK: kek},
paths: paths,
headersObj: headersObj,
keyFormatter: keyutils.Default,
}
}
// SetKeyFormatter sets the keyformatter with which to encrypt and decrypt keys
func (k *KeyReadWriter) SetKeyFormatter(kf keyutils.Formatter) {
k.mu.Lock()
defer k.mu.Unlock()
k.keyFormatter = kf
}
// Migrate checks to see if a temporary key file exists. Older versions of
// swarmkit wrote temporary keys instead of temporary certificates, so
// migrate that temporary key if it exists. We want to write temporary certificates,
// instead of temporary keys, because we may need to periodically re-encrypt the
// keys and modify the headers, and it's easier to have a single canonical key
// location than two possible key locations.
func (k *KeyReadWriter) Migrate() error {
tmpPaths := k.genTempPaths()
keyBytes, err := ioutil.ReadFile(tmpPaths.Key)
if err != nil {
return nil // no key? no migration
}
// it does exist - no need to decrypt, because previous versions of swarmkit
// which supported this temporary key did not support encrypting TLS keys
cert, err := ioutil.ReadFile(k.paths.Cert)
if err != nil {
return os.RemoveAll(tmpPaths.Key) // no cert? no migration
}
// nope, this does not match the cert
if _, err = tls.X509KeyPair(cert, keyBytes); err != nil {
return os.RemoveAll(tmpPaths.Key)
}
return os.Rename(tmpPaths.Key, k.paths.Key)
}
// Read will read a TLS cert and key from the given paths
func (k *KeyReadWriter) Read() ([]byte, []byte, error) {
k.mu.Lock()
defer k.mu.Unlock()
keyBlock, err := k.readKey()
if err != nil {
return nil, nil, err
}
if version, ok := keyBlock.Headers[versionHeader]; ok {
if versionInt, err := strconv.ParseUint(version, 10, 64); err == nil {
k.kekData.Version = versionInt
}
}
delete(keyBlock.Headers, versionHeader)
if k.headersObj != nil {
newHeaders, err := k.headersObj.UnmarshalHeaders(keyBlock.Headers, k.kekData)
if err != nil {
return nil, nil, errors.Wrap(err, "unable to read TLS key headers")
}
k.headersObj = newHeaders
}
keyBytes := pem.EncodeToMemory(keyBlock)
cert, err := ioutil.ReadFile(k.paths.Cert)
// The cert is written to a temporary file first, then the key, and then
// the cert gets renamed - so, if interrupted, it's possible to end up with
// a cert that only exists in the temporary location.
switch {
case err == nil:
_, err = tls.X509KeyPair(cert, keyBytes)
case os.IsNotExist(err): //continue to try temp location
break
default:
return nil, nil, err
}
// either the cert doesn't exist, or it doesn't match the key - try the temp file, if it exists
if err != nil {
var tempErr error
tmpPaths := k.genTempPaths()
cert, tempErr = ioutil.ReadFile(tmpPaths.Cert)
if tempErr != nil {
return nil, nil, err // return the original error
}
if _, tempErr := tls.X509KeyPair(cert, keyBytes); tempErr != nil {
os.RemoveAll(tmpPaths.Cert) // nope, it doesn't match either - remove and return the original error
return nil, nil, err
}
os.Rename(tmpPaths.Cert, k.paths.Cert) // try to move the temp cert back to the regular location
}
return cert, keyBytes, nil
}
// ViewAndRotateKEK re-encrypts the key with a new KEK
func (k *KeyReadWriter) ViewAndRotateKEK(cb func(KEKData, PEMKeyHeaders) (KEKData, PEMKeyHeaders, error)) error {
k.mu.Lock()
defer k.mu.Unlock()
updatedKEK, updatedHeaderObj, err := cb(k.kekData, k.headersObj)
if err != nil {
return err
}
keyBlock, err := k.readKey()
if err != nil {
return err
}
return k.writeKey(keyBlock, updatedKEK, updatedHeaderObj)
}
// ViewAndUpdateHeaders updates the header manager, and updates any headers on the existing key
func (k *KeyReadWriter) ViewAndUpdateHeaders(cb func(PEMKeyHeaders) (PEMKeyHeaders, error)) error {
k.mu.Lock()
defer k.mu.Unlock()
pkh, err := cb(k.headersObj)
if err != nil {
return err
}
keyBlock, err := k.readKeyblock()
if err != nil {
return err
}
headers := make(map[string]string)
if pkh != nil {
var err error
headers, err = pkh.MarshalHeaders(k.kekData)
if err != nil {
return err
}
}
// we WANT any original encryption headers
for key, value := range keyBlock.Headers {
normalizedKey := strings.TrimSpace(strings.ToLower(key))
if normalizedKey == "proc-type" || normalizedKey == "dek-info" {
headers[key] = value
}
}
headers[versionHeader] = strconv.FormatUint(k.kekData.Version, 10)
keyBlock.Headers = headers
if err = ioutils.AtomicWriteFile(k.paths.Key, pem.EncodeToMemory(keyBlock), keyPerms); err != nil {
return err
}
k.headersObj = pkh
return nil
}
// GetCurrentState returns the current KEK data, including version
func (k *KeyReadWriter) GetCurrentState() (PEMKeyHeaders, KEKData) {
k.mu.Lock()
defer k.mu.Unlock()
return k.headersObj, k.kekData
}
// Write attempts write a cert and key to text. This can also optionally update
// the KEK while writing, if an updated KEK is provided. If the pointer to the
// update KEK is nil, then we don't update. If the updated KEK itself is nil,
// then we update the KEK to be nil (data should be unencrypted).
func (k *KeyReadWriter) Write(certBytes, plaintextKeyBytes []byte, kekData *KEKData) error {
k.mu.Lock()
defer k.mu.Unlock()
// current assumption is that the cert and key will be in the same directory
if err := os.MkdirAll(filepath.Dir(k.paths.Key), 0755); err != nil {
return err
}
// Ensure that we will have a keypair on disk at all times by writing the cert to a
// temp path first. This is because we want to have only a single copy of the key
// for rotation and header modification.
tmpPaths := k.genTempPaths()
if err := ioutils.AtomicWriteFile(tmpPaths.Cert, certBytes, certPerms); err != nil {
return err
}
keyBlock, _ := pem.Decode(plaintextKeyBytes)
if keyBlock == nil {
return errors.New("invalid PEM-encoded private key")
}
if kekData == nil {
kekData = &k.kekData
}
pkh := k.headersObj
if k.headersObj != nil {
pkh = k.headersObj.UpdateKEK(k.kekData, *kekData)
}
if err := k.writeKey(keyBlock, *kekData, pkh); err != nil {
return err
}
return os.Rename(tmpPaths.Cert, k.paths.Cert)
}
func (k *KeyReadWriter) genTempPaths() CertPaths {
return CertPaths{
Key: filepath.Join(filepath.Dir(k.paths.Key), "."+filepath.Base(k.paths.Key)),
Cert: filepath.Join(filepath.Dir(k.paths.Cert), "."+filepath.Base(k.paths.Cert)),
}
}
// Target returns a string representation of this KeyReadWriter, namely where
// it is writing to
func (k *KeyReadWriter) Target() string {
return k.paths.Cert
}
func (k *KeyReadWriter) readKeyblock() (*pem.Block, error) {
key, err := ioutil.ReadFile(k.paths.Key)
if err != nil {
return nil, err
}
// Decode the PEM private key
keyBlock, _ := pem.Decode(key)
if keyBlock == nil {
return nil, errors.New("invalid PEM-encoded private key")
}
return keyBlock, nil
}
// readKey returns the decrypted key pem bytes, and enforces the KEK if applicable
// (writes it back with the correct encryption if it is not correctly encrypted)
func (k *KeyReadWriter) readKey() (*pem.Block, error) {
keyBlock, err := k.readKeyblock()
if err != nil {
return nil, err
}
if !keyutils.IsEncryptedPEMBlock(keyBlock) {
return keyBlock, nil
}
// If it's encrypted, we can't read without a passphrase (we're assuming
// empty passphrases are invalid)
if k.kekData.KEK == nil {
return nil, ErrInvalidKEK{Wrapped: x509.IncorrectPasswordError}
}
derBytes, err := k.keyFormatter.DecryptPEMBlock(keyBlock, k.kekData.KEK)
if err == keyutils.ErrFIPSUnsupportedKeyFormat {
return nil, err
} else if err != nil {
return nil, ErrInvalidKEK{Wrapped: err}
}
// change header only if its pkcs8
if keyBlock.Type == "ENCRYPTED PRIVATE KEY" {
keyBlock.Type = "PRIVATE KEY"
}
// remove encryption PEM headers
headers := make(map[string]string)
mergePEMHeaders(headers, keyBlock.Headers)
return &pem.Block{
Type: keyBlock.Type, // the key type doesn't change
Bytes: derBytes,
Headers: headers,
}, nil
}
// writeKey takes an unencrypted keyblock and, if the kek is not nil, encrypts it before
// writing it to disk. If the kek is nil, writes it to disk unencrypted.
func (k *KeyReadWriter) writeKey(keyBlock *pem.Block, kekData KEKData, pkh PEMKeyHeaders) error {
if kekData.KEK != nil {
encryptedPEMBlock, err := k.keyFormatter.EncryptPEMBlock(keyBlock.Bytes, kekData.KEK)
if err != nil {
return err
}
if !keyutils.IsEncryptedPEMBlock(encryptedPEMBlock) {
return errors.New("unable to encrypt key - invalid PEM file produced")
}
keyBlock = encryptedPEMBlock
}
if pkh != nil {
headers, err := pkh.MarshalHeaders(kekData)
if err != nil {
return err
}
mergePEMHeaders(keyBlock.Headers, headers)
}
keyBlock.Headers[versionHeader] = strconv.FormatUint(kekData.Version, 10)
if err := ioutils.AtomicWriteFile(k.paths.Key, pem.EncodeToMemory(keyBlock), keyPerms); err != nil {
return err
}
k.kekData = kekData
k.headersObj = pkh
return nil
}
// DowngradeKey converts the PKCS#8 key to PKCS#1 format and save it
func (k *KeyReadWriter) DowngradeKey() error {
_, key, err := k.Read()
if err != nil {
return err
}
oldBlock, _ := pem.Decode(key)
if oldBlock == nil {
return errors.New("invalid PEM-encoded private key")
}
// stop if the key is already downgraded to pkcs1
if !keyutils.IsPKCS8(oldBlock.Bytes) {
return errors.New("key is already downgraded to PKCS#1")
}
eckey, err := pkcs8.ConvertToECPrivateKeyPEM(key)
if err != nil {
return err
}
newBlock, _ := pem.Decode(eckey)
if newBlock == nil {
return errors.New("invalid PEM-encoded private key")
}
if k.kekData.KEK != nil {
newBlock, err = k.keyFormatter.EncryptPEMBlock(newBlock.Bytes, k.kekData.KEK)
if err != nil {
return err
}
}
// add kek-version header back to the new key
newBlock.Headers[versionHeader] = strconv.FormatUint(k.kekData.Version, 10)
mergePEMHeaders(newBlock.Headers, oldBlock.Headers)
// do not use krw.Write as it will convert the key to pkcs8
return ioutils.AtomicWriteFile(k.paths.Key, pem.EncodeToMemory(newBlock), keyPerms)
}
// merges one set of PEM headers onto another, excepting for key encryption value
// "proc-type" and "dek-info"
func mergePEMHeaders(original, newSet map[string]string) {
for key, value := range newSet {
normalizedKey := strings.TrimSpace(strings.ToLower(key))
if normalizedKey != "proc-type" && normalizedKey != "dek-info" {
original[key] = value
}
}
}