dns_challenge.go raw
1 package dns01
2
3 import (
4 "crypto/sha256"
5 "encoding/base64"
6 "fmt"
7 "os"
8 "strconv"
9 "strings"
10 "time"
11
12 "github.com/go-acme/lego/v4/acme"
13 "github.com/go-acme/lego/v4/acme/api"
14 "github.com/go-acme/lego/v4/challenge"
15 "github.com/go-acme/lego/v4/log"
16 "github.com/go-acme/lego/v4/platform/wait"
17 "github.com/miekg/dns"
18 )
19
20 const (
21 // DefaultPropagationTimeout default propagation timeout.
22 DefaultPropagationTimeout = 60 * time.Second
23
24 // DefaultPollingInterval default polling interval.
25 DefaultPollingInterval = 2 * time.Second
26
27 // DefaultTTL default TTL.
28 DefaultTTL = 120
29 )
30
31 type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
32
33 type ChallengeOption func(*Challenge) error
34
35 // CondOption Conditional challenge option.
36 func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
37 if !condition {
38 // NoOp options
39 return func(*Challenge) error {
40 return nil
41 }
42 }
43
44 return opt
45 }
46
47 // Challenge implements the dns-01 challenge.
48 type Challenge struct {
49 core *api.Core
50 validate ValidateFunc
51 provider challenge.Provider
52 preCheck preCheck
53 dnsTimeout time.Duration
54 }
55
56 func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
57 chlg := &Challenge{
58 core: core,
59 validate: validate,
60 provider: provider,
61 preCheck: newPreCheck(),
62 dnsTimeout: 10 * time.Second,
63 }
64
65 for _, opt := range opts {
66 err := opt(chlg)
67 if err != nil {
68 log.Infof("challenge option error: %v", err)
69 }
70 }
71
72 return chlg
73 }
74
75 // PreSolve just submits the txt record to the dns provider.
76 // It does not validate record propagation, or do anything at all with the acme server.
77 func (c *Challenge) PreSolve(authz acme.Authorization) error {
78 domain := challenge.GetTargetedDomain(authz)
79 log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
80
81 chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
82 if err != nil {
83 return err
84 }
85
86 if c.provider == nil {
87 return fmt.Errorf("[%s] acme: no DNS Provider configured", domain)
88 }
89
90 // Generate the Key Authorization for the challenge
91 keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
92 if err != nil {
93 return err
94 }
95
96 err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
97 if err != nil {
98 return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err)
99 }
100
101 return nil
102 }
103
104 func (c *Challenge) Solve(authz acme.Authorization) error {
105 domain := challenge.GetTargetedDomain(authz)
106 log.Infof("[%s] acme: Trying to solve DNS-01", domain)
107
108 chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
109 if err != nil {
110 return err
111 }
112
113 // Generate the Key Authorization for the challenge
114 keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
115 if err != nil {
116 return err
117 }
118
119 info := GetChallengeInfo(authz.Identifier.Value, keyAuth)
120
121 var timeout, interval time.Duration
122
123 switch provider := c.provider.(type) {
124 case challenge.ProviderTimeout:
125 timeout, interval = provider.Timeout()
126 default:
127 timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
128 }
129
130 log.Infof("[%s] acme: Checking DNS record propagation. [nameservers=%s]", domain, strings.Join(recursiveNameservers, ","))
131
132 time.Sleep(interval)
133
134 err = wait.For("propagation", timeout, interval, func() (bool, error) {
135 stop, errP := c.preCheck.call(domain, info.EffectiveFQDN, info.Value)
136 if !stop || errP != nil {
137 log.Infof("[%s] acme: Waiting for DNS record propagation.", domain)
138 }
139
140 return stop, errP
141 })
142 if err != nil {
143 return err
144 }
145
146 chlng.KeyAuthorization = keyAuth
147
148 return c.validate(c.core, domain, chlng)
149 }
150
151 // CleanUp cleans the challenge.
152 func (c *Challenge) CleanUp(authz acme.Authorization) error {
153 log.Infof("[%s] acme: Cleaning DNS-01 challenge", challenge.GetTargetedDomain(authz))
154
155 chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
156 if err != nil {
157 return err
158 }
159
160 keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
161 if err != nil {
162 return err
163 }
164
165 return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
166 }
167
168 func (c *Challenge) Sequential() (bool, time.Duration) {
169 if p, ok := c.provider.(sequential); ok {
170 return ok, p.Sequential()
171 }
172
173 return false, 0
174 }
175
176 type sequential interface {
177 Sequential() time.Duration
178 }
179
180 // GetRecord returns a DNS record which will fulfill the `dns-01` challenge.
181 //
182 // Deprecated: use GetChallengeInfo instead.
183 func GetRecord(domain, keyAuth string) (fqdn, value string) {
184 info := GetChallengeInfo(domain, keyAuth)
185
186 return info.EffectiveFQDN, info.Value
187 }
188
189 // ChallengeInfo contains the information use to create the TXT record.
190 type ChallengeInfo struct {
191 // FQDN is the full-qualified challenge domain (i.e. `_acme-challenge.[domain].`)
192 FQDN string
193
194 // EffectiveFQDN contains the resulting FQDN after the CNAMEs resolutions.
195 EffectiveFQDN string
196
197 // Value contains the value for the TXT record.
198 Value string
199 }
200
201 // GetChallengeInfo returns information used to create a DNS record which will fulfill the `dns-01` challenge.
202 func GetChallengeInfo(domain, keyAuth string) ChallengeInfo {
203 keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
204 // base64URL encoding without padding
205 value := base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
206
207 ok, _ := strconv.ParseBool(os.Getenv("LEGO_DISABLE_CNAME_SUPPORT"))
208
209 return ChallengeInfo{
210 Value: value,
211 FQDN: getChallengeFQDN(domain, false),
212 EffectiveFQDN: getChallengeFQDN(domain, !ok),
213 }
214 }
215
216 func getChallengeFQDN(domain string, followCNAME bool) string {
217 fqdn := fmt.Sprintf("_acme-challenge.%s.", domain)
218
219 if !followCNAME {
220 return fqdn
221 }
222
223 // recursion counter so it doesn't spin out of control
224 for range 50 {
225 // Keep following CNAMEs
226 r, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true)
227
228 if err != nil || r.Rcode != dns.RcodeSuccess {
229 // No more CNAME records to follow, exit
230 break
231 }
232
233 // Check if the domain has CNAME then use that
234 cname := updateDomainWithCName(r, fqdn)
235 if cname == fqdn {
236 break
237 }
238
239 log.Infof("Found CNAME entry for %q: %q", fqdn, cname)
240
241 fqdn = cname
242 }
243
244 return fqdn
245 }
246