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