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