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	certutil "k8s.io/client-go/util/cert"
29	"k8s.io/klog/v2"
30)
31
32const (
33	keyExtension  = ".key"
34	certExtension = ".crt"
35	pemExtension  = ".pem"
36	currentPair   = "current"
37	updatedPair   = "updated"
38)
39
40type 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.
50type 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.
70func 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.
91func (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.
97func (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
125func (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
172func 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
187func (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.
231func (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
293func (s *fileStore) filename(qualifier string) string {
294	return s.pairNamePrefix + "-" + qualifier + pemExtension
295}
296
297func 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.
311func 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