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