blob: da7d301f4ed05f8b6cf7258a85d65780b8685a98 [file] [log] [blame]
Serge Bazanskidbfc6382020-06-19 20:35:43 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package pki
18
19import (
20 "context"
21 "crypto/ed25519"
22 "crypto/x509"
23 "crypto/x509/pkix"
24 "fmt"
25 "net"
26
27 "go.etcd.io/etcd/clientv3"
28)
29
30// Certificate is the promise of a Certificate being available to the caller. In this case, Certificate refers to a
31// pair of x509 certificate and corresponding private key.
32// Certificates can be stored in etcd, and their issuers might also be store on etcd. As such, this type's methods
33// contain references to an etcd KV client.
34// This Certificate type is agnostic to usage, but mostly geared towards Kubernetes certificates.
35type Certificate struct {
36 // issuer is the Issuer that will generate this certificate if one doesn't yet exist or etcd, or the requested
37 // certificate is volatile (not to be stored on etcd).
38 issuer Issuer
39 // name is a unique key for storing the certificate in etcd. If empty, certificate is 'volatile', will not be stored
40 // on etcd, and every .Ensure() call will generate a new pair.
41 name string
42 // template is an x509 certificate definition that will be used to generate the certificate when issuing it.
43 template x509.Certificate
44}
45
46const (
47 // etcdPrefix is where all the PKI data is stored in etcd.
48 etcdPrefix = "/kube-pki/"
49)
50
51func etcdPath(f string, args ...interface{}) string {
52 return etcdPrefix + fmt.Sprintf(f, args...)
53}
54
55// New creates a new Certificate, or to be more precise, a promise that a certificate will exist once Ensure is called.
56// Issuer must be a valid certificate issuer (SelfSigned or another Certificate). Name must be unique among all
57// certificates, or empty (which will cause the certificate to be volatile, ie. not stored in etcd).
58func New(issuer Issuer, name string, template x509.Certificate) *Certificate {
59 return &Certificate{
60 issuer: issuer,
61 name: name,
62 template: template,
63 }
64}
65
66// Client makes a Kubernetes PKI-compatible client certificate template.
67// Directly derived from Kubernetes PKI requirements documented at
68// https://kubernetes.io/docs/setup/best-practices/certificates/#configure-certificates-manually
69func Client(identity string, groups []string) x509.Certificate {
70 return x509.Certificate{
71 Subject: pkix.Name{
72 CommonName: identity,
73 Organization: groups,
74 },
75 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
76 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
77 }
78}
79
80// Server makes a Kubernetes PKI-compatible server certificate template.
81func Server(dnsNames []string, ips []net.IP) x509.Certificate {
82 return x509.Certificate{
83 Subject: pkix.Name{},
84 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
85 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
86 DNSNames: dnsNames,
87 IPAddresses: ips,
88 }
89}
90
91// CA makes a Certificate that can sign other certificates.
92func CA(cn string) x509.Certificate {
93 return x509.Certificate{
94 Subject: pkix.Name{
95 CommonName: cn,
96 },
97 IsCA: true,
98 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
99 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageOCSPSigning},
100 }
101}
102
103func (c *Certificate) etcdPaths() (cert, key string) {
104 return etcdPath("%s-cert.der", c.name), etcdPath("%s-key.der", c.name)
105}
106
107// ensure returns a DER-encoded x509 certificate and internally encoded bare ed25519 key for a given Certificate,
108// in memory (if volatile), loading it from etcd, or creating and saving it on etcd if needed.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200109// This function is safe to call in parallel from multiple etcd clients (including across machines), but it will error
110// in case a concurrent certificate generation happens. These errors are, however, safe to retry - as long as all the
Serge Bazanski662b5b32020-12-21 13:49:00 +0100111// certificate creators (ie., Metropolis nodes) run the same version of this code.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200112// TODO(q3k): in the future, this should be handled better - especially as we introduce new certificates, or worse,
113// change the issuance chain. As a stopgap measure, an explicit per-certificate or even global lock can be implemented.
114// And, even before that, we can handle concurrency errors in a smarter way.
Serge Bazanskidbfc6382020-06-19 20:35:43 +0200115func (c *Certificate) ensure(ctx context.Context, kv clientv3.KV) (cert, key []byte, err error) {
116 if c.name == "" {
117 // Volatile certificate - generate.
118 // TODO(q3k): cache internally?
119 cert, key, err = c.issuer.Issue(ctx, c.template, kv)
120 if err != nil {
121 err = fmt.Errorf("failed to issue: %w", err)
122 return
123 }
124 return
125 }
126
127 certPath, keyPath := c.etcdPaths()
128
129 // Try loading certificate and key from etcd.
130 certRes, err := kv.Get(ctx, certPath)
131 if err != nil {
132 err = fmt.Errorf("failed to get certificate from etcd: %w", err)
133 return
134 }
135 keyRes, err := kv.Get(ctx, keyPath)
136 if err != nil {
137 err = fmt.Errorf("failed to get key from etcd: %w", err)
138 return
139 }
140
141 if len(certRes.Kvs) == 1 && len(keyRes.Kvs) == 1 {
142 // Certificate and key exists in etcd, return that.
143 cert = certRes.Kvs[0].Value
144 key = keyRes.Kvs[0].Value
145
146 err = nil
147 // TODO(q3k): check for expiration
148 return
149 }
150
151 // No certificate found - issue one.
152 cert, key, err = c.issuer.Issue(ctx, c.template, kv)
153 if err != nil {
154 err = fmt.Errorf("failed to issue: %w", err)
155 return
156 }
157
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200158 // Save to etcd in transaction. This ensures that no partial writes happen, and that we haven't been raced to the
159 // save.
160 res, err := kv.Txn(ctx).
161 If(
162 clientv3.Compare(clientv3.CreateRevision(certPath), "=", 0),
163 clientv3.Compare(clientv3.CreateRevision(keyPath), "=", 0),
164 ).
Serge Bazanskidbfc6382020-06-19 20:35:43 +0200165 Then(
166 clientv3.OpPut(certPath, string(cert)),
167 clientv3.OpPut(keyPath, string(key)),
168 ).Commit()
169 if err != nil {
170 err = fmt.Errorf("failed to write newly issued certificate: %w", err)
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200171 } else if !res.Succeeded {
172 err = fmt.Errorf("certificate issuance transaction failed: concurrent write")
Serge Bazanskidbfc6382020-06-19 20:35:43 +0200173 }
174
175 return
176}
177
178// Ensure returns an x509 DER-encoded (but not PEM-encoded) certificate and key for a given Certificate.
179// If the certificate is volatile, each call to Ensure will cause a new certificate to be generated.
180// Otherwise, it will be retrieved from etcd, or generated and stored there if needed.
181func (c *Certificate) Ensure(ctx context.Context, kv clientv3.KV) (cert, key []byte, err error) {
182 cert, key, err = c.ensure(ctx, kv)
183 if err != nil {
184 return nil, nil, err
185 }
186 key, err = x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(key))
187 if err != nil {
188 err = fmt.Errorf("could not marshal private key (data corruption?): %w", err)
189 return
190 }
191 return cert, key, err
192}