blob: e0dea0d521f397b51f6f9ceccb22bb449f1cc9fa [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.
109func (c *Certificate) ensure(ctx context.Context, kv clientv3.KV) (cert, key []byte, err error) {
110 if c.name == "" {
111 // Volatile certificate - generate.
112 // TODO(q3k): cache internally?
113 cert, key, err = c.issuer.Issue(ctx, c.template, kv)
114 if err != nil {
115 err = fmt.Errorf("failed to issue: %w", err)
116 return
117 }
118 return
119 }
120
121 certPath, keyPath := c.etcdPaths()
122
123 // Try loading certificate and key from etcd.
124 certRes, err := kv.Get(ctx, certPath)
125 if err != nil {
126 err = fmt.Errorf("failed to get certificate from etcd: %w", err)
127 return
128 }
129 keyRes, err := kv.Get(ctx, keyPath)
130 if err != nil {
131 err = fmt.Errorf("failed to get key from etcd: %w", err)
132 return
133 }
134
135 if len(certRes.Kvs) == 1 && len(keyRes.Kvs) == 1 {
136 // Certificate and key exists in etcd, return that.
137 cert = certRes.Kvs[0].Value
138 key = keyRes.Kvs[0].Value
139
140 err = nil
141 // TODO(q3k): check for expiration
142 return
143 }
144
145 // No certificate found - issue one.
146 cert, key, err = c.issuer.Issue(ctx, c.template, kv)
147 if err != nil {
148 err = fmt.Errorf("failed to issue: %w", err)
149 return
150 }
151
152 // Save to etcd in transaction. This ensures that no partial writes happen.
153 _, err = kv.Txn(ctx).
154 Then(
155 clientv3.OpPut(certPath, string(cert)),
156 clientv3.OpPut(keyPath, string(key)),
157 ).Commit()
158 if err != nil {
159 err = fmt.Errorf("failed to write newly issued certificate: %w", err)
160 }
161
162 return
163}
164
165// Ensure returns an x509 DER-encoded (but not PEM-encoded) certificate and key for a given Certificate.
166// If the certificate is volatile, each call to Ensure will cause a new certificate to be generated.
167// Otherwise, it will be retrieved from etcd, or generated and stored there if needed.
168func (c *Certificate) Ensure(ctx context.Context, kv clientv3.KV) (cert, key []byte, err error) {
169 cert, key, err = c.ensure(ctx, kv)
170 if err != nil {
171 return nil, nil, err
172 }
173 key, err = x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(key))
174 if err != nil {
175 err = fmt.Errorf("could not marshal private key (data corruption?): %w", err)
176 return
177 }
178 return cert, key, err
179}