forked from letsencrypt/boulder
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dns.go
150 lines (133 loc) · 5.64 KB
/
dns.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package va
import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/base32"
"encoding/base64"
"fmt"
"net"
"strings"
"github.com/letsencrypt/boulder/bdns"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/probs"
)
// getAddr will query for all A/AAAA records associated with hostname and return
// the preferred address, the first net.IP in the addrs slice, and all addresses
// resolved. This is the same choice made by the Go internal resolution library
// used by net/http. If there is an error resolving the hostname, or if no
// usable IP addresses are available then a berrors.DNSError instance is
// returned with a nil net.IP slice.
func (va ValidationAuthorityImpl) getAddrs(ctx context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) {
addrs, resolvers, err := va.dnsClient.LookupHost(ctx, hostname)
if err != nil {
return nil, resolvers, berrors.DNSError("%v", err)
}
if len(addrs) == 0 {
// This should be unreachable, as no valid IP addresses being found results
// in an error being returned from LookupHost.
return nil, resolvers, berrors.DNSError("No valid IP addresses found for %s", hostname)
}
va.log.Debugf("Resolved addresses for %s: %s", hostname, addrs)
return addrs, resolvers, nil
}
// accountURLHostname takes regid and known AccountURLPrefixes and create
// possible list of dns-account-01 challenge subdomains.
func accountURLHostname(AccountURLPrefixes []string, ident identifier.ACMEIdentifier, regid int64) []string {
var testdomains []string
for _, prefix := range AccountURLPrefixes {
accturl := fmt.Sprintf("%s%d", prefix, regid)
hash := sha256.Sum256([]byte(accturl))
urlhash := strings.ToLower(base32.StdEncoding.EncodeToString(hash[0:10]))
testdomain := fmt.Sprintf("_acme-challenge_%s.%s", urlhash, ident.Value)
testdomains = append(testdomains, testdomain)
}
return testdomains
}
// availableAddresses takes a ValidationRecord and splits the AddressesResolved
// into a list of IPv4 and IPv6 addresses.
func availableAddresses(allAddrs []net.IP) (v4 []net.IP, v6 []net.IP) {
for _, addr := range allAddrs {
if addr.To4() != nil {
v4 = append(v4, addr)
} else {
v6 = append(v6, addr)
}
}
return
}
func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, ident identifier.ACMEIdentifier, challenge core.Challenge) ([]core.ValidationRecord, error) {
if ident.Type != identifier.DNS {
va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident)
return nil, berrors.MalformedError("Identifier type for DNS was not itself DNS")
}
// Compute the digest of the key authorization file
h := sha256.New()
h.Write([]byte(challenge.ProvidedKeyAuthorization))
authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
// Look for the required record in the DNS
challengeSubdomain := fmt.Sprintf("%s.%s", core.DNSPrefix, ident.Value)
return va.validateDNSsingle(ctx, ident, authorizedKeysDigest, challengeSubdomain)
}
func (va *ValidationAuthorityImpl) validateDNSsingle(ctx context.Context, ident identifier.ACMEIdentifier,
authorizedKeysDigest string, challengeSubdomain string) ([]core.ValidationRecord, error) {
txts, resolvers, err := va.dnsClient.LookupTXT(ctx, challengeSubdomain)
if err != nil {
return nil, berrors.DNSError("%s", err)
}
// If there weren't any TXT records return a distinct error message to allow
// troubleshooters to differentiate between no TXT records and
// invalid/incorrect TXT records.
if len(txts) == 0 {
return nil, berrors.UnauthorizedError("No TXT record found at %s", challengeSubdomain)
}
for _, element := range txts {
if subtle.ConstantTimeCompare([]byte(element), []byte(authorizedKeysDigest)) == 1 {
// Successful challenge validation
return []core.ValidationRecord{{Hostname: ident.Value, ResolverAddrs: resolvers}}, nil
}
}
invalidRecord := txts[0]
if len(invalidRecord) > 100 {
invalidRecord = invalidRecord[0:100] + "..."
}
var andMore string
if len(txts) > 1 {
andMore = fmt.Sprintf(" (and %d more)", len(txts)-1)
}
return nil, berrors.UnauthorizedError("Incorrect TXT record %q%s found at %s",
invalidRecord, andMore, challengeSubdomain)
}
func (va *ValidationAuthorityImpl) validateDNSAccount01(ctx context.Context, ident identifier.ACMEIdentifier, challenge core.Challenge, regid int64) ([]core.ValidationRecord, error) {
if ident.Type != identifier.DNS {
va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident)
return nil, probs.Malformed("Identifier type for DNS was not itself DNS")
}
if regid == 0 {
return nil, probs.Malformed("got request for invalid account context")
}
// Compute the digest of the key authorization file
h := sha256.New()
h.Write([]byte(challenge.ProvidedKeyAuthorization))
authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
//we don't know what accountURIPrefixes client used so we have to try all
challengeSubdomains := accountURLHostname(va.accountURIPrefixes, ident, regid)
var errs []error
// for all valid accounturl hostname we try hash of those
for _, csub := range challengeSubdomains {
res, err := va.validateDNSsingle(ctx, ident, authorizedKeysDigest, csub)
if err == nil {
return res, nil
} else {
errs = append(errs, err)
}
} //now everything returned error, what accountURLprefix client used?
if len(errs) == 1 {
return nil, errs[0] //there was only one name on prefix return that
}
//return last one because it's v2 accounturl and what most account would have
//TODO:actually trace request account URL from WFE and use this, or prefer txt than nxdomain
return nil, errs[len(errs)-1]
}