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