1package dane
2
3import (
4 "context"
5 "crypto/ecdsa"
6 "crypto/elliptic"
7 cryptorand "crypto/rand"
8 "crypto/sha256"
9 "crypto/sha512"
10 "crypto/tls"
11 "crypto/x509"
12 "crypto/x509/pkix"
13 "errors"
14 "fmt"
15 "log/slog"
16 "math/big"
17 "net"
18 "reflect"
19 "strconv"
20 "sync/atomic"
21 "testing"
22 "time"
23
24 "github.com/mjl-/adns"
25
26 "github.com/mjl-/mox/dns"
27 "github.com/mjl-/mox/mlog"
28)
29
30func tcheckf(t *testing.T, err error, format string, args ...any) {
31 t.Helper()
32 if err != nil {
33 t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
34 }
35}
36
37// Test dialing and DANE TLS verification.
38func TestDial(t *testing.T) {
39 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
40 log := mlog.New("dane", nil)
41
42 // Create fake CA/trusted-anchor certificate.
43 taTempl := x509.Certificate{
44 SerialNumber: big.NewInt(1), // Required field.
45 Subject: pkix.Name{CommonName: "fake ca"},
46 Issuer: pkix.Name{CommonName: "fake ca"},
47 NotBefore: time.Now().Add(-1 * time.Hour),
48 NotAfter: time.Now().Add(1 * time.Hour),
49 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
50 ExtKeyUsage: []x509.ExtKeyUsage{
51 x509.ExtKeyUsageServerAuth,
52 x509.ExtKeyUsageClientAuth,
53 },
54 BasicConstraintsValid: true,
55 IsCA: true,
56 MaxPathLen: 1,
57 }
58 taPriv, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
59 tcheckf(t, err, "generating trusted-anchor ca private key")
60 taCertBuf, err := x509.CreateCertificate(cryptorand.Reader, &taTempl, &taTempl, taPriv.Public(), taPriv)
61 tcheckf(t, err, "create trusted-anchor ca certificate")
62 taCert, err := x509.ParseCertificate(taCertBuf)
63 tcheckf(t, err, "parsing generated trusted-anchor ca certificate")
64
65 tacertsha256 := sha256.Sum256(taCert.Raw)
66 taCertSHA256 := tacertsha256[:]
67
68 // Generate leaf private key & 2 certs, one expired and one valid, both signed by
69 // trusted-anchor cert.
70 leafPriv, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
71 tcheckf(t, err, "generating leaf private key")
72
73 makeLeaf := func(expired bool) (tls.Certificate, []byte, []byte) {
74 now := time.Now()
75 if expired {
76 now = now.Add(-2 * time.Hour)
77 }
78 leafTempl := x509.Certificate{
79 SerialNumber: big.NewInt(1), // Required field.
80 Issuer: taTempl.Subject,
81 NotBefore: now.Add(-1 * time.Hour),
82 NotAfter: now.Add(1 * time.Hour),
83 DNSNames: []string{"localhost"},
84 }
85 leafCertBuf, err := x509.CreateCertificate(cryptorand.Reader, &leafTempl, taCert, leafPriv.Public(), taPriv)
86 tcheckf(t, err, "create trusted-anchor ca certificate")
87 leafCert, err := x509.ParseCertificate(leafCertBuf)
88 tcheckf(t, err, "parsing generated trusted-anchor ca certificate")
89
90 leafSPKISHA256 := sha256.Sum256(leafCert.RawSubjectPublicKeyInfo)
91 leafSPKISHA512 := sha512.Sum512(leafCert.RawSubjectPublicKeyInfo)
92
93 tlsLeafCert := tls.Certificate{
94 Certificate: [][]byte{leafCertBuf, taCertBuf},
95 PrivateKey: leafPriv, // .(crypto.PrivateKey),
96 Leaf: leafCert,
97 }
98 return tlsLeafCert, leafSPKISHA256[:], leafSPKISHA512[:]
99 }
100 tlsLeafCert, leafSPKISHA256, leafSPKISHA512 := makeLeaf(false)
101 tlsLeafCertExpired, _, _ := makeLeaf(true)
102
103 // Set up loopback tls server.
104 listenConn, err := net.Listen("tcp", "127.0.0.1:0")
105 tcheckf(t, err, "listen for test server")
106 addr := listenConn.Addr().String()
107 _, portstr, err := net.SplitHostPort(addr)
108 tcheckf(t, err, "get localhost port")
109 uport, err := strconv.ParseUint(portstr, 10, 16)
110 tcheckf(t, err, "parse localhost port")
111 port := int(uport)
112
113 defer listenConn.Close()
114
115 // Config for server, replaced during tests.
116 var tlsConfig atomic.Pointer[tls.Config]
117 tlsConfig.Store(&tls.Config{
118 Certificates: []tls.Certificate{tlsLeafCert},
119 })
120
121 // Loop handling incoming TLS connections.
122 go func() {
123 for {
124 conn, err := listenConn.Accept()
125 if err != nil {
126 return
127 }
128
129 tlsConn := tls.Server(conn, tlsConfig.Load())
130 tlsConn.Handshake()
131 tlsConn.Close()
132 }
133 }()
134
135 dialHost := "localhost"
136 var allowedUsages []adns.TLSAUsage
137
138 pkixRoots := x509.NewCertPool()
139
140 // Helper function for dialing with DANE.
141 test := func(resolver dns.Resolver, expRecord adns.TLSA, expErr any) {
142 t.Helper()
143
144 conn, record, err := Dial(context.Background(), log.Logger, resolver, "tcp", net.JoinHostPort(dialHost, portstr), allowedUsages, pkixRoots)
145 if err == nil {
146 conn.Close()
147 }
148 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr.(error)) && !errors.As(err, expErr) {
149 t.Fatalf("got err %v (%#v), expected %#v", err, err, expErr)
150 }
151 if !reflect.DeepEqual(record, expRecord) {
152 t.Fatalf("got verified record %v, expected %v", record, expRecord)
153 }
154 }
155
156 tlsaName := fmt.Sprintf("_%d._tcp.localhost.", port)
157
158 // Make all kinds of records, some invalid or non-matching.
159 var zeroRecord adns.TLSA
160 recordDANEEESPKISHA256 := adns.TLSA{
161 Usage: adns.TLSAUsageDANEEE,
162 Selector: adns.TLSASelectorSPKI,
163 MatchType: adns.TLSAMatchTypeSHA256,
164 CertAssoc: leafSPKISHA256,
165 }
166 recordDANEEESPKISHA512 := adns.TLSA{
167 Usage: adns.TLSAUsageDANEEE,
168 Selector: adns.TLSASelectorSPKI,
169 MatchType: adns.TLSAMatchTypeSHA512,
170 CertAssoc: leafSPKISHA512,
171 }
172 recordDANEEESPKIFull := adns.TLSA{
173 Usage: adns.TLSAUsageDANEEE,
174 Selector: adns.TLSASelectorSPKI,
175 MatchType: adns.TLSAMatchTypeFull,
176 CertAssoc: tlsLeafCert.Leaf.RawSubjectPublicKeyInfo,
177 }
178 mismatchRecordDANEEESPKISHA256 := adns.TLSA{
179 Usage: adns.TLSAUsageDANEEE,
180 Selector: adns.TLSASelectorSPKI,
181 MatchType: adns.TLSAMatchTypeSHA256,
182 CertAssoc: make([]byte, sha256.Size), // Zero, no match.
183 }
184 malformedRecordDANEEESPKISHA256 := adns.TLSA{
185 Usage: adns.TLSAUsageDANEEE,
186 Selector: adns.TLSASelectorSPKI,
187 MatchType: adns.TLSAMatchTypeSHA256,
188 CertAssoc: leafSPKISHA256[:16], // Too short.
189 }
190 unknownparamRecordDANEEESPKISHA256 := adns.TLSA{
191 Usage: adns.TLSAUsage(10), // Unrecognized value.
192 Selector: adns.TLSASelectorSPKI,
193 MatchType: adns.TLSAMatchTypeSHA256,
194 CertAssoc: leafSPKISHA256,
195 }
196 recordDANETACertSHA256 := adns.TLSA{
197 Usage: adns.TLSAUsageDANETA,
198 Selector: adns.TLSASelectorCert,
199 MatchType: adns.TLSAMatchTypeSHA256,
200 CertAssoc: taCertSHA256,
201 }
202 recordDANETACertFull := adns.TLSA{
203 Usage: adns.TLSAUsageDANETA,
204 Selector: adns.TLSASelectorCert,
205 MatchType: adns.TLSAMatchTypeFull,
206 CertAssoc: taCert.Raw,
207 }
208 malformedRecordDANETACertFull := adns.TLSA{
209 Usage: adns.TLSAUsageDANETA,
210 Selector: adns.TLSASelectorCert,
211 MatchType: adns.TLSAMatchTypeFull,
212 CertAssoc: taCert.Raw[1:], // Cannot parse certificate.
213 }
214 mismatchRecordDANETACertSHA256 := adns.TLSA{
215 Usage: adns.TLSAUsageDANETA,
216 Selector: adns.TLSASelectorCert,
217 MatchType: adns.TLSAMatchTypeSHA256,
218 CertAssoc: make([]byte, sha256.Size), // Zero, no match.
219 }
220 recordPKIXEESPKISHA256 := adns.TLSA{
221 Usage: adns.TLSAUsagePKIXEE,
222 Selector: adns.TLSASelectorSPKI,
223 MatchType: adns.TLSAMatchTypeSHA256,
224 CertAssoc: leafSPKISHA256,
225 }
226 recordPKIXTACertSHA256 := adns.TLSA{
227 Usage: adns.TLSAUsagePKIXTA,
228 Selector: adns.TLSASelectorCert,
229 MatchType: adns.TLSAMatchTypeSHA256,
230 CertAssoc: taCertSHA256,
231 }
232
233 resolver := dns.MockResolver{
234 A: map[string][]string{"localhost.": {"127.0.0.1"}},
235 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANEEESPKISHA256}},
236 AllAuthentic: true,
237 }
238
239 // DANE-EE SPKI SHA2-256 record.
240 test(resolver, recordDANEEESPKISHA256, nil)
241
242 // Check that record isn't used if not allowed.
243 allowedUsages = []adns.TLSAUsage{adns.TLSAUsagePKIXTA}
244 test(resolver, zeroRecord, ErrNoMatch)
245 allowedUsages = nil // Restore.
246
247 // Mixed allowed/not allowed usages are fine.
248 resolver = dns.MockResolver{
249 A: map[string][]string{"localhost.": {"127.0.0.1"}},
250 TLSA: map[string][]adns.TLSA{tlsaName: {mismatchRecordDANETACertSHA256, recordDANEEESPKISHA256}},
251 AllAuthentic: true,
252 }
253 allowedUsages = []adns.TLSAUsage{adns.TLSAUsageDANEEE}
254 test(resolver, recordDANEEESPKISHA256, nil)
255 allowedUsages = nil // Restore.
256
257 // DANE-TA CERT SHA2-256 record.
258 resolver.TLSA = map[string][]adns.TLSA{
259 tlsaName: {recordDANETACertSHA256},
260 }
261 test(resolver, recordDANETACertSHA256, nil)
262
263 // No TLSA record.
264 resolver.TLSA = nil
265 test(resolver, zeroRecord, ErrNoRecords)
266
267 // Insecure TLSA record.
268 resolver.TLSA = map[string][]adns.TLSA{
269 tlsaName: {recordDANEEESPKISHA256},
270 }
271 resolver.Inauthentic = []string{"tlsa " + tlsaName}
272 test(resolver, zeroRecord, ErrInsecure)
273
274 // Insecure CNAME.
275 resolver.Inauthentic = []string{"cname localhost."}
276 test(resolver, zeroRecord, ErrInsecure)
277
278 // Insecure TLSA
279 resolver.Inauthentic = []string{"tlsa " + tlsaName}
280 test(resolver, zeroRecord, ErrInsecure)
281
282 // Insecure CNAME should not look at TLSA records under that name, only under original.
283 // Initial name/cname is secure. And it has secure TLSA records. But the lookup for
284 // example1 is not secure, though the final example2 records are.
285 resolver = dns.MockResolver{
286 A: map[string][]string{"example2.": {"127.0.0.1"}},
287 CNAME: map[string]string{"localhost.": "example1.", "example1.": "example2."},
288 TLSA: map[string][]adns.TLSA{
289 fmt.Sprintf("_%d._tcp.example2.", port): {mismatchRecordDANETACertSHA256}, // Should be ignored.
290 tlsaName: {recordDANEEESPKISHA256}, // Should match.
291 },
292 AllAuthentic: true,
293 Inauthentic: []string{"cname example1."},
294 }
295 test(resolver, recordDANEEESPKISHA256, nil)
296
297 // Matching records after following cname.
298 resolver = dns.MockResolver{
299 A: map[string][]string{"example.": {"127.0.0.1"}},
300 CNAME: map[string]string{"localhost.": "example."},
301 TLSA: map[string][]adns.TLSA{fmt.Sprintf("_%d._tcp.example.", port): {recordDANETACertSHA256}},
302 AllAuthentic: true,
303 }
304 test(resolver, recordDANETACertSHA256, nil)
305
306 // Fallback to original name for TLSA records if cname-expanded name doesn't have records.
307 resolver = dns.MockResolver{
308 A: map[string][]string{"example.": {"127.0.0.1"}},
309 CNAME: map[string]string{"localhost.": "example."},
310 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANETACertSHA256}},
311 AllAuthentic: true,
312 }
313 test(resolver, recordDANETACertSHA256, nil)
314
315 // Invalid DANE-EE record.
316 resolver = dns.MockResolver{
317 A: map[string][]string{
318 "localhost.": {"127.0.0.1"},
319 },
320 TLSA: map[string][]adns.TLSA{
321 tlsaName: {mismatchRecordDANEEESPKISHA256},
322 },
323 AllAuthentic: true,
324 }
325 test(resolver, zeroRecord, ErrNoMatch)
326
327 // DANE-EE SPKI SHA2-512 record.
328 resolver = dns.MockResolver{
329 A: map[string][]string{"localhost.": {"127.0.0.1"}},
330 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANEEESPKISHA512}},
331 AllAuthentic: true,
332 }
333 test(resolver, recordDANEEESPKISHA512, nil)
334
335 // DANE-EE SPKI Full record.
336 resolver = dns.MockResolver{
337 A: map[string][]string{"localhost.": {"127.0.0.1"}},
338 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANEEESPKIFull}},
339 AllAuthentic: true,
340 }
341 test(resolver, recordDANEEESPKIFull, nil)
342
343 // DANE-TA with full certificate.
344 resolver = dns.MockResolver{
345 A: map[string][]string{"localhost.": {"127.0.0.1"}},
346 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANETACertFull}},
347 AllAuthentic: true,
348 }
349 test(resolver, recordDANETACertFull, nil)
350
351 // DANE-TA for cert not in TLS handshake.
352 resolver = dns.MockResolver{
353 A: map[string][]string{"localhost.": {"127.0.0.1"}},
354 TLSA: map[string][]adns.TLSA{tlsaName: {mismatchRecordDANETACertSHA256}},
355 AllAuthentic: true,
356 }
357 test(resolver, zeroRecord, ErrNoMatch)
358
359 // DANE-TA with leaf cert for other name.
360 resolver = dns.MockResolver{
361 A: map[string][]string{"example.": {"127.0.0.1"}},
362 TLSA: map[string][]adns.TLSA{fmt.Sprintf("_%d._tcp.example.", port): {recordDANETACertSHA256}},
363 AllAuthentic: true,
364 }
365 origDialHost := dialHost
366 dialHost = "example."
367 test(resolver, zeroRecord, ErrNoMatch)
368 dialHost = origDialHost
369
370 // DANE-TA with expired cert.
371 resolver = dns.MockResolver{
372 A: map[string][]string{"localhost.": {"127.0.0.1"}},
373 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANETACertSHA256}},
374 AllAuthentic: true,
375 }
376 tlsConfig.Store(&tls.Config{
377 Certificates: []tls.Certificate{tlsLeafCertExpired},
378 })
379 test(resolver, zeroRecord, ErrNoMatch)
380 test(resolver, zeroRecord, &VerifyError{})
381 test(resolver, zeroRecord, &x509.CertificateInvalidError{})
382 // Restore.
383 tlsConfig.Store(&tls.Config{
384 Certificates: []tls.Certificate{tlsLeafCert},
385 })
386
387 // Malformed TLSA record is unusable, resulting in failure if none left.
388 resolver = dns.MockResolver{
389 A: map[string][]string{"localhost.": {"127.0.0.1"}},
390 TLSA: map[string][]adns.TLSA{tlsaName: {malformedRecordDANEEESPKISHA256}},
391 AllAuthentic: true,
392 }
393 test(resolver, zeroRecord, ErrNoMatch)
394
395 // Malformed TLSA record is unusable and skipped, other verified record causes Dial to succeed.
396 resolver = dns.MockResolver{
397 A: map[string][]string{"localhost.": {"127.0.0.1"}},
398 TLSA: map[string][]adns.TLSA{tlsaName: {malformedRecordDANEEESPKISHA256, recordDANEEESPKISHA256}},
399 AllAuthentic: true,
400 }
401 test(resolver, recordDANEEESPKISHA256, nil)
402
403 // Record with unknown parameters (usage in this case) is unusable, resulting in failure if none left.
404 resolver = dns.MockResolver{
405 A: map[string][]string{"localhost.": {"127.0.0.1"}},
406 TLSA: map[string][]adns.TLSA{tlsaName: {unknownparamRecordDANEEESPKISHA256}},
407 AllAuthentic: true,
408 }
409 test(resolver, zeroRecord, ErrNoMatch)
410
411 // Unknown parameter does not prevent other valid record to verify.
412 resolver = dns.MockResolver{
413 A: map[string][]string{"localhost.": {"127.0.0.1"}},
414 TLSA: map[string][]adns.TLSA{tlsaName: {unknownparamRecordDANEEESPKISHA256, recordDANEEESPKISHA256}},
415 AllAuthentic: true,
416 }
417 test(resolver, recordDANEEESPKISHA256, nil)
418
419 // Malformed full TA certificate.
420 resolver = dns.MockResolver{
421 A: map[string][]string{"localhost.": {"127.0.0.1"}},
422 TLSA: map[string][]adns.TLSA{tlsaName: {malformedRecordDANETACertFull}},
423 AllAuthentic: true,
424 }
425 test(resolver, zeroRecord, ErrNoMatch)
426
427 // Full TA certificate without getting it from TLS server.
428 resolver = dns.MockResolver{
429 A: map[string][]string{"localhost.": {"127.0.0.1"}},
430 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANETACertFull}},
431 AllAuthentic: true,
432 }
433 tlsLeafOnlyCert := tlsLeafCert
434 tlsLeafOnlyCert.Certificate = tlsLeafOnlyCert.Certificate[:1]
435 tlsConfig.Store(&tls.Config{
436 Certificates: []tls.Certificate{tlsLeafOnlyCert},
437 })
438 test(resolver, recordDANETACertFull, nil)
439 // Restore.
440 tlsConfig.Store(&tls.Config{
441 Certificates: []tls.Certificate{tlsLeafCert},
442 })
443
444 // PKIXEE, will fail due to not being CA-signed.
445 resolver = dns.MockResolver{
446 A: map[string][]string{"localhost.": {"127.0.0.1"}},
447 TLSA: map[string][]adns.TLSA{tlsaName: {recordPKIXEESPKISHA256}},
448 AllAuthentic: true,
449 }
450 test(resolver, zeroRecord, &x509.UnknownAuthorityError{})
451
452 // PKIXTA, will fail due to not being CA-signed.
453 resolver = dns.MockResolver{
454 A: map[string][]string{"localhost.": {"127.0.0.1"}},
455 TLSA: map[string][]adns.TLSA{tlsaName: {recordPKIXTACertSHA256}},
456 AllAuthentic: true,
457 }
458 test(resolver, zeroRecord, &x509.UnknownAuthorityError{})
459
460 // Now we add the TA to the "pkix" trusted roots and try again.
461 pkixRoots.AddCert(taCert)
462
463 // PKIXEE, will now succeed.
464 resolver = dns.MockResolver{
465 A: map[string][]string{"localhost.": {"127.0.0.1"}},
466 TLSA: map[string][]adns.TLSA{tlsaName: {recordPKIXEESPKISHA256}},
467 AllAuthentic: true,
468 }
469 test(resolver, recordPKIXEESPKISHA256, nil)
470
471 // PKIXTA, will fail due to not being CA-signed.
472 resolver = dns.MockResolver{
473 A: map[string][]string{"localhost.": {"127.0.0.1"}},
474 TLSA: map[string][]adns.TLSA{tlsaName: {recordPKIXTACertSHA256}},
475 AllAuthentic: true,
476 }
477 test(resolver, recordPKIXTACertSHA256, nil)
478}
479