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