k8s.io/client-go@v0.31.1/util/certificate/certificate_store.go (about) 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 }