blob: 26277764979c66bd262b40c590d17767a669d6dd [file] [log] [blame]
Serge Bazanski999e1db2021-11-30 20:37:38 +01001package pki
2
3import (
4 "context"
5 "crypto/rand"
6 "crypto/x509"
7 "crypto/x509/pkix"
8 "fmt"
9 "math/big"
10 "time"
11
12 "go.etcd.io/etcd/clientv3"
13
14 "source.monogon.dev/metropolis/node/core/consensus/client"
15 "source.monogon.dev/metropolis/pkg/event"
16 "source.monogon.dev/metropolis/pkg/event/etcd"
17)
18
19// crlPath returns the etcd path under which the marshaled X.509 Certificate
20// Revocation List is stored.
21//
22// TODO(q3k): use etcd keyspace API from
23func (c *Certificate) crlPath() string {
24 return c.Namespace.etcdPath("%s-crl.der", c.Name)
25}
26
27// Revoke performs a CRL-based revocation of a given certificate by this CA,
28// looking it up by DNS name. The revocation is immediately written to the
29// backing etcd store and will be available to consumers through the WatchCRL
30// API.
31//
32// An error is returned if the CRL could not be emitted (eg. due to an etcd
33// communication error, a conflicting CRL write) or if the given hostname
34// matches no emitted certificate.
35//
36// Only Managed and External certificates can be revoked.
37func (c Certificate) Revoke(ctx context.Context, kv clientv3.KV, hostname string) error {
38 crlPath := c.crlPath()
39 issuedCerts := c.Namespace.etcdPath("issued/")
40
41 res, err := kv.Txn(ctx).Then(
42 clientv3.OpGet(crlPath),
43 clientv3.OpGet(issuedCerts, clientv3.WithPrefix())).Commit()
44 if err != nil {
45 return fmt.Errorf("failed to retrieve certificates and CRL from etcd: %w", err)
46 }
47
48 // Parse certs, CRL and CRL revision from state.
49 var certs []*x509.Certificate
50 var crlRevision int64
51 var crl *pkix.CertificateList
52 for _, el := range res.Responses {
53 for _, kv := range el.GetResponseRange().GetKvs() {
54 if string(kv.Key) == crlPath {
55 crl, err = x509.ParseCRL(kv.Value)
56 if err != nil {
57 return fmt.Errorf("could not parse CRL from etcd: %w", err)
58 }
59 crlRevision = kv.CreateRevision
60 } else {
61 cert, err := x509.ParseCertificate(kv.Value)
62 if err != nil {
63 return fmt.Errorf("could not parse certificate %q from etcd: %w", string(kv.Key), err)
64 }
65 certs = append(certs, cert)
66 }
67 }
68 }
69 if crl == nil {
70 return fmt.Errorf("could not find CRL in etcd")
71 }
72 revoked := crl.TBSCertList.RevokedCertificates
73
74 // Find requested hostname in issued certificates.
75 var serial *big.Int
76 for _, cert := range certs {
77 for _, dnsName := range cert.DNSNames {
78 if dnsName == hostname {
79 serial = cert.SerialNumber
80 break
81 }
82 }
83 if serial != nil {
84 break
85 }
86 }
87 if serial == nil {
88 return fmt.Errorf("could not find requested hostname")
89 }
90
91 // Check if certificate has already been revoked.
92 for _, revokedCert := range revoked {
93 if revokedCert.SerialNumber.Cmp(serial) == 0 {
94 return nil // Already revoked
95 }
96 }
97
98 // Otherwise, revoke and save new CRL.
99 revoked = append(revoked, pkix.RevokedCertificate{
100 SerialNumber: serial,
101 RevocationTime: time.Now(),
102 })
103
104 crlRaw, err := c.makeCRL(ctx, kv, revoked)
105 if err != nil {
106 return fmt.Errorf("when generating new CRL for revocation: %w", err)
107 }
108
109 res, err = kv.Txn(ctx).If(
110 clientv3.Compare(clientv3.CreateRevision(crlPath), "=", crlRevision),
111 ).Then(
112 clientv3.OpPut(crlPath, string(crlRaw)),
113 ).Commit()
114 if err != nil {
115 return fmt.Errorf("when saving new CRL: %w", err)
116 }
117 if !res.Succeeded {
118 return fmt.Errorf("CRL save transaction failed, retry possible")
119 }
120
121 return nil
122}
123
124// makeCRL returns a valid CRL for a given list of certificates to be revoked.
125// The given etcd client is used to ensure this CA certificate exists in etcd,
126// but is not used to write any CRL to etcd.
127func (c *Certificate) makeCRL(ctx context.Context, kv clientv3.KV, revoked []pkix.RevokedCertificate) ([]byte, error) {
128 if c.Mode != CertificateManaged {
129 return nil, fmt.Errorf("only managed certificates can issue CRLs")
130 }
131 certBytes, err := c.ensure(ctx, kv)
132 if err != nil {
133 return nil, fmt.Errorf("when ensuring certificate: %w", err)
134 }
135 cert, err := x509.ParseCertificate(certBytes)
136 if err != nil {
137 return nil, fmt.Errorf("when parsing issuing certificate: %w", err)
138 }
139 crl, err := cert.CreateCRL(rand.Reader, c.PrivateKey, revoked, time.Now(), UnknownNotAfter)
140 if err != nil {
141 return nil, fmt.Errorf("failed to generate CRL: %w", err)
142 }
143 return crl, nil
144}
145
146// WatchCRL returns and Event Value compatible CRLWatcher which can be used to
147// retrieve and watch for the newest CRL available from this CA certificate.
148func (c *Certificate) WatchCRL(cl client.Namespaced) CRLWatcher {
149 value := etcd.NewValue(cl, c.crlPath(), func(_, data []byte) (interface{}, error) {
150 crl, err := x509.ParseCRL(data)
151 if err != nil {
152 return nil, fmt.Errorf("could not parse CRL from etcd: %w", err)
153 }
154 return &CRL{
155 Raw: data,
156 List: crl,
157 }, nil
158 })
159 return CRLWatcher{value.Watch()}
160}
161
162// CRLWatcher is a Event Value compatible Watcher which will be updated any time
163// a given CA certificate's CRL gets updated.
164type CRLWatcher struct {
165 event.Watcher
166}
167
168type CRL struct {
169 Raw []byte
170 List *pkix.CertificateList
171}
172
173// Retrieve the newest available CRL from etcd, blocking until one is available
174// or updated.
175//
176// The first call will block until a CRL is available, which happens the first
177// time a given CA certificate is stored in etcd (eg. through an Ensure call).
178func (c *CRLWatcher) Get(ctx context.Context, opts ...event.GetOption) (*CRL, error) {
179 v, err := c.Watcher.Get(ctx, opts...)
180 if err != nil {
181 return nil, err
182 }
183 return v.(*CRL), nil
184}