File: dns_challenge.go

package info (click to toggle)
golang-github-xenolf-lego 4.9.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 5,080 kB
  • sloc: xml: 533; makefile: 128; sh: 18
file content (214 lines) | stat: -rw-r--r-- 5,280 bytes parent folder | download | duplicates (2)
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package dns01

import (
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/go-acme/lego/v4/acme"
	"github.com/go-acme/lego/v4/acme/api"
	"github.com/go-acme/lego/v4/challenge"
	"github.com/go-acme/lego/v4/log"
	"github.com/go-acme/lego/v4/platform/wait"
	"github.com/miekg/dns"
)

const (
	// DefaultPropagationTimeout default propagation timeout.
	DefaultPropagationTimeout = 60 * time.Second

	// DefaultPollingInterval default polling interval.
	DefaultPollingInterval = 2 * time.Second

	// DefaultTTL default TTL.
	DefaultTTL = 120
)

type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error

type ChallengeOption func(*Challenge) error

// CondOption Conditional challenge option.
func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
	if !condition {
		// NoOp options
		return func(*Challenge) error {
			return nil
		}
	}
	return opt
}

// Challenge implements the dns-01 challenge.
type Challenge struct {
	core       *api.Core
	validate   ValidateFunc
	provider   challenge.Provider
	preCheck   preCheck
	dnsTimeout time.Duration
}

func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
	chlg := &Challenge{
		core:       core,
		validate:   validate,
		provider:   provider,
		preCheck:   newPreCheck(),
		dnsTimeout: 10 * time.Second,
	}

	for _, opt := range opts {
		err := opt(chlg)
		if err != nil {
			log.Infof("challenge option error: %v", err)
		}
	}

	return chlg
}

// PreSolve just submits the txt record to the dns provider.
// It does not validate record propagation, or do anything at all with the acme server.
func (c *Challenge) PreSolve(authz acme.Authorization) error {
	domain := challenge.GetTargetedDomain(authz)
	log.Infof("[%s] acme: Preparing to solve DNS-01", domain)

	chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
	if err != nil {
		return err
	}

	if c.provider == nil {
		return fmt.Errorf("[%s] acme: no DNS Provider configured", domain)
	}

	// Generate the Key Authorization for the challenge
	keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
	if err != nil {
		return err
	}

	err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
	if err != nil {
		return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err)
	}

	return nil
}

func (c *Challenge) Solve(authz acme.Authorization) error {
	domain := challenge.GetTargetedDomain(authz)
	log.Infof("[%s] acme: Trying to solve DNS-01", domain)

	chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
	if err != nil {
		return err
	}

	// Generate the Key Authorization for the challenge
	keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
	if err != nil {
		return err
	}

	fqdn, value := GetRecord(authz.Identifier.Value, keyAuth)

	var timeout, interval time.Duration
	switch provider := c.provider.(type) {
	case challenge.ProviderTimeout:
		timeout, interval = provider.Timeout()
	default:
		timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
	}

	log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers)

	time.Sleep(interval)

	err = wait.For("propagation", timeout, interval, func() (bool, error) {
		stop, errP := c.preCheck.call(domain, fqdn, value)
		if !stop || errP != nil {
			log.Infof("[%s] acme: Waiting for DNS record propagation.", domain)
		}
		return stop, errP
	})
	if err != nil {
		return err
	}

	chlng.KeyAuthorization = keyAuth
	return c.validate(c.core, domain, chlng)
}

// CleanUp cleans the challenge.
func (c *Challenge) CleanUp(authz acme.Authorization) error {
	log.Infof("[%s] acme: Cleaning DNS-01 challenge", challenge.GetTargetedDomain(authz))

	chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
	if err != nil {
		return err
	}

	keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
	if err != nil {
		return err
	}

	return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
}

func (c *Challenge) Sequential() (bool, time.Duration) {
	if p, ok := c.provider.(sequential); ok {
		return ok, p.Sequential()
	}
	return false, 0
}

type sequential interface {
	Sequential() time.Duration
}

// GetRecord returns a DNS record which will fulfill the `dns-01` challenge.
func GetRecord(domain, keyAuth string) (fqdn, value string) {
	keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
	// base64URL encoding without padding
	value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])

	fqdn = getChallengeFqdn(domain)

	return
}

func getChallengeFqdn(domain string) string {
	fqdn := fmt.Sprintf("_acme-challenge.%s.", domain)

	if ok, _ := strconv.ParseBool(os.Getenv("LEGO_DISABLE_CNAME_SUPPORT")); ok {
		return fqdn
	}

	// recursion counter so it doesn't spin out of control
	for limit := 0; limit < 50; limit++ {
		// Keep following CNAMEs
		r, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true)

		if err != nil || r.Rcode != dns.RcodeSuccess {
			// No more CNAME records to follow, exit
			break
		}

		// Check if the domain has CNAME then use that
		cname := updateDomainWithCName(r, fqdn)
		if cname == fqdn {
			break
		}

		log.Infof("Found CNAME entry for %q: %q", fqdn, cname)

		fqdn = cname
	}

	return fqdn
}