ALL

Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.

tunnel/certreload_test.go

SHA-256: e0256e8bd580e5cf9308902f738dfe46914f1d9c15a87bb4707cb892c67f67c5
297 líneas · 11102 bytes · Ver en GitHub

1package main
2
3import (
4 "bufio"
5 "context"
6 "crypto/ecdsa"
7 "crypto/elliptic"
8 "crypto/rand"
9 "crypto/tls"
10 "crypto/x509"
11 "crypto/x509/pkix"
12 "encoding/pem"
13 "log/slog"
14 "math/big"
15 "net"
16 "os"
17 "path/filepath"
18 "strings"
19 "testing"
20 "time"
21)
22
23// The node's own public-CA certificate is hot reloaded: a renewal goes into service without a restart and without a dropped connection, and a renewal
24// that is not good never replaces the one that works.
25
26type certFiles struct {
27 dir, cert, key string
28}
29
30func newCertFiles(t *testing.T) *certFiles {
31 dir := t.TempDir()
32 return &certFiles{dir: dir, cert: filepath.Join(dir, "fullchain.pem"), key: filepath.Join(dir, "privkey.pem")}
33}
34
35// write puts a certificate for names, valid between notBefore and notAfter, on disk (the key too, unless keyFrom is set: then that pair's key is
36// written, which is a renewal whose key does not match). It returns the serial.
37func (c *certFiles) write(t *testing.T, serial int64, notBefore, notAfter time.Time, names ...string) {
38 t.Helper()
39 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
40 if err != nil {
41 t.Fatal(err)
42 }
43 tmpl := &x509.Certificate{
44 SerialNumber: big.NewInt(serial), Subject: pkix.Name{CommonName: names[0]}, NotBefore: notBefore, NotAfter: notAfter,
45 KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: names,
46 }
47 der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
48 if err != nil {
49 t.Fatal(err)
50 }
51 keyDER, _ := x509.MarshalECPrivateKey(key)
52 c.put(t, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}))
53}
54
55// put writes both files the way a renewal tool does: each replaced atomically, the key after the certificate (so there is a moment when they do not match).
56func (c *certFiles) put(t *testing.T, certPEM, keyPEM []byte) {
57 t.Helper()
58 for path, data := range map[string][]byte{c.cert: certPEM, c.key: keyPEM} {
59 tmp := path + ".new"
60 if err := os.WriteFile(tmp, data, 0o600); err != nil {
61 t.Fatal(err)
62 }
63 if err := os.Rename(tmp, path); err != nil {
64 t.Fatal(err)
65 }
66 }
67}
68
69func (c *certFiles) fresh(t *testing.T, serial int64, names ...string) {
70 t.Helper()
71 c.write(t, serial, time.Now().Add(-time.Hour), time.Now().Add(60*24*time.Hour), names...)
72}
73
74// servedSerial connects to the node's own name and reports the serial of the certificate it presents.
75func servedSerial(t *testing.T, addr string) int64 {
76 t.Helper()
77 conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp", addr, &tls.Config{
78 ServerName: testNodeHost, InsecureSkipVerify: true, NextProtos: []string{policyData.Protocol.ALPN}, MinVersion: tls.VersionTLS13,
79 })
80 if err != nil {
81 t.Fatal(err)
82 }
83 defer conn.Close()
84 return conn.ConnectionState().PeerCertificates[0].SerialNumber.Int64()
85}
86
87func certNode(t *testing.T, r *certReloader) *testEnv {
88 return newTestEnv(t, func(c *Config) { c.CertSource = r.get })
89}
90
91func TestCertReload_ARenewalIsInServiceForTheNextHandshakeAndAnOpenConnectionIsUntouched(t *testing.T) {
92 files := newCertFiles(t)
93 files.fresh(t, 1, testNodeHost)
94 r, err := newCertReloader(files.cert, files.key, testNodeHost, clock{}, nil)
95 if err != nil {
96 t.Fatal(err)
97 }
98 r.lookGap = 0
99 e := certNode(t, r)
100 if got := servedSerial(t, e.httpsAddr); got != 1 {
101 t.Fatalf("served %d", got)
102 }
103 // a device is connected (its TLS session was made with the first certificate)
104 d := e.newDevice()
105 conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp", e.httpsAddr, &tls.Config{
106 ServerName: testNodeHost, InsecureSkipVerify: true, NextProtos: []string{policyData.Protocol.ALPN}, MinVersion: tls.VersionTLS13,
107 })
108 if err != nil {
109 t.Fatal(err)
110 }
111 defer conn.Close()
112 dc := &devConn{t: t, e: e, d: d, c: conn, br: bufio.NewReaderSize(conn, 1<<17)}
113 dc.session = e.session(d, nil)
114 dc.send("HELLO 2 " + dc.session)
115 m, perr := parseLine(dc.recv(), dirN2D)
116 if perr != nil || m.str("verb") != "CHALLENGE" {
117 t.Fatal("no challenge")
118 }
119 dc.send("PROOF " + e.proof(d, dc.session, m.str("nonce"), nil))
120 if line := dc.recv(); !strings.HasPrefix(line, "OK ") {
121 t.Fatalf("%q", line)
122 }
123
124 // the renewal lands on disk, the node is not restarted
125 time.Sleep(20 * time.Millisecond)
126 files.fresh(t, 2, testNodeHost)
127 if got := servedSerial(t, e.httpsAddr); got != 2 {
128 t.Fatalf("after the renewal the node served %d", got)
129 }
130 // the connection that was up goes on
131 dc.send("PING")
132 if line := dc.recv(); line != "PONG" {
133 t.Fatalf("a connection made with the old certificate: %q", line)
134 }
135 if !e.srv.Online(d.host) {
136 t.Fatal("the device was dropped by the renewal")
137 }
138 if want := r.NotAfter(); want.Before(time.Now().Add(50 * 24 * time.Hour)) {
139 t.Fatalf("not_after %s", want)
140 }
141}
142
143func TestCertReload_ARenewalThatIsNotGoodNeverReplacesTheOneThatWorks(t *testing.T) {
144 files := newCertFiles(t)
145 files.fresh(t, 1, testNodeHost)
146 var logs syncBuf
147 r, err := newCertReloader(files.cert, files.key, testNodeHost, clock{}, slog.New(slog.NewJSONHandler(&logs, nil)))
148 if err != nil {
149 t.Fatal(err)
150 }
151 r.lookGap = 0
152 e := certNode(t, r)
153 good := func() {
154 t.Helper()
155 if got := servedSerial(t, e.httpsAddr); got != 1 {
156 t.Fatalf("the node serves %d, the certificate that works is 1", got)
157 }
158 }
159 now := time.Now()
160 for name, write := range map[string]func(){
161 "another name": func() { files.write(t, 10, now.Add(-time.Hour), now.Add(48*time.Hour), "someone-else.example.test") },
162 "expired": func() { files.write(t, 11, now.Add(-72*time.Hour), now.Add(-time.Hour), testNodeHost) },
163 "not yet valid": func() { files.write(t, 12, now.Add(48*time.Hour), now.Add(96*time.Hour), testNodeHost) },
164 "garbage": func() { files.put(t, []byte("garbage"), []byte("garbage")) },
165 "half written": func() { _ = os.WriteFile(files.cert, []byte("-----BEGIN CERTIFICATE-----\nAAAA"), 0o600) },
166 "a missing key": func() { _ = os.Remove(files.key) },
167 "a missing cert": func() { files.fresh(t, 13, testNodeHost); _ = os.Remove(files.cert) },
168 "a certificate and a key that are not a pair": func() {
169 other := newCertFiles(t)
170 other.fresh(t, 14, testNodeHost)
171 certPEM, _ := os.ReadFile(other.cert)
172 third := newCertFiles(t)
173 third.fresh(t, 15, testNodeHost)
174 keyPEM, _ := os.ReadFile(third.key)
175 files.put(t, certPEM, keyPEM)
176 },
177 } {
178 time.Sleep(15 * time.Millisecond)
179 write()
180 good()
181 if name != "a missing key" && name != "a missing cert" && name != "half written" && r.LastError() == "" {
182 t.Errorf("%s: the refusal is not recorded", name)
183 }
184 }
185 if !strings.Contains(logs.String(), "node_cert_reload_refused") {
186 t.Fatal("refusals are not logged")
187 }
188 // the files are in a bad state now; a good renewal still gets in
189 time.Sleep(20 * time.Millisecond)
190 files.fresh(t, 3, testNodeHost)
191 if got := servedSerial(t, e.httpsAddr); got != 3 {
192 t.Fatalf("a good renewal after the bad ones: served %d", got)
193 }
194 if r.LastError() != "" {
195 t.Fatalf("last error %q", r.LastError())
196 }
197}
198
199func TestCertReload_ARefusedRenewalIsNotParsedAgainAtEveryHandshake(t *testing.T) {
200 files := newCertFiles(t)
201 files.fresh(t, 1, testNodeHost)
202 var logs syncBuf
203 r, err := newCertReloader(files.cert, files.key, testNodeHost, clock{}, slog.New(slog.NewJSONHandler(&logs, nil)))
204 if err != nil {
205 t.Fatal(err)
206 }
207 r.lookGap = 0
208 time.Sleep(15 * time.Millisecond)
209 files.put(t, []byte("garbage"), []byte("garbage"))
210 for i := 0; i < 50; i++ {
211 if pair, err := r.get(nil); err != nil || pair.Leaf.SerialNumber.Int64() != 1 {
212 t.Fatalf("handshake %d: %v", i, err)
213 }
214 }
215 if n := logs.Count("node_cert_reload_refused"); n != 1 {
216 t.Fatalf("one bad renewal was parsed and refused %d times", n)
217 }
218}
219
220func TestCertReload_ReloadNowIsWhatSIGHUPDoes(t *testing.T) {
221 files := newCertFiles(t)
222 files.fresh(t, 1, testNodeHost)
223 r, err := newCertReloader(files.cert, files.key, testNodeHost, clock{}, nil)
224 if err != nil {
225 t.Fatal(err)
226 }
227 // a look gap: the handshake path has just looked and does not look again for a while, SIGHUP does not wait
228 _, _ = r.get(nil)
229 files.fresh(t, 2, testNodeHost)
230 if pair, _ := r.get(nil); pair.Leaf.SerialNumber.Int64() != 1 {
231 t.Fatal("looked inside the gap")
232 }
233 if err := r.Reload(); err != nil {
234 t.Fatal(err)
235 }
236 if pair, _ := r.get(nil); pair.Leaf.SerialNumber.Int64() != 2 {
237 t.Fatal("Reload did not put the new certificate in service")
238 }
239 files.write(t, 3, time.Now().Add(-time.Hour), time.Now().Add(-time.Minute), testNodeHost)
240 if err := r.Reload(); err == nil || r.LastError() == "" {
241 t.Fatal("an expired certificate was accepted by Reload")
242 }
243 if pair, _ := r.get(nil); pair.Leaf.SerialNumber.Int64() != 2 {
244 t.Fatal("a refused Reload replaced the certificate")
245 }
246}
247
248func TestCertReload_TheWatcherPicksUpARenewalWithoutAnyHandshakeAndWarnsBeforeTheEnd(t *testing.T) {
249 files := newCertFiles(t)
250 files.write(t, 1, time.Now().Add(-time.Hour), time.Now().Add(3*24*time.Hour), testNodeHost) // 3 days left: under the warning
251 var logs syncBuf
252 clk := newFakeClock()
253 r, err := newCertReloader(files.cert, files.key, testNodeHost, newClock(clk.Now, clk.AfterFunc), slog.New(slog.NewJSONHandler(&logs, nil)))
254 if err != nil {
255 t.Fatal(err)
256 }
257 ctx, cancel := context.WithCancel(context.Background())
258 done := make(chan struct{})
259 go func() { r.Run(ctx); close(done) }()
260 eventually(t, "the first look and its warning", 2*time.Second, func() bool { return logs.Count("node_cert_expires_soon") == 1 })
261 eventually(t, "the watcher sleeping", 2*time.Second, func() bool { return clk.pending() == 1 })
262
263 time.Sleep(20 * time.Millisecond)
264 files.fresh(t, 2, testNodeHost)
265 clk.Advance(certWatchEvery)
266 eventually(t, "the renewal in service", 2*time.Second, func() bool { return logs.Count("node_cert_reloaded") == 1 })
267 if pair, _ := r.get(nil); pair.Leaf.SerialNumber.Int64() != 2 {
268 t.Fatal("not in service")
269 }
270 eventually(t, "the watcher sleeping again", 2*time.Second, func() bool { return clk.pending() == 1 })
271 clk.Advance(certWatchEvery)
272 time.Sleep(50 * time.Millisecond)
273 if logs.Count("node_cert_expires_soon") != 1 {
274 t.Fatal("warned about a certificate with 60 days left")
275 }
276 cancel()
277 select {
278 case <-done:
279 case <-time.After(2 * time.Second):
280 t.Fatal("the watcher did not stop")
281 }
282}
283
284func TestCertReload_TheNodeDoesNotStartWithoutAUsableCertificate(t *testing.T) {
285 files := newCertFiles(t)
286 if _, err := newCertReloader(files.cert, files.key, testNodeHost, clock{}, nil); err == nil {
287 t.Fatal("started without files")
288 }
289 files.write(t, 1, time.Now().Add(-time.Hour), time.Now().Add(time.Hour), "other.example.test")
290 if _, err := newCertReloader(files.cert, files.key, testNodeHost, clock{}, nil); err == nil || !strings.Contains(err.Error(), testNodeHost) {
291 t.Fatalf("started with a certificate for another name: %v", err)
292 }
293 files.write(t, 2, time.Now().Add(-48*time.Hour), time.Now().Add(-time.Hour), testNodeHost)
294 if _, err := newCertReloader(files.cert, files.key, testNodeHost, clock{}, nil); err == nil {
295 t.Fatal("started with an expired certificate")
296 }
297}