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