blob: c0a1f53eb042194727ece87a105ad8e0d37fb7ff [file] [log] [blame]
Serge Bazanski9411f7c2021-03-10 13:12:53 +01001// 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 "encoding/pem"
25 "fmt"
26 "net"
27
28 "go.etcd.io/etcd/clientv3"
29
30 "source.monogon.dev/metropolis/pkg/fileargs"
31)
32
33// Namespace represents some path in etcd where certificate/CA data will be
34// stored. Creating a namespace via Namespaced then permits the consumer of
35// this library to start creating certificates within this namespace.
36type Namespace struct {
37 prefix string
38}
39
Serge Bazanski216fe7b2021-05-21 18:36:16 +020040// Namespaced creates a namespace for storing certificate data in etcd at a
41// given 'path' prefix.
Serge Bazanski9411f7c2021-03-10 13:12:53 +010042func Namespaced(prefix string) Namespace {
43 return Namespace{
44 prefix: prefix,
45 }
46}
47
48// Certificate is the promise of a Certificate being available to the caller.
49// In this case, Certificate refers to a pair of x509 certificate and
50// corresponding private key. Certificates can be stored in etcd, and their
51// issuers might also be store on etcd. As such, this type's methods contain
52// references to an etcd KV client. This Certificate type is agnostic to
53// usage, but mostly geared towards Kubernetes certificates.
54type Certificate struct {
55 namespace *Namespace
56
57 // issuer is the Issuer that will generate this certificate if one doesn't
58 // yet exist or etcd, or the requested certificate is volatile (not to be
59 // stored on etcd).
60 Issuer Issuer
61 // name is a unique key for storing the certificate in etcd. If empty,
62 // certificate is 'volatile', will not be stored on etcd, and every
63 // .Ensure() call will generate a new pair.
64 name string
65 // template is an x509 certificate definition that will be used to generate
66 // the certificate when issuing it.
67 template x509.Certificate
68 // key is the private key for which the certificate should emitted, or nil
69 // if the key should be generated. The private key is required (vs. the
70 // private one) because the Certificate might be attempted to be issued via
71 // self-signing.
72 key ed25519.PrivateKey
73}
74
75func (n *Namespace) etcdPath(f string, args ...interface{}) string {
76 return n.prefix + fmt.Sprintf(f, args...)
77}
78
79// New creates a new Certificate, or to be more precise, a promise that a
80// certificate will exist once Ensure is called. Issuer must be a valid
81// certificate issuer (SelfSigned or another Certificate). Name must be unique
82// among all certificates, or empty (which will cause the certificate to be
83// volatile, ie. not stored in etcd).
84func (n *Namespace) New(issuer Issuer, name string, template x509.Certificate) *Certificate {
85 return &Certificate{
86 namespace: n,
87 Issuer: issuer,
88 name: name,
89 template: template,
90 }
91}
92
93// Client makes a Kubernetes PKI-compatible client certificate template.
94// Directly derived from Kubernetes PKI requirements documented at
Serge Bazanski216fe7b2021-05-21 18:36:16 +020095// https://kubernetes.io/docs/setup/best-practices/certificates/#configure-certificates-manually
Serge Bazanski9411f7c2021-03-10 13:12:53 +010096func Client(identity string, groups []string) x509.Certificate {
97 return x509.Certificate{
98 Subject: pkix.Name{
99 CommonName: identity,
100 Organization: groups,
101 },
102 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
103 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
104 }
105}
106
107// Server makes a Kubernetes PKI-compatible server certificate template.
108func Server(dnsNames []string, ips []net.IP) x509.Certificate {
109 return x509.Certificate{
110 Subject: pkix.Name{},
111 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
112 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
113 DNSNames: dnsNames,
114 IPAddresses: ips,
115 }
116}
117
118// CA makes a Certificate that can sign other certificates.
119func CA(cn string) x509.Certificate {
120 return x509.Certificate{
121 Subject: pkix.Name{
122 CommonName: cn,
123 },
124 IsCA: true,
125 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
126 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageOCSPSigning},
127 }
128}
129
130func (c *Certificate) etcdPaths() (cert, key string) {
131 return c.namespace.etcdPath("%s-cert.der", c.name), c.namespace.etcdPath("%s-key.der", c.name)
132}
133
134func (c *Certificate) UseExistingKey(key ed25519.PrivateKey) {
135 c.key = key
136}
137
138// ensure returns a DER-encoded x509 certificate and internally encoded bare
139// ed25519 key for a given Certificate, in memory (if volatile), loading it
140// from etcd, or creating and saving it on etcd if needed.
141// This function is safe to call in parallel from multiple etcd clients
142// (including across machines), but it will error in case a concurrent
143// certificate generation happens. These errors are, however, safe to retry -
144// as long as all the certificate creators (ie., Metropolis nodes) run the same
145// version of this code.
146//
147// TODO(q3k): in the future, this should be handled better - especially as we
148// introduce new certificates, or worse, change the issuance chain. As a
149// stopgap measure, an explicit per-certificate or even global lock can be
150// implemented. And, even before that, we can handle concurrency errors in a
151// smarter way.
152func (c *Certificate) ensure(ctx context.Context, kv clientv3.KV) (cert, key []byte, err error) {
153 if c.name == "" {
154 // Volatile certificate - generate.
155 // TODO(q3k): cache internally?
156 cert, key, err = c.Issuer.Issue(ctx, c, kv)
157 if err != nil {
158 err = fmt.Errorf("failed to issue: %w", err)
159 return
160 }
161 return
162 }
163
164 certPath, keyPath := c.etcdPaths()
165
166 // Try loading certificate and key from etcd.
167 certRes, err := kv.Get(ctx, certPath)
168 if err != nil {
169 err = fmt.Errorf("failed to get certificate from etcd: %w", err)
170 return
171 }
172 keyRes, err := kv.Get(ctx, keyPath)
173 if err != nil {
174 err = fmt.Errorf("failed to get key from etcd: %w", err)
175 return
176 }
177
178 if len(certRes.Kvs) == 1 && len(keyRes.Kvs) == 1 {
179 // Certificate and key exists in etcd, return that.
180 cert = certRes.Kvs[0].Value
181 key = keyRes.Kvs[0].Value
182
183 err = nil
184 // TODO(q3k): check for expiration
185 return
186 }
187
188 // No certificate found - issue one.
189 cert, key, err = c.Issuer.Issue(ctx, c, kv)
190 if err != nil {
191 err = fmt.Errorf("failed to issue: %w", err)
192 return
193 }
194
195 // Save to etcd in transaction. This ensures that no partial writes happen,
196 // and that we haven't been raced to the save.
197 res, err := kv.Txn(ctx).
198 If(
199 clientv3.Compare(clientv3.CreateRevision(certPath), "=", 0),
200 clientv3.Compare(clientv3.CreateRevision(keyPath), "=", 0),
201 ).
202 Then(
203 clientv3.OpPut(certPath, string(cert)),
204 clientv3.OpPut(keyPath, string(key)),
205 ).Commit()
206 if err != nil {
207 err = fmt.Errorf("failed to write newly issued certificate: %w", err)
208 } else if !res.Succeeded {
209 err = fmt.Errorf("certificate issuance transaction failed: concurrent write")
210 }
211
212 return
213}
214
215// Ensure returns an x509 DER-encoded (but not PEM-encoded) certificate and key
216// for a given Certificate. If the certificate is volatile, each call to
217// Ensure will cause a new certificate to be generated. Otherwise, it will be
218// retrieved from etcd, or generated and stored there if needed.
219func (c *Certificate) Ensure(ctx context.Context, kv clientv3.KV) (cert, key []byte, err error) {
220 cert, key, err = c.ensure(ctx, kv)
221 if err != nil {
222 return nil, nil, err
223 }
224 key, err = x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(key))
225 if err != nil {
226 err = fmt.Errorf("could not marshal private key (data corruption?): %w", err)
227 return
228 }
229 return cert, key, err
230}
231
232// FilesystemCertificate is a fileargs.FileArgs wrapper which will contain PEM
233// encoded certificate material when Mounted. This construct is useful when
234// dealing with services that want to access etcd-backed certificates as files
235// available locally.
236// Paths to the available files are considered opaque and should not be leaked
237// outside of the struct. Further restrictions on access to these files might
238// be imposed in the future.
239type FilesystemCertificate struct {
240 *fileargs.FileArgs
241 // CACertPath is the full path at which the CA certificate is available.
242 // Read only.
243 CACertPath string
244 // CertPath is the full path at which the certificate is available. Read
245 // only.
246 CertPath string
247 // KeyPath is the full path at which the key is available. Read only.
248 KeyPath string
249}
250
251// Mount returns a locally mounted FilesystemCertificate for this Certificate,
252// which allows services to access this Certificate via local filesystem
253// access.
254// The embeded fileargs.FileArgs can also be used to add additional file-backed
255// data under the same mount by calling ArgPath.
256// The returned FilesystemCertificate must be Closed in order to prevent a
257// system mount leak.
258func (c *Certificate) Mount(ctx context.Context, kv clientv3.KV) (*FilesystemCertificate, error) {
259 fa, err := fileargs.New()
260 if err != nil {
261 return nil, fmt.Errorf("when creating fileargs mount: %w", err)
262 }
263 fs := &FilesystemCertificate{FileArgs: fa}
264
265 cert, key, err := c.Ensure(ctx, kv)
266 if err != nil {
267 return nil, fmt.Errorf("when issuing certificate: %w", err)
268 }
269
270 cacert, err := c.Issuer.CACertificate(ctx, kv)
271 if err != nil {
272 return nil, fmt.Errorf("when getting issuer CA: %w", err)
273 }
274 // cacert will be null if this is a self-signed certificate.
275 if cacert == nil {
276 cacert = cert
277 }
278
279 fs.CACertPath = fs.ArgPath("ca.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cacert}))
280 fs.CertPath = fs.ArgPath("tls.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}))
281 fs.KeyPath = fs.ArgPath("tls.key", pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key}))
282
283 return fs, nil
284}