package va import ( "context" "crypto/sha256" "crypto/subtle" "encoding/base64" "fmt" "net/netip" "github.com/letsencrypt/boulder/bdns" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/identifier" ) // getAddr will query for all A/AAAA records associated with hostname and return // the preferred address, the first netip.Addr 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 netip.Addr slice. func (va ValidationAuthorityImpl) getAddrs(ctx context.Context, hostname string) ([]netip.Addr, 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 } // availableAddresses takes a ValidationRecord and splits the AddressesResolved // into a list of IPv4 and IPv6 addresses. func availableAddresses(allAddrs []netip.Addr) (v4 []netip.Addr, v6 []netip.Addr) { for _, addr := range allAddrs { if addr.Is4() { v4 = append(v4, addr) } else { v6 = append(v6, addr) } } return } func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { if ident.Type != identifier.TypeDNS { va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident) return nil, berrors.MalformedError("Identifier type for DNS challenge was not DNS") } // Compute the digest of the key authorization file h := sha256.New() h.Write([]byte(keyAuthorization)) authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) // Look for the required record in the DNS challengeSubdomain := fmt.Sprintf("%s.%s", core.DNSPrefix, ident.Value) 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) }