7 cryptorand "crypto/rand"
24 "github.com/mjl-/adns"
26 "github.com/mjl-/mox/dns"
27 "github.com/mjl-/mox/mlog"
30func tcheckf(t *testing.T, err error, format string, args ...any) {
33 t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
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)
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,
54 BasicConstraintsValid: true,
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")
65 tacertsha256 := sha256.Sum256(taCert.Raw)
66 taCertSHA256 := tacertsha256[:]
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")
73 makeLeaf := func(expired bool) (tls.Certificate, []byte, []byte) {
76 now = now.Add(-2 * time.Hour)
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"},
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")
90 leafSPKISHA256 := sha256.Sum256(leafCert.RawSubjectPublicKeyInfo)
91 leafSPKISHA512 := sha512.Sum512(leafCert.RawSubjectPublicKeyInfo)
93 tlsLeafCert := tls.Certificate{
94 Certificate: [][]byte{leafCertBuf, taCertBuf},
95 PrivateKey: leafPriv, // .(crypto.PrivateKey),
98 return tlsLeafCert, leafSPKISHA256[:], leafSPKISHA512[:]
100 tlsLeafCert, leafSPKISHA256, leafSPKISHA512 := makeLeaf(false)
101 tlsLeafCertExpired, _, _ := makeLeaf(true)
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")
113 defer listenConn.Close()
115 // Config for server, replaced during tests.
116 var tlsConfig atomic.Pointer[tls.Config]
117 tlsConfig.Store(&tls.Config{
118 Certificates: []tls.Certificate{tlsLeafCert},
121 // Loop handling incoming TLS connections.
124 conn, err := listenConn.Accept()
129 tlsConn := tls.Server(conn, tlsConfig.Load())
135 dialHost := "localhost"
136 var allowedUsages []adns.TLSAUsage
138 pkixRoots := x509.NewCertPool()
140 // Helper function for dialing with DANE.
141 test := func(resolver dns.Resolver, expRecord adns.TLSA, expErr any) {
144 conn, record, err := Dial(context.Background(), log.Logger, resolver, "tcp", net.JoinHostPort(dialHost, portstr), allowedUsages, pkixRoots)
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)
151 if !reflect.DeepEqual(record, expRecord) {
152 t.Fatalf("got verified record %v, expected %v", record, expRecord)
156 tlsaName := fmt.Sprintf("_%d._tcp.localhost.", port)
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,
166 recordDANEEESPKISHA512 := adns.TLSA{
167 Usage: adns.TLSAUsageDANEEE,
168 Selector: adns.TLSASelectorSPKI,
169 MatchType: adns.TLSAMatchTypeSHA512,
170 CertAssoc: leafSPKISHA512,
172 recordDANEEESPKIFull := adns.TLSA{
173 Usage: adns.TLSAUsageDANEEE,
174 Selector: adns.TLSASelectorSPKI,
175 MatchType: adns.TLSAMatchTypeFull,
176 CertAssoc: tlsLeafCert.Leaf.RawSubjectPublicKeyInfo,
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.
184 malformedRecordDANEEESPKISHA256 := adns.TLSA{
185 Usage: adns.TLSAUsageDANEEE,
186 Selector: adns.TLSASelectorSPKI,
187 MatchType: adns.TLSAMatchTypeSHA256,
188 CertAssoc: leafSPKISHA256[:16], // Too short.
190 unknownparamRecordDANEEESPKISHA256 := adns.TLSA{
191 Usage: adns.TLSAUsage(10), // Unrecognized value.
192 Selector: adns.TLSASelectorSPKI,
193 MatchType: adns.TLSAMatchTypeSHA256,
194 CertAssoc: leafSPKISHA256,
196 recordDANETACertSHA256 := adns.TLSA{
197 Usage: adns.TLSAUsageDANETA,
198 Selector: adns.TLSASelectorCert,
199 MatchType: adns.TLSAMatchTypeSHA256,
200 CertAssoc: taCertSHA256,
202 recordDANETACertFull := adns.TLSA{
203 Usage: adns.TLSAUsageDANETA,
204 Selector: adns.TLSASelectorCert,
205 MatchType: adns.TLSAMatchTypeFull,
206 CertAssoc: taCert.Raw,
208 malformedRecordDANETACertFull := adns.TLSA{
209 Usage: adns.TLSAUsageDANETA,
210 Selector: adns.TLSASelectorCert,
211 MatchType: adns.TLSAMatchTypeFull,
212 CertAssoc: taCert.Raw[1:], // Cannot parse certificate.
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.
220 recordPKIXEESPKISHA256 := adns.TLSA{
221 Usage: adns.TLSAUsagePKIXEE,
222 Selector: adns.TLSASelectorSPKI,
223 MatchType: adns.TLSAMatchTypeSHA256,
224 CertAssoc: leafSPKISHA256,
226 recordPKIXTACertSHA256 := adns.TLSA{
227 Usage: adns.TLSAUsagePKIXTA,
228 Selector: adns.TLSASelectorCert,
229 MatchType: adns.TLSAMatchTypeSHA256,
230 CertAssoc: taCertSHA256,
233 resolver := dns.MockResolver{
234 A: map[string][]string{"localhost.": {"127.0.0.1"}},
235 TLSA: map[string][]adns.TLSA{tlsaName: {recordDANEEESPKISHA256}},
239 // DANE-EE SPKI SHA2-256 record.
240 test(resolver, recordDANEEESPKISHA256, nil)
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.
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}},
253 allowedUsages = []adns.TLSAUsage{adns.TLSAUsageDANEEE}
254 test(resolver, recordDANEEESPKISHA256, nil)
255 allowedUsages = nil // Restore.
257 // DANE-TA CERT SHA2-256 record.
258 resolver.TLSA = map[string][]adns.TLSA{
259 tlsaName: {recordDANETACertSHA256},
261 test(resolver, recordDANETACertSHA256, nil)
265 test(resolver, zeroRecord, ErrNoRecords)
267 // Insecure TLSA record.
268 resolver.TLSA = map[string][]adns.TLSA{
269 tlsaName: {recordDANEEESPKISHA256},
271 resolver.Inauthentic = []string{"tlsa " + tlsaName}
272 test(resolver, zeroRecord, ErrInsecure)
275 resolver.Inauthentic = []string{"cname localhost."}
276 test(resolver, zeroRecord, ErrInsecure)
279 resolver.Inauthentic = []string{"tlsa " + tlsaName}
280 test(resolver, zeroRecord, ErrInsecure)
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.
293 Inauthentic: []string{"cname example1."},
295 test(resolver, recordDANEEESPKISHA256, nil)
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}},
304 test(resolver, recordDANETACertSHA256, nil)
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}},
313 test(resolver, recordDANETACertSHA256, nil)
315 // Invalid DANE-EE record.
316 resolver = dns.MockResolver{
317 A: map[string][]string{
318 "localhost.": {"127.0.0.1"},
320 TLSA: map[string][]adns.TLSA{
321 tlsaName: {mismatchRecordDANEEESPKISHA256},
325 test(resolver, zeroRecord, ErrNoMatch)
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}},
333 test(resolver, recordDANEEESPKISHA512, nil)
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}},
341 test(resolver, recordDANEEESPKIFull, nil)
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}},
349 test(resolver, recordDANETACertFull, nil)
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}},
357 test(resolver, zeroRecord, ErrNoMatch)
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}},
365 origDialHost := dialHost
366 dialHost = "example."
367 test(resolver, zeroRecord, ErrNoMatch)
368 dialHost = origDialHost
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}},
376 tlsConfig.Store(&tls.Config{
377 Certificates: []tls.Certificate{tlsLeafCertExpired},
379 test(resolver, zeroRecord, ErrNoMatch)
380 test(resolver, zeroRecord, &VerifyError{})
381 test(resolver, zeroRecord, &x509.CertificateInvalidError{})
383 tlsConfig.Store(&tls.Config{
384 Certificates: []tls.Certificate{tlsLeafCert},
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}},
393 test(resolver, zeroRecord, ErrNoMatch)
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}},
401 test(resolver, recordDANEEESPKISHA256, nil)
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}},
409 test(resolver, zeroRecord, ErrNoMatch)
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}},
417 test(resolver, recordDANEEESPKISHA256, nil)
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}},
425 test(resolver, zeroRecord, ErrNoMatch)
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}},
433 tlsLeafOnlyCert := tlsLeafCert
434 tlsLeafOnlyCert.Certificate = tlsLeafOnlyCert.Certificate[:1]
435 tlsConfig.Store(&tls.Config{
436 Certificates: []tls.Certificate{tlsLeafOnlyCert},
438 test(resolver, recordDANETACertFull, nil)
440 tlsConfig.Store(&tls.Config{
441 Certificates: []tls.Certificate{tlsLeafCert},
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}},
450 test(resolver, zeroRecord, &x509.UnknownAuthorityError{})
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}},
458 test(resolver, zeroRecord, &x509.UnknownAuthorityError{})
460 // Now we add the TA to the "pkix" trusted roots and try again.
461 pkixRoots.AddCert(taCert)
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}},
469 test(resolver, recordPKIXEESPKISHA256, nil)
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}},
477 test(resolver, recordPKIXTACertSHA256, nil)