7 cryptorand "crypto/rand"
23 "github.com/mjl-/adns"
25 "github.com/mjl-/mox/dns"
26 "github.com/mjl-/mox/mlog"
29func tcheckf(t *testing.T, err error, format string, args ...any) {
32 t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
36// Test dialing and DANE TLS verification.
37func TestDial(t *testing.T) {
38 log := mlog.New("dane", nil)
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,
52 BasicConstraintsValid: true,
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")
63 tacertsha256 := sha256.Sum256(taCert.Raw)
64 taCertSHA256 := tacertsha256[:]
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")
71 makeLeaf := func(expired bool) (tls.Certificate, []byte, []byte) {
74 now = now.Add(-2 * time.Hour)
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"},
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")
88 leafSPKISHA256 := sha256.Sum256(leafCert.RawSubjectPublicKeyInfo)
89 leafSPKISHA512 := sha512.Sum512(leafCert.RawSubjectPublicKeyInfo)
91 tlsLeafCert := tls.Certificate{
92 Certificate: [][]byte{leafCertBuf, taCertBuf},
93 PrivateKey: leafPriv, // .(crypto.PrivateKey),
96 return tlsLeafCert, leafSPKISHA256[:], leafSPKISHA512[:]
98 tlsLeafCert, leafSPKISHA256, leafSPKISHA512 := makeLeaf(false)
99 tlsLeafCertExpired, _, _ := makeLeaf(true)
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")
111 defer listenConn.Close()
113 // Config for server, replaced during tests.
114 var tlsConfig atomic.Pointer[tls.Config]
115 tlsConfig.Store(&tls.Config{
116 Certificates: []tls.Certificate{tlsLeafCert},
119 // Loop handling incoming TLS connections.
122 conn, err := listenConn.Accept()
127 tlsConn := tls.Server(conn, tlsConfig.Load())
133 dialHost := "localhost"
134 var allowedUsages []adns.TLSAUsage
136 pkixRoots := x509.NewCertPool()
138 // Helper function for dialing with DANE.
139 test := func(resolver dns.Resolver, expRecord adns.TLSA, expErr any) {
142 conn, record, err := Dial(context.Background(), log.Logger, resolver, "tcp", net.JoinHostPort(dialHost, portstr), allowedUsages, pkixRoots)
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)
149 if !reflect.DeepEqual(record, expRecord) {
150 t.Fatalf("got verified record %v, expected %v", record, expRecord)
154 tlsaName := fmt.Sprintf("_%d._tcp.localhost.", port)
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,
164 recordDANEEESPKISHA512 := adns.TLSA{
165 Usage: adns.TLSAUsageDANEEE,
166 Selector: adns.TLSASelectorSPKI,
167 MatchType: adns.TLSAMatchTypeSHA512,
168 CertAssoc: leafSPKISHA512,
170 recordDANEEESPKIFull := adns.TLSA{
171 Usage: adns.TLSAUsageDANEEE,
172 Selector: adns.TLSASelectorSPKI,
173 MatchType: adns.TLSAMatchTypeFull,
174 CertAssoc: tlsLeafCert.Leaf.RawSubjectPublicKeyInfo,
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.
182 malformedRecordDANEEESPKISHA256 := adns.TLSA{
183 Usage: adns.TLSAUsageDANEEE,
184 Selector: adns.TLSASelectorSPKI,
185 MatchType: adns.TLSAMatchTypeSHA256,
186 CertAssoc: leafSPKISHA256[:16], // Too short.
188 unknownparamRecordDANEEESPKISHA256 := adns.TLSA{
189 Usage: adns.TLSAUsage(10), // Unrecognized value.
190 Selector: adns.TLSASelectorSPKI,
191 MatchType: adns.TLSAMatchTypeSHA256,
192 CertAssoc: leafSPKISHA256,
194 recordDANETACertSHA256 := adns.TLSA{
195 Usage: adns.TLSAUsageDANETA,
196 Selector: adns.TLSASelectorCert,
197 MatchType: adns.TLSAMatchTypeSHA256,
198 CertAssoc: taCertSHA256,
200 recordDANETACertFull := adns.TLSA{
201 Usage: adns.TLSAUsageDANETA,
202 Selector: adns.TLSASelectorCert,
203 MatchType: adns.TLSAMatchTypeFull,
204 CertAssoc: taCert.Raw,
206 malformedRecordDANETACertFull := adns.TLSA{
207 Usage: adns.TLSAUsageDANETA,
208 Selector: adns.TLSASelectorCert,
209 MatchType: adns.TLSAMatchTypeFull,
210 CertAssoc: taCert.Raw[1:], // Cannot parse certificate.
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.
218 recordPKIXEESPKISHA256 := adns.TLSA{
219 Usage: adns.TLSAUsagePKIXEE,
220 Selector: adns.TLSASelectorSPKI,
221 MatchType: adns.TLSAMatchTypeSHA256,
222 CertAssoc: leafSPKISHA256,
224 recordPKIXTACertSHA256 := adns.TLSA{
225 Usage: adns.TLSAUsagePKIXTA,
226 Selector: adns.TLSASelectorCert,
227 MatchType: adns.TLSAMatchTypeSHA256,
228 CertAssoc: taCertSHA256,
231 resolver := dns.MockResolver{
232 A: map[string][]string{"localhost.": {"127.0.0.1"}},
233 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANEEESPKISHA256}},
237 // DANE-EE SPKI SHA2-256 record.
238 test(resolver, recordDANEEESPKISHA256, nil)
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.
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}},
251 allowedUsages = []adns.TLSAUsage{adns.TLSAUsageDANEEE}
252 test(resolver, recordDANEEESPKISHA256, nil)
253 allowedUsages = nil // Restore.
255 // DANE-TA CERT SHA2-256 record.
256 resolver.TLSA = map[string][]adns.TLSA{
257 tlsaName: {recordDANETACertSHA256},
259 test(resolver, recordDANETACertSHA256, nil)
263 test(resolver, zeroRecord, ErrNoRecords)
265 // Insecure TLSA record.
266 resolver.TLSA = map[string][]adns.TLSA{
267 tlsaName: {recordDANEEESPKISHA256},
269 resolver.Inauthentic = []string{"tlsa " + tlsaName}
270 test(resolver, zeroRecord, ErrInsecure)
273 resolver.Inauthentic = []string{"cname localhost."}
274 test(resolver, zeroRecord, ErrInsecure)
277 resolver.Inauthentic = []string{"tlsa " + tlsaName}
278 test(resolver, zeroRecord, ErrInsecure)
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.
291 Inauthentic: []string{"cname example1."},
293 test(resolver, recordDANEEESPKISHA256, nil)
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}},
302 test(resolver, recordDANETACertSHA256, nil)
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}},
311 test(resolver, recordDANETACertSHA256, nil)
313 // Invalid DANE-EE record.
314 resolver = dns.MockResolver{
315 A: map[string][]string{
316 "localhost.": {"127.0.0.1"},
318 TLSA: map[string][]adns.TLSA{
319 tlsaName: {mismatchRecordDANEEESPKISHA256},
323 test(resolver, zeroRecord, ErrNoMatch)
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}},
331 test(resolver, recordDANEEESPKISHA512, nil)
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}},
339 test(resolver, recordDANEEESPKIFull, nil)
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}},
347 test(resolver, recordDANETACertFull, nil)
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}},
355 test(resolver, zeroRecord, ErrNoMatch)
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}},
363 origDialHost := dialHost
364 dialHost = "example."
365 test(resolver, zeroRecord, ErrNoMatch)
366 dialHost = origDialHost
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}},
374 tlsConfig.Store(&tls.Config{
375 Certificates: []tls.Certificate{tlsLeafCertExpired},
377 test(resolver, zeroRecord, ErrNoMatch)
378 test(resolver, zeroRecord, &VerifyError{})
379 test(resolver, zeroRecord, &x509.CertificateInvalidError{})
381 tlsConfig.Store(&tls.Config{
382 Certificates: []tls.Certificate{tlsLeafCert},
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}},
391 test(resolver, zeroRecord, ErrNoMatch)
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}},
399 test(resolver, recordDANEEESPKISHA256, nil)
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}},
407 test(resolver, zeroRecord, ErrNoMatch)
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}},
415 test(resolver, recordDANEEESPKISHA256, nil)
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}},
423 test(resolver, zeroRecord, ErrNoMatch)
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}},
431 tlsLeafOnlyCert := tlsLeafCert
432 tlsLeafOnlyCert.Certificate = tlsLeafOnlyCert.Certificate[:1]
433 tlsConfig.Store(&tls.Config{
434 Certificates: []tls.Certificate{tlsLeafOnlyCert},
436 test(resolver, recordDANETACertFull, nil)
438 tlsConfig.Store(&tls.Config{
439 Certificates: []tls.Certificate{tlsLeafCert},
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}},
448 test(resolver, zeroRecord, &x509.UnknownAuthorityError{})
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}},
456 test(resolver, zeroRecord, &x509.UnknownAuthorityError{})
458 // Now we add the TA to the "pkix" trusted roots and try again.
459 pkixRoots.AddCert(taCert)
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}},
467 test(resolver, recordPKIXEESPKISHA256, nil)
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}},
475 test(resolver, recordPKIXTACertSHA256, nil)