blob: 93e6ed92a1b2f7cafdd2148a2101d4404bb654b9 [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
Serge Bazanski52538842021-08-11 16:22:41 +020017// package pki implements an x509 PKI (Public Key Infrastructure) system backed
18// on etcd.
Serge Bazanski9411f7c2021-03-10 13:12:53 +010019package pki
20
21import (
Serge Bazanski52538842021-08-11 16:22:41 +020022 "bytes"
Serge Bazanski9411f7c2021-03-10 13:12:53 +010023 "context"
24 "crypto/ed25519"
Serge Bazanski52538842021-08-11 16:22:41 +020025 "crypto/rand"
Serge Bazanski9411f7c2021-03-10 13:12:53 +010026 "crypto/x509"
27 "crypto/x509/pkix"
28 "encoding/pem"
29 "fmt"
30 "net"
31
Lorenz Brund13c1c62022-03-30 19:58:58 +020032 clientv3 "go.etcd.io/etcd/client/v3"
Serge Bazanski9411f7c2021-03-10 13:12:53 +010033
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020034 "source.monogon.dev/osbase/fileargs"
Serge Bazanski9411f7c2021-03-10 13:12:53 +010035)
36
37// Namespace represents some path in etcd where certificate/CA data will be
38// stored. Creating a namespace via Namespaced then permits the consumer of
39// this library to start creating certificates within this namespace.
40type Namespace struct {
41 prefix string
42}
43
Serge Bazanski216fe7b2021-05-21 18:36:16 +020044// Namespaced creates a namespace for storing certificate data in etcd at a
45// given 'path' prefix.
Serge Bazanski9411f7c2021-03-10 13:12:53 +010046func Namespaced(prefix string) Namespace {
47 return Namespace{
48 prefix: prefix,
49 }
50}
51
Serge Bazanski52538842021-08-11 16:22:41 +020052type CertificateMode int
53
54const (
55 // CertificateManaged is a certificate whose key material is fully managed by
56 // the Certificate code. When set, PublicKey and PrivateKey must not be set by
57 // the user, and instead will be populated by the Ensure call. Name must be set,
58 // and will be used to store this Certificate and its keys within etcd. After
59 // the initial generation during Ensure, other Certificates with the same Name
60 // will be retrieved (including key material) from etcd.
61 CertificateManaged CertificateMode = iota
62
63 // CertificateExternal is a certificate whose key material is not managed by
64 // Certificate or stored in etcd, but the X509 certificate itself is. PublicKey
65 // must be set while PrivateKey must not be set. Name must be set, and will be
66 // used to store the emitted X509 certificate in etcd on Ensure. After the
67 // initial generation during Ensure, other Certificates with the same Name will
68 // be retrieved (without key material) from etcd.
69 CertificateExternal
70
71 // CertificateEphemeral is a certificate whose data (X509 certificate and
72 // possibly key material) is generated on demand each time Ensure is called.
73 // Nothing is stored in etcd or loaded from etcd. PrivateKey or PublicKey can be
74 // set, if both are nil then a new keypair will be generated. Name is ignored.
75 CertificateEphemeral
76)
77
Serge Bazanski9411f7c2021-03-10 13:12:53 +010078// Certificate is the promise of a Certificate being available to the caller.
79// In this case, Certificate refers to a pair of x509 certificate and
80// corresponding private key. Certificates can be stored in etcd, and their
81// issuers might also be store on etcd. As such, this type's methods contain
Serge Bazanski52538842021-08-11 16:22:41 +020082// references to an etcd KV client.
Serge Bazanski9411f7c2021-03-10 13:12:53 +010083type Certificate struct {
Serge Bazanski52538842021-08-11 16:22:41 +020084 Namespace *Namespace
Serge Bazanski9411f7c2021-03-10 13:12:53 +010085
Serge Bazanski52538842021-08-11 16:22:41 +020086 // Issuer is the Issuer that will generate this certificate if one doesn't
87 // yet exist or etcd, or the requested certificate is ephemeral (not to be
Serge Bazanski9411f7c2021-03-10 13:12:53 +010088 // stored on etcd).
89 Issuer Issuer
Serge Bazanski52538842021-08-11 16:22:41 +020090 // Name is a unique key for storing the certificate in etcd (if the requested
91 // certificate is not ephemeral).
92 Name string
93 // Template is an x509 certificate definition that will be used to generate
Serge Bazanski9411f7c2021-03-10 13:12:53 +010094 // the certificate when issuing it.
Serge Bazanski52538842021-08-11 16:22:41 +020095 Template x509.Certificate
96
97 // Mode in which this Certificate will operate. This influences the behaviour of
98 // the Ensure call.
99 Mode CertificateMode
100
101 // PrivateKey is the private key for this Certificate. It should never be set by
102 // the user, and instead will be populated by the Ensure call for Managed
103 // Certificates.
104 PrivateKey ed25519.PrivateKey
105
106 // PublicKey is the public key for this Certificate. It should only be set by
107 // the user for External or Ephemeral certificates, and will be populated by the
108 // next Ensure call if missing.
109 PublicKey ed25519.PublicKey
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100110}
111
112func (n *Namespace) etcdPath(f string, args ...interface{}) string {
113 return n.prefix + fmt.Sprintf(f, args...)
114}
115
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100116// Client makes a Kubernetes PKI-compatible client certificate template.
117// Directly derived from Kubernetes PKI requirements documented at
Tim Windelschmidt99e15112025-02-05 17:38:16 +0100118//
119// https://kubernetes.io/docs/setup/best-practices/certificates/#configure-certificates-manually
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100120func Client(identity string, groups []string) x509.Certificate {
121 return x509.Certificate{
122 Subject: pkix.Name{
123 CommonName: identity,
124 Organization: groups,
125 },
126 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
127 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
128 }
129}
130
131// Server makes a Kubernetes PKI-compatible server certificate template.
132func Server(dnsNames []string, ips []net.IP) x509.Certificate {
133 return x509.Certificate{
134 Subject: pkix.Name{},
135 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
136 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
137 DNSNames: dnsNames,
138 IPAddresses: ips,
139 }
140}
141
142// CA makes a Certificate that can sign other certificates.
143func CA(cn string) x509.Certificate {
144 return x509.Certificate{
145 Subject: pkix.Name{
146 CommonName: cn,
147 },
148 IsCA: true,
149 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
150 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageOCSPSigning},
151 }
152}
153
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100154// ensure returns a DER-encoded x509 certificate and internally encoded bare
Serge Bazanski52538842021-08-11 16:22:41 +0200155// ed25519 key for a given Certificate, in memory (if ephemeral), loading it
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100156// from etcd, or creating and saving it on etcd if needed.
157// This function is safe to call in parallel from multiple etcd clients
158// (including across machines), but it will error in case a concurrent
159// certificate generation happens. These errors are, however, safe to retry -
160// as long as all the certificate creators (ie., Metropolis nodes) run the same
161// version of this code.
Serge Bazanski52538842021-08-11 16:22:41 +0200162func (c *Certificate) ensure(ctx context.Context, kv clientv3.KV) (cert []byte, err error) {
163 // Ensure key is available.
164 if err := c.ensureKey(ctx, kv); err != nil {
165 return nil, err
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100166 }
167
Serge Bazanski52538842021-08-11 16:22:41 +0200168 switch c.Mode {
169 case CertificateEphemeral:
170 // TODO(q3k): cache internally?
171 cert, err = c.Issuer.Issue(ctx, c, kv)
172 if err != nil {
173 return nil, fmt.Errorf("failed to issue: %w", err)
174 }
175 return cert, nil
176 case CertificateManaged, CertificateExternal:
177 default:
178 return nil, fmt.Errorf("invalid certificate mode %v", c.Mode)
179 }
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100180
Serge Bazanskia41caac2021-08-12 17:00:55 +0200181 if c.Name == "" {
182 if c.Mode == CertificateExternal {
183 return nil, fmt.Errorf("external certificate must have name set")
184 } else {
185 return nil, fmt.Errorf("managed certificate must have name set")
186 }
187 }
188
Serge Bazanski999e1db2021-11-30 20:37:38 +0100189 certPath := c.Namespace.etcdPath("issued/%s-cert.der", c.Name)
Serge Bazanski52538842021-08-11 16:22:41 +0200190
191 // Try loading certificate from etcd.
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100192 certRes, err := kv.Get(ctx, certPath)
193 if err != nil {
Serge Bazanski52538842021-08-11 16:22:41 +0200194 return nil, fmt.Errorf("failed to get certificate from etcd: %w", err)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100195 }
Serge Bazanski52538842021-08-11 16:22:41 +0200196
197 if len(certRes.Kvs) == 1 {
198 certBytes := certRes.Kvs[0].Value
199 cert, err := x509.ParseCertificate(certBytes)
200 if err != nil {
201 return nil, fmt.Errorf("failed to parse certificate retrieved from etcd: %w", err)
202 }
203 pk, ok := cert.PublicKey.(ed25519.PublicKey)
204 if !ok {
205 return nil, fmt.Errorf("unexpected non-ed25519 certificate found in etcd")
206 }
207 if !bytes.Equal(pk, c.PublicKey) {
208 return nil, fmt.Errorf("certificate stored in etcd emitted for different public key")
209 }
210 // TODO(q3k): ensure issuer and template haven't changed
211 return certBytes, nil
212 }
213
214 // No certificate found - issue one and save to etcd.
215 cert, err = c.Issuer.Issue(ctx, c, kv)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100216 if err != nil {
Serge Bazanski52538842021-08-11 16:22:41 +0200217 return nil, fmt.Errorf("failed to issue: %w", err)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100218 }
219
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100220 res, err := kv.Txn(ctx).
221 If(
222 clientv3.Compare(clientv3.CreateRevision(certPath), "=", 0),
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100223 ).
224 Then(
225 clientv3.OpPut(certPath, string(cert)),
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100226 ).Commit()
227 if err != nil {
228 err = fmt.Errorf("failed to write newly issued certificate: %w", err)
229 } else if !res.Succeeded {
230 err = fmt.Errorf("certificate issuance transaction failed: concurrent write")
231 }
232
233 return
234}
235
Serge Bazanski52538842021-08-11 16:22:41 +0200236// ensureKey retrieves or creates PublicKey as needed based on the Certificate
237// Mode. For Managed Certificates and Ephemeral Certificates with no PrivateKey
238// it will also populate PrivateKay.
239func (c *Certificate) ensureKey(ctx context.Context, kv clientv3.KV) error {
240 // If we have a public key then we're all set.
241 if c.PublicKey != nil {
242 return nil
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100243 }
Serge Bazanski52538842021-08-11 16:22:41 +0200244
245 // For ephemeral keys, we just generate them.
246 // For external keys, we can't do anything - not having the keys set means
247 // a programming error.
248
249 switch c.Mode {
250 case CertificateEphemeral:
251 pub, priv, err := ed25519.GenerateKey(rand.Reader)
252 if err != nil {
253 return fmt.Errorf("when generating ephemeral key: %w", err)
254 }
255 c.PublicKey = pub
256 c.PrivateKey = priv
257 return nil
258 case CertificateExternal:
259 if c.PrivateKey != nil {
260 // We prohibit having PrivateKey set in External Certificates to simplify the
261 // different logic paths this library implements. Being able to assume External
262 // == PublicKey only makes things easier elsewhere.
263 return fmt.Errorf("external certificate must not have PrivateKey set")
264 }
265 return fmt.Errorf("external certificate must have PublicKey set")
266 case CertificateManaged:
267 default:
268 return fmt.Errorf("invalid certificate mode %v", c.Mode)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100269 }
Serge Bazanski52538842021-08-11 16:22:41 +0200270
271 // For managed keys, synchronize with etcd.
272 if c.Name == "" {
273 return fmt.Errorf("managed certificate must have Name set")
274 }
275
276 // First, try loading.
Serge Bazanski999e1db2021-11-30 20:37:38 +0100277 privPath := c.Namespace.etcdPath("keys/%s-privkey.bin", c.Name)
Serge Bazanski52538842021-08-11 16:22:41 +0200278 privRes, err := kv.Get(ctx, privPath)
279 if err != nil {
280 return fmt.Errorf("failed to get private key from etcd: %w", err)
281 }
282 if len(privRes.Kvs) == 1 {
283 privBytes := privRes.Kvs[0].Value
284 if len(privBytes) != ed25519.PrivateKeySize {
285 return fmt.Errorf("stored private key has invalid size")
286 }
287 c.PrivateKey = privBytes
288 c.PublicKey = c.PrivateKey.Public().(ed25519.PublicKey)
289 return nil
290 }
291
292 // No key in etcd? Generate and save.
293 pub, priv, err := ed25519.GenerateKey(rand.Reader)
294 if err != nil {
295 return fmt.Errorf("while generating keypair: %w", err)
296 }
297
298 res, err := kv.Txn(ctx).
299 If(
300 clientv3.Compare(clientv3.CreateRevision(privPath), "=", 0),
301 ).
302 Then(
303 clientv3.OpPut(privPath, string(priv)),
304 ).Commit()
305 if err != nil {
306 return fmt.Errorf("failed to write newly generated keypair: %w", err)
307 } else if !res.Succeeded {
308 return fmt.Errorf("key generation transaction failed: concurrent write")
309 }
310
Serge Bazanski999e1db2021-11-30 20:37:38 +0100311 crlPath := c.crlPath()
312 emptyCRL, err := c.makeCRL(ctx, kv, nil)
313 if err != nil {
314 return fmt.Errorf("failed to generate empty CRL: %w", err)
315 }
316
317 // Also attempt to emit an empty CRL if one doesn't exist yet.
318 _, err = kv.Txn(ctx).
319 If(
320 clientv3.Compare(clientv3.CreateRevision(crlPath), "=", 0),
321 ).
322 Then(
323 clientv3.OpPut(crlPath, string(emptyCRL)),
324 ).Commit()
325 if err != nil {
326 return fmt.Errorf("failed to upsert empty CRL")
327 }
328
Serge Bazanski52538842021-08-11 16:22:41 +0200329 c.PrivateKey = priv
330 c.PublicKey = pub
331 return nil
332}
333
334// Ensure returns an x509 DER-encoded (but not PEM-encoded) certificate for a
335// given Certificate.
336//
337// If the Certificate is ephemeral, each call to Ensure will cause a new
338// certificate to be generated. Otherwise, it will be retrieved from etcd, or
339// generated and stored there if needed.
340func (c *Certificate) Ensure(ctx context.Context, kv clientv3.KV) (cert []byte, err error) {
341 return c.ensure(ctx, kv)
342}
343
344func (c *Certificate) PrivateKeyX509() ([]byte, error) {
345 if c.PrivateKey == nil {
346 return nil, fmt.Errorf("certificate has no private key")
347 }
348 key, err := x509.MarshalPKCS8PrivateKey(c.PrivateKey)
349 if err != nil {
350 return nil, fmt.Errorf("could not marshal private key (data corruption?): %w", err)
351 }
352 return key, nil
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100353}
354
355// FilesystemCertificate is a fileargs.FileArgs wrapper which will contain PEM
356// encoded certificate material when Mounted. This construct is useful when
357// dealing with services that want to access etcd-backed certificates as files
358// available locally.
359// Paths to the available files are considered opaque and should not be leaked
360// outside of the struct. Further restrictions on access to these files might
361// be imposed in the future.
362type FilesystemCertificate struct {
363 *fileargs.FileArgs
364 // CACertPath is the full path at which the CA certificate is available.
365 // Read only.
366 CACertPath string
367 // CertPath is the full path at which the certificate is available. Read
368 // only.
369 CertPath string
Serge Bazanski52538842021-08-11 16:22:41 +0200370 // KeyPath is the full path at which the private key is available, or an empty
371 // string if the Certificate was created without a private key. Read only.
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100372 KeyPath string
373}
374
375// Mount returns a locally mounted FilesystemCertificate for this Certificate,
376// which allows services to access this Certificate via local filesystem
377// access.
378// The embeded fileargs.FileArgs can also be used to add additional file-backed
379// data under the same mount by calling ArgPath.
380// The returned FilesystemCertificate must be Closed in order to prevent a
381// system mount leak.
382func (c *Certificate) Mount(ctx context.Context, kv clientv3.KV) (*FilesystemCertificate, error) {
383 fa, err := fileargs.New()
384 if err != nil {
385 return nil, fmt.Errorf("when creating fileargs mount: %w", err)
386 }
387 fs := &FilesystemCertificate{FileArgs: fa}
388
Serge Bazanski52538842021-08-11 16:22:41 +0200389 cert, err := c.Ensure(ctx, kv)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100390 if err != nil {
391 return nil, fmt.Errorf("when issuing certificate: %w", err)
392 }
393
394 cacert, err := c.Issuer.CACertificate(ctx, kv)
395 if err != nil {
396 return nil, fmt.Errorf("when getting issuer CA: %w", err)
397 }
398 // cacert will be null if this is a self-signed certificate.
399 if cacert == nil {
400 cacert = cert
401 }
402
403 fs.CACertPath = fs.ArgPath("ca.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cacert}))
404 fs.CertPath = fs.ArgPath("tls.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}))
Serge Bazanski52538842021-08-11 16:22:41 +0200405 if c.PrivateKey != nil {
406 key, err := c.PrivateKeyX509()
407 if err != nil {
408 return nil, err
409 }
410 fs.KeyPath = fs.ArgPath("tls.key", pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key}))
411 }
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100412
413 return fs, nil
414}