...

Source file src/k8s.io/client-go/util/certificate/certificate_store.go

Documentation: k8s.io/client-go/util/certificate

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package certificate
    18  
    19  import (
    20  	"crypto/tls"
    21  	"crypto/x509"
    22  	"encoding/pem"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"time"
    27  
    28  	certutil "k8s.io/client-go/util/cert"
    29  	"k8s.io/klog/v2"
    30  )
    31  
    32  const (
    33  	keyExtension  = ".key"
    34  	certExtension = ".crt"
    35  	pemExtension  = ".pem"
    36  	currentPair   = "current"
    37  	updatedPair   = "updated"
    38  )
    39  
    40  type fileStore struct {
    41  	pairNamePrefix string
    42  	certDirectory  string
    43  	keyDirectory   string
    44  	certFile       string
    45  	keyFile        string
    46  }
    47  
    48  // FileStore is a store that provides certificate retrieval as well as
    49  // the path on disk of the current PEM.
    50  type FileStore interface {
    51  	Store
    52  	// CurrentPath returns the path on disk of the current certificate/key
    53  	// pair encoded as PEM files.
    54  	CurrentPath() string
    55  }
    56  
    57  // NewFileStore returns a concrete implementation of a Store that is based on
    58  // storing the cert/key pairs in a single file per pair on disk in the
    59  // designated directory. When starting up it will look for the currently
    60  // selected cert/key pair in:
    61  //
    62  // 1. ${certDirectory}/${pairNamePrefix}-current.pem - both cert and key are in the same file.
    63  // 2. ${certFile}, ${keyFile}
    64  // 3. ${certDirectory}/${pairNamePrefix}.crt, ${keyDirectory}/${pairNamePrefix}.key
    65  //
    66  // The first one found will be used. If rotation is enabled, future cert/key
    67  // updates will be written to the ${certDirectory} directory and
    68  // ${certDirectory}/${pairNamePrefix}-current.pem will be created as a soft
    69  // link to the currently selected cert/key pair.
    70  func NewFileStore(
    71  	pairNamePrefix string,
    72  	certDirectory string,
    73  	keyDirectory string,
    74  	certFile string,
    75  	keyFile string) (FileStore, error) {
    76  
    77  	s := fileStore{
    78  		pairNamePrefix: pairNamePrefix,
    79  		certDirectory:  certDirectory,
    80  		keyDirectory:   keyDirectory,
    81  		certFile:       certFile,
    82  		keyFile:        keyFile,
    83  	}
    84  	if err := s.recover(); err != nil {
    85  		return nil, err
    86  	}
    87  	return &s, nil
    88  }
    89  
    90  // CurrentPath returns the path to the current version of these certificates.
    91  func (s *fileStore) CurrentPath() string {
    92  	return filepath.Join(s.certDirectory, s.filename(currentPair))
    93  }
    94  
    95  // recover checks if there is a certificate rotation that was interrupted while
    96  // progress, and if so, attempts to recover to a good state.
    97  func (s *fileStore) recover() error {
    98  	// If the 'current' file doesn't exist, continue on with the recovery process.
    99  	currentPath := filepath.Join(s.certDirectory, s.filename(currentPair))
   100  	if exists, err := fileExists(currentPath); err != nil {
   101  		return err
   102  	} else if exists {
   103  		return nil
   104  	}
   105  
   106  	// If the 'updated' file exists, and it is a symbolic link, continue on
   107  	// with the recovery process.
   108  	updatedPath := filepath.Join(s.certDirectory, s.filename(updatedPair))
   109  	if fi, err := os.Lstat(updatedPath); err != nil {
   110  		if os.IsNotExist(err) {
   111  			return nil
   112  		}
   113  		return err
   114  	} else if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
   115  		return fmt.Errorf("expected %q to be a symlink but it is a file", updatedPath)
   116  	}
   117  
   118  	// Move the 'updated' symlink to 'current'.
   119  	if err := os.Rename(updatedPath, currentPath); err != nil {
   120  		return fmt.Errorf("unable to rename %q to %q: %v", updatedPath, currentPath, err)
   121  	}
   122  	return nil
   123  }
   124  
   125  func (s *fileStore) Current() (*tls.Certificate, error) {
   126  	pairFile := filepath.Join(s.certDirectory, s.filename(currentPair))
   127  	if pairFileExists, err := fileExists(pairFile); err != nil {
   128  		return nil, err
   129  	} else if pairFileExists {
   130  		klog.Infof("Loading cert/key pair from %q.", pairFile)
   131  		return loadFile(pairFile)
   132  	}
   133  
   134  	certFileExists, err := fileExists(s.certFile)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	keyFileExists, err := fileExists(s.keyFile)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	if certFileExists && keyFileExists {
   143  		klog.Infof("Loading cert/key pair from (%q, %q).", s.certFile, s.keyFile)
   144  		return loadX509KeyPair(s.certFile, s.keyFile)
   145  	}
   146  
   147  	c := filepath.Join(s.certDirectory, s.pairNamePrefix+certExtension)
   148  	k := filepath.Join(s.keyDirectory, s.pairNamePrefix+keyExtension)
   149  	certFileExists, err = fileExists(c)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	keyFileExists, err = fileExists(k)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  	if certFileExists && keyFileExists {
   158  		klog.Infof("Loading cert/key pair from (%q, %q).", c, k)
   159  		return loadX509KeyPair(c, k)
   160  	}
   161  
   162  	noKeyErr := NoCertKeyError(
   163  		fmt.Sprintf("no cert/key files read at %q, (%q, %q) or (%q, %q)",
   164  			pairFile,
   165  			s.certFile,
   166  			s.keyFile,
   167  			s.certDirectory,
   168  			s.keyDirectory))
   169  	return nil, &noKeyErr
   170  }
   171  
   172  func loadFile(pairFile string) (*tls.Certificate, error) {
   173  	// LoadX509KeyPair knows how to parse combined cert and private key from
   174  	// the same file.
   175  	cert, err := tls.LoadX509KeyPair(pairFile, pairFile)
   176  	if err != nil {
   177  		return nil, fmt.Errorf("could not convert data from %q into cert/key pair: %v", pairFile, err)
   178  	}
   179  	certs, err := x509.ParseCertificates(cert.Certificate[0])
   180  	if err != nil {
   181  		return nil, fmt.Errorf("unable to parse certificate data: %v", err)
   182  	}
   183  	cert.Leaf = certs[0]
   184  	return &cert, nil
   185  }
   186  
   187  func (s *fileStore) Update(certData, keyData []byte) (*tls.Certificate, error) {
   188  	ts := time.Now().Format("2006-01-02-15-04-05")
   189  	pemFilename := s.filename(ts)
   190  
   191  	if err := os.MkdirAll(s.certDirectory, 0755); err != nil {
   192  		return nil, fmt.Errorf("could not create directory %q to store certificates: %v", s.certDirectory, err)
   193  	}
   194  	certPath := filepath.Join(s.certDirectory, pemFilename)
   195  
   196  	f, err := os.OpenFile(certPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0600)
   197  	if err != nil {
   198  		return nil, fmt.Errorf("could not open %q: %v", certPath, err)
   199  	}
   200  	defer f.Close()
   201  
   202  	// First cert is leaf, remainder are intermediates
   203  	certs, err := certutil.ParseCertsPEM(certData)
   204  	if err != nil {
   205  		return nil, fmt.Errorf("invalid certificate data: %v", err)
   206  	}
   207  	for _, c := range certs {
   208  		pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})
   209  	}
   210  
   211  	keyBlock, _ := pem.Decode(keyData)
   212  	if keyBlock == nil {
   213  		return nil, fmt.Errorf("invalid key data")
   214  	}
   215  	pem.Encode(f, keyBlock)
   216  
   217  	cert, err := loadFile(certPath)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  	if err := s.updateSymlink(certPath); err != nil {
   223  		return nil, err
   224  	}
   225  	return cert, nil
   226  }
   227  
   228  // updateSymLink updates the current symlink to point to the file that is
   229  // passed it. It will fail if there is a non-symlink file exists where the
   230  // symlink is expected to be.
   231  func (s *fileStore) updateSymlink(filename string) error {
   232  	// If the 'current' file either doesn't exist, or is already a symlink,
   233  	// proceed. Otherwise, this is an unrecoverable error.
   234  	currentPath := filepath.Join(s.certDirectory, s.filename(currentPair))
   235  	currentPathExists := false
   236  	if fi, err := os.Lstat(currentPath); err != nil {
   237  		if !os.IsNotExist(err) {
   238  			return err
   239  		}
   240  	} else if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
   241  		return fmt.Errorf("expected %q to be a symlink but it is a file", currentPath)
   242  	} else {
   243  		currentPathExists = true
   244  	}
   245  
   246  	// If the 'updated' file doesn't exist, proceed. If it exists but it is a
   247  	// symlink, delete it.  Otherwise, this is an unrecoverable error.
   248  	updatedPath := filepath.Join(s.certDirectory, s.filename(updatedPair))
   249  	if fi, err := os.Lstat(updatedPath); err != nil {
   250  		if !os.IsNotExist(err) {
   251  			return err
   252  		}
   253  	} else if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
   254  		return fmt.Errorf("expected %q to be a symlink but it is a file", updatedPath)
   255  	} else {
   256  		if err := os.Remove(updatedPath); err != nil {
   257  			return fmt.Errorf("unable to remove %q: %v", updatedPath, err)
   258  		}
   259  	}
   260  
   261  	// Check that the new cert/key pair file exists to avoid rotating to an
   262  	// invalid cert/key.
   263  	if filenameExists, err := fileExists(filename); err != nil {
   264  		return err
   265  	} else if !filenameExists {
   266  		return fmt.Errorf("file %q does not exist so it can not be used as the currently selected cert/key", filename)
   267  	}
   268  
   269  	// Ensure the source path is absolute to ensure the symlink target is
   270  	// correct when certDirectory is a relative path.
   271  	filename, err := filepath.Abs(filename)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	// Create the 'updated' symlink pointing to the requested file name.
   277  	if err := os.Symlink(filename, updatedPath); err != nil {
   278  		return fmt.Errorf("unable to create a symlink from %q to %q: %v", updatedPath, filename, err)
   279  	}
   280  
   281  	// Replace the 'current' symlink.
   282  	if currentPathExists {
   283  		if err := os.Remove(currentPath); err != nil {
   284  			return fmt.Errorf("unable to remove %q: %v", currentPath, err)
   285  		}
   286  	}
   287  	if err := os.Rename(updatedPath, currentPath); err != nil {
   288  		return fmt.Errorf("unable to rename %q to %q: %v", updatedPath, currentPath, err)
   289  	}
   290  	return nil
   291  }
   292  
   293  func (s *fileStore) filename(qualifier string) string {
   294  	return s.pairNamePrefix + "-" + qualifier + pemExtension
   295  }
   296  
   297  func loadX509KeyPair(certFile, keyFile string) (*tls.Certificate, error) {
   298  	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  	certs, err := x509.ParseCertificates(cert.Certificate[0])
   303  	if err != nil {
   304  		return nil, fmt.Errorf("unable to parse certificate data: %v", err)
   305  	}
   306  	cert.Leaf = certs[0]
   307  	return &cert, nil
   308  }
   309  
   310  // FileExists checks if specified file exists.
   311  func fileExists(filename string) (bool, error) {
   312  	if _, err := os.Stat(filename); os.IsNotExist(err) {
   313  		return false, nil
   314  	} else if err != nil {
   315  		return false, err
   316  	}
   317  	return true, nil
   318  }
   319  

View as plain text