blob: e99900305f26dc1f0c6b616a58a83ea62ee788e7 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Serge Bazanski9411f7c2021-03-10 13:12:53 +01002// SPDX-License-Identifier: Apache-2.0
Serge Bazanski9411f7c2021-03-10 13:12:53 +01003
Serge Bazanski52538842021-08-11 16:22:41 +02004// package pki implements an x509 PKI (Public Key Infrastructure) system backed
5// on etcd.
Serge Bazanski9411f7c2021-03-10 13:12:53 +01006package pki
7
8import (
Serge Bazanski52538842021-08-11 16:22:41 +02009 "bytes"
Serge Bazanski9411f7c2021-03-10 13:12:53 +010010 "context"
11 "crypto/ed25519"
Serge Bazanski52538842021-08-11 16:22:41 +020012 "crypto/rand"
Serge Bazanski9411f7c2021-03-10 13:12:53 +010013 "crypto/x509"
14 "crypto/x509/pkix"
15 "encoding/pem"
16 "fmt"
17 "net"
18
Lorenz Brund13c1c62022-03-30 19:58:58 +020019 clientv3 "go.etcd.io/etcd/client/v3"
Serge Bazanski9411f7c2021-03-10 13:12:53 +010020
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020021 "source.monogon.dev/osbase/fileargs"
Serge Bazanski9411f7c2021-03-10 13:12:53 +010022)
23
24// Namespace represents some path in etcd where certificate/CA data will be
25// stored. Creating a namespace via Namespaced then permits the consumer of
26// this library to start creating certificates within this namespace.
27type Namespace struct {
28 prefix string
29}
30
Serge Bazanski216fe7b2021-05-21 18:36:16 +020031// Namespaced creates a namespace for storing certificate data in etcd at a
32// given 'path' prefix.
Serge Bazanski9411f7c2021-03-10 13:12:53 +010033func Namespaced(prefix string) Namespace {
34 return Namespace{
35 prefix: prefix,
36 }
37}
38
Serge Bazanski52538842021-08-11 16:22:41 +020039type CertificateMode int
40
41const (
42 // CertificateManaged is a certificate whose key material is fully managed by
43 // the Certificate code. When set, PublicKey and PrivateKey must not be set by
44 // the user, and instead will be populated by the Ensure call. Name must be set,
45 // and will be used to store this Certificate and its keys within etcd. After
46 // the initial generation during Ensure, other Certificates with the same Name
47 // will be retrieved (including key material) from etcd.
48 CertificateManaged CertificateMode = iota
49
50 // CertificateExternal is a certificate whose key material is not managed by
51 // Certificate or stored in etcd, but the X509 certificate itself is. PublicKey
52 // must be set while PrivateKey must not be set. Name must be set, and will be
53 // used to store the emitted X509 certificate in etcd on Ensure. After the
54 // initial generation during Ensure, other Certificates with the same Name will
55 // be retrieved (without key material) from etcd.
56 CertificateExternal
57
58 // CertificateEphemeral is a certificate whose data (X509 certificate and
59 // possibly key material) is generated on demand each time Ensure is called.
60 // Nothing is stored in etcd or loaded from etcd. PrivateKey or PublicKey can be
61 // set, if both are nil then a new keypair will be generated. Name is ignored.
62 CertificateEphemeral
63)
64
Serge Bazanski9411f7c2021-03-10 13:12:53 +010065// Certificate is the promise of a Certificate being available to the caller.
66// In this case, Certificate refers to a pair of x509 certificate and
67// corresponding private key. Certificates can be stored in etcd, and their
68// issuers might also be store on etcd. As such, this type's methods contain
Serge Bazanski52538842021-08-11 16:22:41 +020069// references to an etcd KV client.
Serge Bazanski9411f7c2021-03-10 13:12:53 +010070type Certificate struct {
Serge Bazanski52538842021-08-11 16:22:41 +020071 Namespace *Namespace
Serge Bazanski9411f7c2021-03-10 13:12:53 +010072
Serge Bazanski52538842021-08-11 16:22:41 +020073 // Issuer is the Issuer that will generate this certificate if one doesn't
74 // yet exist or etcd, or the requested certificate is ephemeral (not to be
Serge Bazanski9411f7c2021-03-10 13:12:53 +010075 // stored on etcd).
76 Issuer Issuer
Serge Bazanski52538842021-08-11 16:22:41 +020077 // Name is a unique key for storing the certificate in etcd (if the requested
78 // certificate is not ephemeral).
79 Name string
80 // Template is an x509 certificate definition that will be used to generate
Serge Bazanski9411f7c2021-03-10 13:12:53 +010081 // the certificate when issuing it.
Serge Bazanski52538842021-08-11 16:22:41 +020082 Template x509.Certificate
83
84 // Mode in which this Certificate will operate. This influences the behaviour of
85 // the Ensure call.
86 Mode CertificateMode
87
88 // PrivateKey is the private key for this Certificate. It should never be set by
89 // the user, and instead will be populated by the Ensure call for Managed
90 // Certificates.
91 PrivateKey ed25519.PrivateKey
92
93 // PublicKey is the public key for this Certificate. It should only be set by
94 // the user for External or Ephemeral certificates, and will be populated by the
95 // next Ensure call if missing.
96 PublicKey ed25519.PublicKey
Serge Bazanski9411f7c2021-03-10 13:12:53 +010097}
98
99func (n *Namespace) etcdPath(f string, args ...interface{}) string {
100 return n.prefix + fmt.Sprintf(f, args...)
101}
102
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100103// Client makes a Kubernetes PKI-compatible client certificate template.
104// Directly derived from Kubernetes PKI requirements documented at
Tim Windelschmidt99e15112025-02-05 17:38:16 +0100105//
106// https://kubernetes.io/docs/setup/best-practices/certificates/#configure-certificates-manually
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100107func Client(identity string, groups []string) x509.Certificate {
108 return x509.Certificate{
109 Subject: pkix.Name{
110 CommonName: identity,
111 Organization: groups,
112 },
113 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
114 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
115 }
116}
117
118// Server makes a Kubernetes PKI-compatible server certificate template.
119func Server(dnsNames []string, ips []net.IP) x509.Certificate {
120 return x509.Certificate{
121 Subject: pkix.Name{},
122 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
123 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
124 DNSNames: dnsNames,
125 IPAddresses: ips,
126 }
127}
128
129// CA makes a Certificate that can sign other certificates.
130func CA(cn string) x509.Certificate {
131 return x509.Certificate{
132 Subject: pkix.Name{
133 CommonName: cn,
134 },
135 IsCA: true,
136 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
137 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageOCSPSigning},
138 }
139}
140
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100141// ensure returns a DER-encoded x509 certificate and internally encoded bare
Serge Bazanski52538842021-08-11 16:22:41 +0200142// ed25519 key for a given Certificate, in memory (if ephemeral), loading it
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100143// from etcd, or creating and saving it on etcd if needed.
144// This function is safe to call in parallel from multiple etcd clients
145// (including across machines), but it will error in case a concurrent
146// certificate generation happens. These errors are, however, safe to retry -
147// as long as all the certificate creators (ie., Metropolis nodes) run the same
148// version of this code.
Serge Bazanski52538842021-08-11 16:22:41 +0200149func (c *Certificate) ensure(ctx context.Context, kv clientv3.KV) (cert []byte, err error) {
150 // Ensure key is available.
151 if err := c.ensureKey(ctx, kv); err != nil {
152 return nil, err
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100153 }
154
Serge Bazanski52538842021-08-11 16:22:41 +0200155 switch c.Mode {
156 case CertificateEphemeral:
157 // TODO(q3k): cache internally?
158 cert, err = c.Issuer.Issue(ctx, c, kv)
159 if err != nil {
160 return nil, fmt.Errorf("failed to issue: %w", err)
161 }
162 return cert, nil
163 case CertificateManaged, CertificateExternal:
164 default:
165 return nil, fmt.Errorf("invalid certificate mode %v", c.Mode)
166 }
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100167
Serge Bazanskia41caac2021-08-12 17:00:55 +0200168 if c.Name == "" {
169 if c.Mode == CertificateExternal {
170 return nil, fmt.Errorf("external certificate must have name set")
171 } else {
172 return nil, fmt.Errorf("managed certificate must have name set")
173 }
174 }
175
Serge Bazanski999e1db2021-11-30 20:37:38 +0100176 certPath := c.Namespace.etcdPath("issued/%s-cert.der", c.Name)
Serge Bazanski52538842021-08-11 16:22:41 +0200177
178 // Try loading certificate from etcd.
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100179 certRes, err := kv.Get(ctx, certPath)
180 if err != nil {
Serge Bazanski52538842021-08-11 16:22:41 +0200181 return nil, fmt.Errorf("failed to get certificate from etcd: %w", err)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100182 }
Serge Bazanski52538842021-08-11 16:22:41 +0200183
184 if len(certRes.Kvs) == 1 {
185 certBytes := certRes.Kvs[0].Value
186 cert, err := x509.ParseCertificate(certBytes)
187 if err != nil {
188 return nil, fmt.Errorf("failed to parse certificate retrieved from etcd: %w", err)
189 }
190 pk, ok := cert.PublicKey.(ed25519.PublicKey)
191 if !ok {
192 return nil, fmt.Errorf("unexpected non-ed25519 certificate found in etcd")
193 }
194 if !bytes.Equal(pk, c.PublicKey) {
195 return nil, fmt.Errorf("certificate stored in etcd emitted for different public key")
196 }
197 // TODO(q3k): ensure issuer and template haven't changed
198 return certBytes, nil
199 }
200
201 // No certificate found - issue one and save to etcd.
202 cert, err = c.Issuer.Issue(ctx, c, kv)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100203 if err != nil {
Serge Bazanski52538842021-08-11 16:22:41 +0200204 return nil, fmt.Errorf("failed to issue: %w", err)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100205 }
206
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100207 res, err := kv.Txn(ctx).
208 If(
209 clientv3.Compare(clientv3.CreateRevision(certPath), "=", 0),
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100210 ).
211 Then(
212 clientv3.OpPut(certPath, string(cert)),
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100213 ).Commit()
214 if err != nil {
215 err = fmt.Errorf("failed to write newly issued certificate: %w", err)
216 } else if !res.Succeeded {
217 err = fmt.Errorf("certificate issuance transaction failed: concurrent write")
218 }
219
220 return
221}
222
Serge Bazanski52538842021-08-11 16:22:41 +0200223// ensureKey retrieves or creates PublicKey as needed based on the Certificate
224// Mode. For Managed Certificates and Ephemeral Certificates with no PrivateKey
225// it will also populate PrivateKay.
226func (c *Certificate) ensureKey(ctx context.Context, kv clientv3.KV) error {
227 // If we have a public key then we're all set.
228 if c.PublicKey != nil {
229 return nil
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100230 }
Serge Bazanski52538842021-08-11 16:22:41 +0200231
232 // For ephemeral keys, we just generate them.
233 // For external keys, we can't do anything - not having the keys set means
234 // a programming error.
235
236 switch c.Mode {
237 case CertificateEphemeral:
238 pub, priv, err := ed25519.GenerateKey(rand.Reader)
239 if err != nil {
240 return fmt.Errorf("when generating ephemeral key: %w", err)
241 }
242 c.PublicKey = pub
243 c.PrivateKey = priv
244 return nil
245 case CertificateExternal:
246 if c.PrivateKey != nil {
247 // We prohibit having PrivateKey set in External Certificates to simplify the
248 // different logic paths this library implements. Being able to assume External
249 // == PublicKey only makes things easier elsewhere.
250 return fmt.Errorf("external certificate must not have PrivateKey set")
251 }
252 return fmt.Errorf("external certificate must have PublicKey set")
253 case CertificateManaged:
254 default:
255 return fmt.Errorf("invalid certificate mode %v", c.Mode)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100256 }
Serge Bazanski52538842021-08-11 16:22:41 +0200257
258 // For managed keys, synchronize with etcd.
259 if c.Name == "" {
260 return fmt.Errorf("managed certificate must have Name set")
261 }
262
263 // First, try loading.
Serge Bazanski999e1db2021-11-30 20:37:38 +0100264 privPath := c.Namespace.etcdPath("keys/%s-privkey.bin", c.Name)
Serge Bazanski52538842021-08-11 16:22:41 +0200265 privRes, err := kv.Get(ctx, privPath)
266 if err != nil {
267 return fmt.Errorf("failed to get private key from etcd: %w", err)
268 }
269 if len(privRes.Kvs) == 1 {
270 privBytes := privRes.Kvs[0].Value
271 if len(privBytes) != ed25519.PrivateKeySize {
272 return fmt.Errorf("stored private key has invalid size")
273 }
274 c.PrivateKey = privBytes
275 c.PublicKey = c.PrivateKey.Public().(ed25519.PublicKey)
276 return nil
277 }
278
279 // No key in etcd? Generate and save.
280 pub, priv, err := ed25519.GenerateKey(rand.Reader)
281 if err != nil {
282 return fmt.Errorf("while generating keypair: %w", err)
283 }
284
285 res, err := kv.Txn(ctx).
286 If(
287 clientv3.Compare(clientv3.CreateRevision(privPath), "=", 0),
288 ).
289 Then(
290 clientv3.OpPut(privPath, string(priv)),
291 ).Commit()
292 if err != nil {
293 return fmt.Errorf("failed to write newly generated keypair: %w", err)
294 } else if !res.Succeeded {
295 return fmt.Errorf("key generation transaction failed: concurrent write")
296 }
297
Serge Bazanski999e1db2021-11-30 20:37:38 +0100298 crlPath := c.crlPath()
299 emptyCRL, err := c.makeCRL(ctx, kv, nil)
300 if err != nil {
301 return fmt.Errorf("failed to generate empty CRL: %w", err)
302 }
303
304 // Also attempt to emit an empty CRL if one doesn't exist yet.
305 _, err = kv.Txn(ctx).
306 If(
307 clientv3.Compare(clientv3.CreateRevision(crlPath), "=", 0),
308 ).
309 Then(
310 clientv3.OpPut(crlPath, string(emptyCRL)),
311 ).Commit()
312 if err != nil {
313 return fmt.Errorf("failed to upsert empty CRL")
314 }
315
Serge Bazanski52538842021-08-11 16:22:41 +0200316 c.PrivateKey = priv
317 c.PublicKey = pub
318 return nil
319}
320
321// Ensure returns an x509 DER-encoded (but not PEM-encoded) certificate for a
322// given Certificate.
323//
324// If the Certificate is ephemeral, each call to Ensure will cause a new
325// certificate to be generated. Otherwise, it will be retrieved from etcd, or
326// generated and stored there if needed.
327func (c *Certificate) Ensure(ctx context.Context, kv clientv3.KV) (cert []byte, err error) {
328 return c.ensure(ctx, kv)
329}
330
331func (c *Certificate) PrivateKeyX509() ([]byte, error) {
332 if c.PrivateKey == nil {
333 return nil, fmt.Errorf("certificate has no private key")
334 }
335 key, err := x509.MarshalPKCS8PrivateKey(c.PrivateKey)
336 if err != nil {
337 return nil, fmt.Errorf("could not marshal private key (data corruption?): %w", err)
338 }
339 return key, nil
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100340}
341
342// FilesystemCertificate is a fileargs.FileArgs wrapper which will contain PEM
343// encoded certificate material when Mounted. This construct is useful when
344// dealing with services that want to access etcd-backed certificates as files
345// available locally.
346// Paths to the available files are considered opaque and should not be leaked
347// outside of the struct. Further restrictions on access to these files might
348// be imposed in the future.
349type FilesystemCertificate struct {
350 *fileargs.FileArgs
351 // CACertPath is the full path at which the CA certificate is available.
352 // Read only.
353 CACertPath string
354 // CertPath is the full path at which the certificate is available. Read
355 // only.
356 CertPath string
Serge Bazanski52538842021-08-11 16:22:41 +0200357 // KeyPath is the full path at which the private key is available, or an empty
358 // string if the Certificate was created without a private key. Read only.
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100359 KeyPath string
360}
361
362// Mount returns a locally mounted FilesystemCertificate for this Certificate,
363// which allows services to access this Certificate via local filesystem
364// access.
365// The embeded fileargs.FileArgs can also be used to add additional file-backed
366// data under the same mount by calling ArgPath.
367// The returned FilesystemCertificate must be Closed in order to prevent a
368// system mount leak.
369func (c *Certificate) Mount(ctx context.Context, kv clientv3.KV) (*FilesystemCertificate, error) {
370 fa, err := fileargs.New()
371 if err != nil {
372 return nil, fmt.Errorf("when creating fileargs mount: %w", err)
373 }
374 fs := &FilesystemCertificate{FileArgs: fa}
375
Serge Bazanski52538842021-08-11 16:22:41 +0200376 cert, err := c.Ensure(ctx, kv)
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100377 if err != nil {
378 return nil, fmt.Errorf("when issuing certificate: %w", err)
379 }
380
381 cacert, err := c.Issuer.CACertificate(ctx, kv)
382 if err != nil {
383 return nil, fmt.Errorf("when getting issuer CA: %w", err)
384 }
385 // cacert will be null if this is a self-signed certificate.
386 if cacert == nil {
387 cacert = cert
388 }
389
390 fs.CACertPath = fs.ArgPath("ca.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cacert}))
391 fs.CertPath = fs.ArgPath("tls.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}))
Serge Bazanski52538842021-08-11 16:22:41 +0200392 if c.PrivateKey != nil {
393 key, err := c.PrivateKeyX509()
394 if err != nil {
395 return nil, err
396 }
397 fs.KeyPath = fs.ArgPath("tls.key", pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key}))
398 }
Serge Bazanski9411f7c2021-03-10 13:12:53 +0100399
400 return fs, nil
401}