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