1898 lines
		
	
	
		
			62 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			1898 lines
		
	
	
		
			62 KiB
		
	
	
	
		
			Go
		
	
	
	
package va
 | 
						|
 | 
						|
import (
 | 
						|
	"crypto/rand"
 | 
						|
	"crypto/rsa"
 | 
						|
	"crypto/sha256"
 | 
						|
	"crypto/tls"
 | 
						|
	"crypto/x509"
 | 
						|
	"crypto/x509/pkix"
 | 
						|
	"encoding/asn1"
 | 
						|
	"encoding/base64"
 | 
						|
	"encoding/hex"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"math/big"
 | 
						|
	mrand "math/rand"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"net/http/httptest"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"syscall"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
	"unicode/utf8"
 | 
						|
 | 
						|
	"github.com/golang/mock/gomock"
 | 
						|
	"github.com/jmhodges/clock"
 | 
						|
	"github.com/letsencrypt/boulder/bdns"
 | 
						|
	"github.com/letsencrypt/boulder/cmd"
 | 
						|
	"github.com/letsencrypt/boulder/core"
 | 
						|
	blog "github.com/letsencrypt/boulder/log"
 | 
						|
	"github.com/letsencrypt/boulder/metrics"
 | 
						|
	"github.com/letsencrypt/boulder/metrics/mock_metrics"
 | 
						|
	"github.com/letsencrypt/boulder/probs"
 | 
						|
	"github.com/letsencrypt/boulder/test"
 | 
						|
	vaPB "github.com/letsencrypt/boulder/va/proto"
 | 
						|
	"github.com/prometheus/client_golang/prometheus"
 | 
						|
	"golang.org/x/net/context"
 | 
						|
	"gopkg.in/square/go-jose.v2"
 | 
						|
)
 | 
						|
 | 
						|
func bigIntFromB64(b64 string) *big.Int {
 | 
						|
	bytes, _ := base64.URLEncoding.DecodeString(b64)
 | 
						|
	x := big.NewInt(0)
 | 
						|
	x.SetBytes(bytes)
 | 
						|
	return x
 | 
						|
}
 | 
						|
 | 
						|
func intFromB64(b64 string) int {
 | 
						|
	return int(bigIntFromB64(b64).Int64())
 | 
						|
}
 | 
						|
 | 
						|
var n = bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==")
 | 
						|
var e = intFromB64("AQAB")
 | 
						|
var d = bigIntFromB64("bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ==")
 | 
						|
var p = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
 | 
						|
var q = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
 | 
						|
 | 
						|
var TheKey = rsa.PrivateKey{
 | 
						|
	PublicKey: rsa.PublicKey{N: n, E: e},
 | 
						|
	D:         d,
 | 
						|
	Primes:    []*big.Int{p, q},
 | 
						|
}
 | 
						|
 | 
						|
var accountKey = &jose.JSONWebKey{Key: TheKey.Public()}
 | 
						|
 | 
						|
// Return an ACME DNS identifier for the given hostname
 | 
						|
func dnsi(hostname string) core.AcmeIdentifier {
 | 
						|
	return core.AcmeIdentifier{Type: core.IdentifierDNS, Value: hostname}
 | 
						|
}
 | 
						|
 | 
						|
var ctx = context.Background()
 | 
						|
 | 
						|
var accountURIPrefixes = []string{"http://boulder:4000/acme/reg/"}
 | 
						|
 | 
						|
// All paths that get assigned to tokens MUST be valid tokens
 | 
						|
const expectedToken = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
 | 
						|
const expectedKeyAuthorization = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI"
 | 
						|
const pathWrongToken = "i6lNAC4lOOLYCl-A08VJt9z_tKYvVk63Dumo8icsBjQ"
 | 
						|
const path404 = "404"
 | 
						|
const path500 = "500"
 | 
						|
const pathFound = "GBq8SwWq3JsbREFdCamk5IX3KLsxW5ULeGs98Ajl_UM"
 | 
						|
const pathMoved = "5J4FIMrWNfmvHZo-QpKZngmuhqZGwRm21-oEgUDstJM"
 | 
						|
const pathRedirectInvalidPort = "port-redirect"
 | 
						|
const pathWait = "wait"
 | 
						|
const pathWaitLong = "wait-long"
 | 
						|
const pathReLookup = "7e-P57coLM7D3woNTp_xbJrtlkDYy6PWf3mSSbLwCr4"
 | 
						|
const pathReLookupInvalid = "re-lookup-invalid"
 | 
						|
const pathRedirectToFailingURL = "re-to-failing-url"
 | 
						|
const pathLooper = "looper"
 | 
						|
const pathValid = "valid"
 | 
						|
const rejectUserAgent = "rejectMe"
 | 
						|
 | 
						|
func httpSrv(t *testing.T, token string) *httptest.Server {
 | 
						|
	m := http.NewServeMux()
 | 
						|
 | 
						|
	server := httptest.NewUnstartedServer(m)
 | 
						|
 | 
						|
	defaultToken := token
 | 
						|
	currentToken := defaultToken
 | 
						|
 | 
						|
	m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		if strings.HasSuffix(r.URL.Path, path404) {
 | 
						|
			t.Logf("HTTPSRV: Got a 404 req\n")
 | 
						|
			http.NotFound(w, r)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, path500) {
 | 
						|
			t.Logf("HTTPSRV: Got a 500 req\n")
 | 
						|
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathMoved) {
 | 
						|
			t.Logf("HTTPSRV: Got a 301 redirect req\n")
 | 
						|
			if currentToken == defaultToken {
 | 
						|
				currentToken = pathMoved
 | 
						|
			}
 | 
						|
			http.Redirect(w, r, pathValid, 301)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathFound) {
 | 
						|
			t.Logf("HTTPSRV: Got a 302 redirect req\n")
 | 
						|
			if currentToken == defaultToken {
 | 
						|
				currentToken = pathFound
 | 
						|
			}
 | 
						|
			http.Redirect(w, r, pathMoved, 302)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathWait) {
 | 
						|
			t.Logf("HTTPSRV: Got a wait req\n")
 | 
						|
			time.Sleep(time.Second * 3)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathWaitLong) {
 | 
						|
			t.Logf("HTTPSRV: Got a wait-long req\n")
 | 
						|
			time.Sleep(time.Second * 10)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathReLookup) {
 | 
						|
			t.Logf("HTTPSRV: Got a redirect req to a valid hostname\n")
 | 
						|
			if currentToken == defaultToken {
 | 
						|
				currentToken = pathReLookup
 | 
						|
			}
 | 
						|
			port := getPort(server)
 | 
						|
			http.Redirect(w, r, fmt.Sprintf("http://other.valid:%d/path", port), 302)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathReLookupInvalid) {
 | 
						|
			t.Logf("HTTPSRV: Got a redirect req to an invalid hostname\n")
 | 
						|
			http.Redirect(w, r, "http://invalid.invalid/path", 302)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathRedirectToFailingURL) {
 | 
						|
			t.Logf("HTTPSRV: Redirecting to a URL that will fail\n")
 | 
						|
			port := getPort(server)
 | 
						|
			http.Redirect(w, r, fmt.Sprintf("http://other.valid:%d/%s", port, path500), 301)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathLooper) {
 | 
						|
			t.Logf("HTTPSRV: Got a loop req\n")
 | 
						|
			http.Redirect(w, r, r.URL.String(), 301)
 | 
						|
		} else if strings.HasSuffix(r.URL.Path, pathRedirectInvalidPort) {
 | 
						|
			t.Logf("HTTPSRV: Got a port redirect req\n")
 | 
						|
			// Port 8080 is not the VA's httpPort or httpsPort and should be rejected
 | 
						|
			http.Redirect(w, r, "http://other.valid:8080/path", 302)
 | 
						|
		} else if r.Header.Get("User-Agent") == rejectUserAgent {
 | 
						|
			w.WriteHeader(http.StatusBadRequest)
 | 
						|
			w.Write([]byte("found trap User-Agent"))
 | 
						|
		} else {
 | 
						|
			t.Logf("HTTPSRV: Got a valid req\n")
 | 
						|
			t.Logf("HTTPSRV: Path = %s\n", r.URL.Path)
 | 
						|
 | 
						|
			ch := core.Challenge{Token: currentToken}
 | 
						|
			keyAuthz, _ := ch.ExpectedKeyAuthorization(accountKey)
 | 
						|
			t.Logf("HTTPSRV: Key Authz = '%s%s'\n", keyAuthz, "\\n\\r \\t")
 | 
						|
 | 
						|
			fmt.Fprint(w, keyAuthz, "\n\r \t")
 | 
						|
			currentToken = defaultToken
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	server.Start()
 | 
						|
	return server
 | 
						|
}
 | 
						|
 | 
						|
func tlssni01Srv(t *testing.T, chall core.Challenge) *httptest.Server {
 | 
						|
	h := sha256.Sum256([]byte(chall.ProvidedKeyAuthorization))
 | 
						|
	Z := hex.EncodeToString(h[:])
 | 
						|
	ZName := fmt.Sprintf("%s.%s.acme.invalid", Z[:32], Z[32:])
 | 
						|
 | 
						|
	return tlssniSrvWithNames(t, chall, ZName)
 | 
						|
}
 | 
						|
 | 
						|
func tlsCertTemplate(names []string) *x509.Certificate {
 | 
						|
	return &x509.Certificate{
 | 
						|
		SerialNumber: big.NewInt(1337),
 | 
						|
		Subject: pkix.Name{
 | 
						|
			Organization: []string{"tests"},
 | 
						|
		},
 | 
						|
		NotBefore: time.Now(),
 | 
						|
		NotAfter:  time.Now().AddDate(0, 0, 1),
 | 
						|
 | 
						|
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
 | 
						|
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
 | 
						|
		BasicConstraintsValid: true,
 | 
						|
 | 
						|
		DNSNames: names,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func makeACert(names []string) *tls.Certificate {
 | 
						|
	template := tlsCertTemplate(names)
 | 
						|
	certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
 | 
						|
	return &tls.Certificate{
 | 
						|
		Certificate: [][]byte{certBytes},
 | 
						|
		PrivateKey:  &TheKey,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func tlssniSrvWithNames(t *testing.T, chall core.Challenge, names ...string) *httptest.Server {
 | 
						|
	cert := makeACert(names)
 | 
						|
	tlsConfig := &tls.Config{
 | 
						|
		Certificates: []tls.Certificate{*cert},
 | 
						|
		ClientAuth:   tls.NoClientCert,
 | 
						|
		GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
 | 
						|
			return cert, nil
 | 
						|
		},
 | 
						|
		NextProtos: []string{"http/1.1"},
 | 
						|
	}
 | 
						|
 | 
						|
	hs := httptest.NewUnstartedServer(http.DefaultServeMux)
 | 
						|
	hs.TLS = tlsConfig
 | 
						|
	hs.StartTLS()
 | 
						|
	return hs
 | 
						|
}
 | 
						|
 | 
						|
func tlsalpn01Srv(t *testing.T, chall core.Challenge, oid asn1.ObjectIdentifier, names ...string) *httptest.Server {
 | 
						|
	template := tlsCertTemplate(names)
 | 
						|
	certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
 | 
						|
	cert := &tls.Certificate{
 | 
						|
		Certificate: [][]byte{certBytes},
 | 
						|
		PrivateKey:  &TheKey,
 | 
						|
	}
 | 
						|
 | 
						|
	shasum := sha256.Sum256([]byte(chall.ProvidedKeyAuthorization))
 | 
						|
	encHash, _ := asn1.Marshal(shasum[:])
 | 
						|
	acmeExtension := pkix.Extension{
 | 
						|
		Id:       oid,
 | 
						|
		Critical: true,
 | 
						|
		Value:    encHash,
 | 
						|
	}
 | 
						|
 | 
						|
	template.ExtraExtensions = []pkix.Extension{acmeExtension}
 | 
						|
	certBytes, _ = x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
 | 
						|
	acmeCert := &tls.Certificate{
 | 
						|
		Certificate: [][]byte{certBytes},
 | 
						|
		PrivateKey:  &TheKey,
 | 
						|
	}
 | 
						|
 | 
						|
	tlsConfig := &tls.Config{
 | 
						|
		Certificates: []tls.Certificate{},
 | 
						|
		ClientAuth:   tls.NoClientCert,
 | 
						|
		GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
 | 
						|
			if clientHello.ServerName != names[0] {
 | 
						|
				return nil, nil
 | 
						|
			}
 | 
						|
			if len(clientHello.SupportedProtos) == 1 && clientHello.SupportedProtos[0] == ACMETLS1Protocol {
 | 
						|
				return acmeCert, nil
 | 
						|
			}
 | 
						|
			return cert, nil
 | 
						|
		},
 | 
						|
		NextProtos: []string{"http/1.1", ACMETLS1Protocol},
 | 
						|
	}
 | 
						|
 | 
						|
	hs := httptest.NewUnstartedServer(http.DefaultServeMux)
 | 
						|
	hs.TLS = tlsConfig
 | 
						|
	hs.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){
 | 
						|
		ACMETLS1Protocol: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
 | 
						|
			_ = conn.Close()
 | 
						|
		},
 | 
						|
	}
 | 
						|
	hs.StartTLS()
 | 
						|
	return hs
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPBadPort(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, expectedToken)
 | 
						|
 | 
						|
	hs := httpSrv(t, chall.Token)
 | 
						|
	defer hs.Close()
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	// Pick a random port between 40000 and 65000 - with great certainty we won't
 | 
						|
	// have an HTTP server listening on this port and the test will fail as
 | 
						|
	// intended
 | 
						|
	badPort := 40000 + mrand.Intn(25000)
 | 
						|
	va.httpPort = badPort
 | 
						|
 | 
						|
	_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Server's down; expected refusal. Where did we connect?")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
 | 
						|
	if !strings.Contains(prob.Detail, "Connection refused") {
 | 
						|
		t.Errorf("Expected a connection refused error, got %q", prob.Detail)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTP(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, expectedToken)
 | 
						|
 | 
						|
	// NOTE: We do not attempt to shut down the server. The problem is that the
 | 
						|
	// "wait-long" handler sleeps for ten seconds, but this test finishes in less
 | 
						|
	// than that. So if we try to call hs.Close() at the end of the test, we'll be
 | 
						|
	// closing the test server while a request is still pending. Unfortunately,
 | 
						|
	// there appears to be an issue in httptest that trips Go's race detector when
 | 
						|
	// that happens, failing the test. So instead, we live with leaving the server
 | 
						|
	// around till the process exits.
 | 
						|
	// TODO(#1989): close hs
 | 
						|
	hs := httpSrv(t, chall.Token)
 | 
						|
 | 
						|
	va, log := setup(hs, 0)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	t.Logf("Trying to validate: %+v\n", chall)
 | 
						|
	_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Errorf("Unexpected failure in HTTP validation: %s", prob)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, path404)
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Should have found a 404 for the challenge.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, pathWrongToken)
 | 
						|
	// The "wrong token" will actually be the expectedToken.  It's wrong
 | 
						|
	// because it doesn't match pathWrongToken.
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Should have found the wrong token value.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, pathMoved)
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Fatalf("Failed to follow 301 redirect")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, pathFound)
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Fatalf("Failed to follow 302 redirect")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathFound+`" to ".*/`+pathMoved+`"`)), 1)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
 | 
						|
 | 
						|
	ipIdentifier := core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}
 | 
						|
	_, prob = va.validateHTTP01(ctx, ipIdentifier, chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("IdentifierType IP shouldn't have worked.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.MalformedProblem)
 | 
						|
 | 
						|
	_, prob = va.validateHTTP01(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Domain name is invalid.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnknownHostProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPTimeout(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, expectedToken)
 | 
						|
 | 
						|
	hs := httpSrv(t, chall.Token)
 | 
						|
	// TODO(#1989): close hs
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	setChallengeToken(&chall, pathWaitLong)
 | 
						|
	started := time.Now()
 | 
						|
 | 
						|
	timeout := 50 * time.Millisecond
 | 
						|
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 | 
						|
	defer cancel()
 | 
						|
 | 
						|
	_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Connection should've timed out")
 | 
						|
	}
 | 
						|
	took := time.Since(started)
 | 
						|
	// Check that the HTTP connection doesn't return before a timeout, and times
 | 
						|
	// out after the expected time
 | 
						|
	if took < timeout {
 | 
						|
		t.Fatalf("HTTP timed out before %s: %s with %s", timeout, took, prob)
 | 
						|
	}
 | 
						|
	if took > 2*timeout {
 | 
						|
		t.Fatalf("HTTP connection didn't timeout after %s", timeout)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
 | 
						|
	expectMatch := regexp.MustCompile(
 | 
						|
		"Fetching http://localhost:\\d+/.well-known/acme-challenge/wait-long: Timeout after connect")
 | 
						|
	if !expectMatch.MatchString(prob.Detail) {
 | 
						|
		t.Errorf("Problem details incorrect. Got %q, expected to match %q",
 | 
						|
			prob.Detail, expectMatch)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// dnsMockReturnsUnroutable is a DNSClient mock that always returns an
 | 
						|
// unroutable address for LookupHost. This is useful in testing connect
 | 
						|
// timeouts.
 | 
						|
type dnsMockReturnsUnroutable struct {
 | 
						|
	*bdns.MockDNSClient
 | 
						|
}
 | 
						|
 | 
						|
func (mock dnsMockReturnsUnroutable) LookupHost(_ context.Context, hostname string) ([]net.IP, error) {
 | 
						|
	return []net.IP{net.ParseIP("198.51.100.1")}, nil
 | 
						|
}
 | 
						|
 | 
						|
// TestHTTPDialTimeout tests that we give the proper "Timeout during connect"
 | 
						|
// error when dial fails. We do this by using a mock DNS client that resolves
 | 
						|
// everything to an unroutable IP address.
 | 
						|
func TestHTTPDialTimeout(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	started := time.Now()
 | 
						|
	timeout := 50 * time.Millisecond
 | 
						|
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 | 
						|
	defer cancel()
 | 
						|
 | 
						|
	va.dnsClient = dnsMockReturnsUnroutable{&bdns.MockDNSClient{}}
 | 
						|
	// The only method I've found so far to trigger a connect timeout is to
 | 
						|
	// connect to an unrouteable IP address. This usuall generates a connection
 | 
						|
	// timeout, but will rarely return "Network unreachable" instead. If we get
 | 
						|
	// that, just retry until we get something other than "Network unreachable".
 | 
						|
	var prob *probs.ProblemDetails
 | 
						|
	for i := 0; i < 20; i++ {
 | 
						|
		_, prob = va.validateHTTP01(ctx, dnsi("unroutable.invalid"), core.HTTPChallenge01())
 | 
						|
		if prob != nil && strings.Contains(prob.Detail, "Network unreachable") {
 | 
						|
			continue
 | 
						|
		} else {
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Connection should've timed out")
 | 
						|
	}
 | 
						|
	took := time.Since(started)
 | 
						|
	// Check that the HTTP connection doesn't return too fast, and times
 | 
						|
	// out after the expected time
 | 
						|
	if took < timeout/2 {
 | 
						|
		t.Fatalf("HTTP returned before %s (%s) with %#v", timeout, took, prob)
 | 
						|
	}
 | 
						|
	if took > 2*timeout {
 | 
						|
		t.Fatalf("HTTP connection didn't timeout after %s seconds", timeout)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
 | 
						|
	expectMatch := regexp.MustCompile(
 | 
						|
		"Fetching http://unroutable.invalid/.well-known/acme-challenge/.*: Timeout during connect")
 | 
						|
	if !expectMatch.MatchString(prob.Detail) {
 | 
						|
		t.Errorf("Problem details incorrect. Got %q, expected to match %q",
 | 
						|
			prob.Detail, expectMatch)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPRedirectLookup(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, expectedToken)
 | 
						|
 | 
						|
	hs := httpSrv(t, expectedToken)
 | 
						|
	defer hs.Close()
 | 
						|
	va, log := setup(hs, 0)
 | 
						|
 | 
						|
	setChallengeToken(&chall, pathMoved)
 | 
						|
	_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Fatalf("Unexpected failure in redirect (%s): %s", pathMoved, prob)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost: \[127.0.0.1\]`)), 2)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, pathFound)
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Fatalf("Unexpected failure in redirect (%s): %s", pathFound, prob)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathFound+`" to ".*/`+pathMoved+`"`)), 1)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathMoved+`" to ".*/`+pathValid+`"`)), 1)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost: \[127.0.0.1\]`)), 3)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, pathReLookupInvalid)
 | 
						|
	_, err := va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	test.AssertError(t, err, chall.Token)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost: \[127.0.0.1\]`)), 1)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`No valid IP addresses found for invalid.invalid`)), 1)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, pathReLookup)
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Fatalf("Unexpected error in redirect (%s): %s", pathReLookup, prob)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/`+pathReLookup+`" to ".*other.valid:\d+/path"`)), 1)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost: \[127.0.0.1\]`)), 1)
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid: \[127.0.0.1\]`)), 1)
 | 
						|
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, pathRedirectInvalidPort)
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	test.AssertNotNil(t, prob, "Problem details for pathRedirectInvalidPort should not be nil")
 | 
						|
	test.AssertEquals(t, prob.Detail, fmt.Sprintf(
 | 
						|
		"Fetching http://other.valid:8080/path: Invalid port in redirect target. "+
 | 
						|
			"Only ports %d and %d are supported, not 8080", va.httpPort, va.httpsPort))
 | 
						|
 | 
						|
	// This case will redirect from a valid host to a host that is throwing
 | 
						|
	// HTTP 500 errors. The test case is ensuring that the connection error
 | 
						|
	// is referencing the redirected to host, instead of the original host.
 | 
						|
	log.Clear()
 | 
						|
	setChallengeToken(&chall, pathRedirectToFailingURL)
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	test.AssertNotNil(t, prob, "Problem Details should not be nil")
 | 
						|
	test.AssertEquals(t, prob.Detail, fmt.Sprintf(
 | 
						|
		"Invalid response from http://localhost:%d/.well-known/acme-challenge/re-to-failing-url [127.0.0.1]: 500",
 | 
						|
		va.httpPort))
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPRedirectLoop(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, "looper")
 | 
						|
 | 
						|
	hs := httpSrv(t, expectedToken)
 | 
						|
	defer hs.Close()
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Challenge should have failed for %s", chall.Token)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestHTTPRedirectUserAgent(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, expectedToken)
 | 
						|
 | 
						|
	hs := httpSrv(t, expectedToken)
 | 
						|
	defer hs.Close()
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
	va.userAgent = rejectUserAgent
 | 
						|
 | 
						|
	setChallengeToken(&chall, pathMoved)
 | 
						|
	_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathMoved)
 | 
						|
	}
 | 
						|
 | 
						|
	setChallengeToken(&chall, pathFound)
 | 
						|
	_, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathFound)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func getPort(hs *httptest.Server) int {
 | 
						|
	url, err := url.Parse(hs.URL)
 | 
						|
	if err != nil {
 | 
						|
		panic(fmt.Sprintf("Failed to parse hs URL: %q - %s", hs.URL, err.Error()))
 | 
						|
	}
 | 
						|
	_, portString, err := net.SplitHostPort(url.Host)
 | 
						|
	if err != nil {
 | 
						|
		panic(fmt.Sprintf("Failed to split hs URL host: %q - %s", url.Host, err.Error()))
 | 
						|
	}
 | 
						|
	port, err := strconv.ParseInt(portString, 10, 64)
 | 
						|
	if err != nil {
 | 
						|
		panic(fmt.Sprintf("Failed to parse hs URL port: %q - %s", portString, err.Error()))
 | 
						|
	}
 | 
						|
	return int(port)
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSSNI01Success(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := tlssni01Srv(t, chall)
 | 
						|
	va, log := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Fatalf("Unexpected failure in validate TLS-SNI-01: %s", prob)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost: \[127.0.0.1\]`)), 1)
 | 
						|
	if len(log.GetAllMatching(`challenge for localhost received certificate \(1 of 1\): cert=\[`)) != 1 {
 | 
						|
		t.Errorf("Didn't get log message with validated certificate. Instead got:\n%s",
 | 
						|
			strings.Join(log.GetAllMatching(".*"), "\n"))
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSSNI01FailIP(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := tlssni01Srv(t, chall)
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	port := getPort(hs)
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, core.AcmeIdentifier{
 | 
						|
		Type:  core.IdentifierType("ip"),
 | 
						|
		Value: net.JoinHostPort("127.0.0.1", strconv.Itoa(port)),
 | 
						|
	}, chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("IdentifierType IP shouldn't have worked.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.MalformedProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSSNI01Invalid(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := tlssni01Srv(t, chall)
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Domain name was supposed to be invalid.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnknownHostProblem)
 | 
						|
	expected := "No valid IP addresses found for always.invalid"
 | 
						|
	if prob.Detail != expected {
 | 
						|
		t.Errorf("Got wrong error detail. Expected %q, got %q",
 | 
						|
			expected, prob.Detail)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func slowTLSSrv() *httptest.Server {
 | 
						|
	server := httptest.NewUnstartedServer(http.DefaultServeMux)
 | 
						|
	server.TLS = &tls.Config{
 | 
						|
		GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
 | 
						|
			time.Sleep(100 * time.Millisecond)
 | 
						|
			return makeACert([]string{"nomatter"}), nil
 | 
						|
		},
 | 
						|
	}
 | 
						|
	server.StartTLS()
 | 
						|
	return server
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSSNI01TimeoutAfterConnect(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := slowTLSSrv()
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	timeout := 50 * time.Millisecond
 | 
						|
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 | 
						|
	defer cancel()
 | 
						|
 | 
						|
	started := time.Now()
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, dnsi("slow.server"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Validation should've failed")
 | 
						|
	}
 | 
						|
	// Check that the TLS connection doesn't return before a timeout, and times
 | 
						|
	// out after the expected time
 | 
						|
	took := time.Since(started)
 | 
						|
	// Check that the HTTP connection doesn't return too fast, and times
 | 
						|
	// out after the expected time
 | 
						|
	if took < timeout/2 {
 | 
						|
		t.Fatalf("TLSSNI returned before %s (%s) with %#v", timeout, took, prob)
 | 
						|
	}
 | 
						|
	if took > 2*timeout {
 | 
						|
		t.Fatalf("TLSSNI didn't timeout after %s (took %s to return %#v)", timeout,
 | 
						|
			took, prob)
 | 
						|
	}
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Connection should've timed out")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
 | 
						|
	expected := "Timeout after connect (your server may be slow or overloaded)"
 | 
						|
	if prob.Detail != expected {
 | 
						|
		t.Errorf("Wrong error detail. Expected %q, got %q", expected, prob.Detail)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSSNI01DialTimeout(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := slowTLSSrv()
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
	va.dnsClient = dnsMockReturnsUnroutable{&bdns.MockDNSClient{}}
 | 
						|
	started := time.Now()
 | 
						|
 | 
						|
	timeout := 50 * time.Millisecond
 | 
						|
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 | 
						|
	defer cancel()
 | 
						|
 | 
						|
	// The only method I've found so far to trigger a connect timeout is to
 | 
						|
	// connect to an unrouteable IP address. This usuall generates a connection
 | 
						|
	// timeout, but will rarely return "Network unreachable" instead. If we get
 | 
						|
	// that, just retry until we get something other than "Network unreachable".
 | 
						|
	var prob *probs.ProblemDetails
 | 
						|
	for i := 0; i < 20; i++ {
 | 
						|
		_, prob = va.validateTLSSNI01(ctx, dnsi("unroutable.invalid"), chall)
 | 
						|
		if prob != nil && strings.Contains(prob.Detail, "Network unreachable") {
 | 
						|
			continue
 | 
						|
		} else {
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Validation should've failed")
 | 
						|
	}
 | 
						|
	// Check that the TLS connection doesn't return before a timeout, and times
 | 
						|
	// out after the expected time
 | 
						|
	took := time.Since(started)
 | 
						|
	// Check that the HTTP connection doesn't return too fast, and times
 | 
						|
	// out after the expected time
 | 
						|
	if took < timeout/2 {
 | 
						|
		t.Fatalf("TLSSNI returned before %s (%s) with %#v", timeout, took, prob)
 | 
						|
	}
 | 
						|
	if took > 2*timeout {
 | 
						|
		t.Fatalf("TLSSNI didn't timeout after %s", timeout)
 | 
						|
	}
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Connection should've timed out")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
 | 
						|
	expected := "Timeout during connect (likely firewall problem)"
 | 
						|
	if prob.Detail != expected {
 | 
						|
		t.Errorf("Wrong error detail. Expected %q, got %q", expected, prob.Detail)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSSNI01InvalidResponse(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := tlssni01Srv(t, chall)
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	differentChall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	differentChall.ProvidedKeyAuthorization = "invalid.keyAuthorization"
 | 
						|
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), differentChall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Validation should've failed")
 | 
						|
	}
 | 
						|
	expected := "Incorrect validation certificate for tls-sni-01 challenge."
 | 
						|
	if !strings.HasPrefix(prob.Detail, expected) {
 | 
						|
		t.Errorf("Wrong error detail. Expected %q, got %q", expected, prob.Detail)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSSNI01Refused(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := tlssni01Srv(t, chall)
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
	// Take down validation server and check that validation fails.
 | 
						|
	hs.Close()
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Server's down; expected refusal. Where did we connect?")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSSNI01TalkingToHTTP(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := tlssni01Srv(t, chall)
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
	httpOnly := httpSrv(t, "")
 | 
						|
	va.tlsPort = getPort(httpOnly)
 | 
						|
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
 | 
						|
	test.AssertError(t, prob, "TLS-SNI-01 validation passed when talking to a HTTP-only server")
 | 
						|
	expected := "Server only speaks HTTP, not TLS"
 | 
						|
	if !strings.HasSuffix(prob.Detail, expected) {
 | 
						|
		t.Errorf("Got wrong error detail. Expected %q, got %q", expected, prob.Detail)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func brokenTLSSrv() *httptest.Server {
 | 
						|
	server := httptest.NewUnstartedServer(http.DefaultServeMux)
 | 
						|
	server.TLS = &tls.Config{
 | 
						|
		GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
 | 
						|
			return nil, fmt.Errorf("Failing on purpose")
 | 
						|
		},
 | 
						|
	}
 | 
						|
	server.StartTLS()
 | 
						|
	return server
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSError(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := brokenTLSSrv()
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("TLS validation should have failed: What cert was used?")
 | 
						|
	}
 | 
						|
	if prob.Type != probs.TLSProblem {
 | 
						|
		t.Errorf("Wrong problem type: got %s, expected type %s",
 | 
						|
			prob, probs.TLSProblem)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// misconfiguredTLSSrv is a TLS HTTP test server that returns a certificate
 | 
						|
// chain with more than one cert, none of which will solve a TLS SNI challenge
 | 
						|
func misconfiguredTLSSrv() *httptest.Server {
 | 
						|
	template := &x509.Certificate{
 | 
						|
		SerialNumber:          big.NewInt(1337),
 | 
						|
		NotBefore:             time.Now(),
 | 
						|
		NotAfter:              time.Now().AddDate(0, 0, 1),
 | 
						|
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
 | 
						|
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
 | 
						|
		BasicConstraintsValid: true,
 | 
						|
 | 
						|
		Subject: pkix.Name{
 | 
						|
			CommonName: "hello.world",
 | 
						|
		},
 | 
						|
		DNSNames: []string{"goodbye.world", "hello.world"},
 | 
						|
	}
 | 
						|
 | 
						|
	certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
 | 
						|
	cert := &tls.Certificate{
 | 
						|
		Certificate: [][]byte{certBytes, certBytes},
 | 
						|
		PrivateKey:  &TheKey,
 | 
						|
	}
 | 
						|
 | 
						|
	server := httptest.NewUnstartedServer(http.DefaultServeMux)
 | 
						|
	server.TLS = &tls.Config{
 | 
						|
		Certificates: []tls.Certificate{*cert},
 | 
						|
	}
 | 
						|
	server.StartTLS()
 | 
						|
	return server
 | 
						|
}
 | 
						|
 | 
						|
func TestCertNames(t *testing.T) {
 | 
						|
	// We duplicate names inside the SAN set
 | 
						|
	names := []string{
 | 
						|
		"hello.world", "goodbye.world",
 | 
						|
		"hello.world", "goodbye.world",
 | 
						|
		"bonjour.le.monde", "au.revoir.le.monde",
 | 
						|
		"bonjour.le.monde", "au.revoir.le.monde",
 | 
						|
	}
 | 
						|
	// We expect only unique names, in sorted order
 | 
						|
	expected := []string{
 | 
						|
		"au.revoir.le.monde", "bonjour.le.monde",
 | 
						|
		"goodbye.world", "hello.world",
 | 
						|
	}
 | 
						|
	template := &x509.Certificate{
 | 
						|
		SerialNumber:          big.NewInt(1337),
 | 
						|
		NotBefore:             time.Now(),
 | 
						|
		NotAfter:              time.Now().AddDate(0, 0, 1),
 | 
						|
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
 | 
						|
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
 | 
						|
		BasicConstraintsValid: true,
 | 
						|
 | 
						|
		Subject: pkix.Name{
 | 
						|
			// We also duplicate a name from the SANs as the CN
 | 
						|
			CommonName: names[0],
 | 
						|
		},
 | 
						|
		DNSNames: names,
 | 
						|
	}
 | 
						|
 | 
						|
	// Create the certificate, check that certNames provides the expected result
 | 
						|
	certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
 | 
						|
	cert, _ := x509.ParseCertificate(certBytes)
 | 
						|
	actual := certNames(cert)
 | 
						|
	test.AssertDeepEquals(t, actual, expected)
 | 
						|
}
 | 
						|
 | 
						|
// TestSNIErrInvalidChain sets up a TLS server with two certificates, neither of
 | 
						|
// which validate the SNI challenge.
 | 
						|
func TestSNIErrInvalidChain(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := misconfiguredTLSSrv()
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	// Validate the SNI challenge with the test server, expecting it to fail
 | 
						|
	_, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("TLS validation should have failed")
 | 
						|
	}
 | 
						|
 | 
						|
	// We expect that the error message will say 2 certificates were received, and
 | 
						|
	// we expect the error to contain a deduplicated list of domain names from the
 | 
						|
	// subject CN and SANs of the leaf cert
 | 
						|
	expected := "Received 2 certificate(s), first certificate had names \"goodbye.world, hello.world\""
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
 | 
						|
	test.AssertContains(t, prob.Detail, expected)
 | 
						|
}
 | 
						|
 | 
						|
func TestValidateHTTP(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, core.NewToken())
 | 
						|
 | 
						|
	hs := httpSrv(t, chall.Token)
 | 
						|
	defer hs.Close()
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
 | 
						|
	test.Assert(t, prob == nil, "validation failed")
 | 
						|
}
 | 
						|
 | 
						|
func TestGSBAtValidation(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, core.NewToken())
 | 
						|
 | 
						|
	hs := httpSrv(t, chall.Token)
 | 
						|
	defer hs.Close()
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	ctrl := gomock.NewController(t)
 | 
						|
	sbc := NewMockSafeBrowsing(ctrl)
 | 
						|
	sbc.EXPECT().IsListed(gomock.Any(), "good.com").Return("", nil)
 | 
						|
	sbc.EXPECT().IsListed(gomock.Any(), "bad.com").Return("bad", nil)
 | 
						|
	sbc.EXPECT().IsListed(gomock.Any(), "errorful.com").Return("", fmt.Errorf("welp"))
 | 
						|
	va.safeBrowsing = sbc
 | 
						|
 | 
						|
	_, prob := va.validate(ctx, dnsi("bad.com"), chall, core.Authorization{})
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Expected rejection for bad.com, got success")
 | 
						|
	}
 | 
						|
	if !strings.Contains(prob.Error(), "unsafe domain") {
 | 
						|
		t.Errorf("Got error %q, expected an unsafe domain error.", prob.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	_, prob = va.validate(ctx, dnsi("errorful.com"), chall, core.Authorization{})
 | 
						|
	if prob != nil {
 | 
						|
		t.Fatalf("Expected success for errorful.com, got error")
 | 
						|
	}
 | 
						|
 | 
						|
	_, prob = va.validate(ctx, dnsi("good.com"), chall, core.Authorization{})
 | 
						|
	if prob != nil {
 | 
						|
		t.Fatalf("Expected success for good.com, got %s", prob)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// challengeType == "tls-sni-00" or "dns-00", since they're the same
 | 
						|
func createChallenge(challengeType string) core.Challenge {
 | 
						|
	chall := core.Challenge{
 | 
						|
		Type:                     challengeType,
 | 
						|
		Status:                   core.StatusPending,
 | 
						|
		Token:                    expectedToken,
 | 
						|
		ValidationRecord:         []core.ValidationRecord{},
 | 
						|
		ProvidedKeyAuthorization: expectedKeyAuthorization,
 | 
						|
	}
 | 
						|
 | 
						|
	return chall
 | 
						|
}
 | 
						|
 | 
						|
// setChallengeToken sets the token value, and sets the ProvidedKeyAuthorization
 | 
						|
// to match.
 | 
						|
func setChallengeToken(ch *core.Challenge, token string) {
 | 
						|
	ch.Token = token
 | 
						|
	ch.ProvidedKeyAuthorization = token + ".9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI"
 | 
						|
}
 | 
						|
 | 
						|
func TestValidateTLSSNI01(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := tlssni01Srv(t, chall)
 | 
						|
	defer hs.Close()
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
 | 
						|
 | 
						|
	test.Assert(t, prob == nil, "validation failed")
 | 
						|
}
 | 
						|
 | 
						|
func TestValidateTLSSNI01NotSane(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
 | 
						|
	chall.Token = "not sane"
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
 | 
						|
 | 
						|
	test.AssertEquals(t, prob.Type, probs.MalformedProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestValidateTLSALPN01(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSALPN01)
 | 
						|
	hs := tlsalpn01Srv(t, chall, IdPeAcmeIdentifier, "localhost")
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Errorf("Validation failed: %v", prob)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, test.CountCounterVec("oid", IdPeAcmeIdentifier.String(), va.metrics.tlsALPNOIDCounter), 1)
 | 
						|
 | 
						|
	hs.Close()
 | 
						|
	chall = createChallenge(core.ChallengeTypeTLSALPN01)
 | 
						|
	hs = tlsalpn01Srv(t, chall, IdPeAcmeIdentifierV1Obsolete, "localhost")
 | 
						|
 | 
						|
	va, _ = setup(hs, 0)
 | 
						|
 | 
						|
	_, prob = va.validateChallenge(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob != nil {
 | 
						|
		t.Errorf("Validation failed: %v", prob)
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, test.CountCounterVec("oid", IdPeAcmeIdentifierV1Obsolete.String(), va.metrics.tlsALPNOIDCounter), 1)
 | 
						|
}
 | 
						|
 | 
						|
func TestValidateTLSALPN01BadChallenge(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSALPN01)
 | 
						|
	chall2 := chall
 | 
						|
	setChallengeToken(&chall2, "bad token")
 | 
						|
 | 
						|
	hs := tlsalpn01Srv(t, chall2, IdPeAcmeIdentifier, "localhost")
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateTLSALPN01(ctx, dnsi("localhost"), chall)
 | 
						|
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("TLS ALPN validation should have failed.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestValidateTLSALPN01BrokenSrv(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := brokenTLSSrv()
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateTLSALPN01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("TLS ALPN validation should have failed.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.TLSProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestValidateTLSALPN01UnawareSrv(t *testing.T) {
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
	hs := tlssniSrvWithNames(t, chall, "localhost")
 | 
						|
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	_, prob := va.validateTLSALPN01(ctx, dnsi("localhost"), chall)
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("TLS ALPN validation should have failed.")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestPerformValidationInvalid(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chalDNS := createChallenge(core.ChallengeTypeDNS01)
 | 
						|
	_, prob := va.PerformValidation(context.Background(), "foo.com", chalDNS, core.Authorization{})
 | 
						|
	test.Assert(t, prob != nil, "validation succeeded")
 | 
						|
 | 
						|
	samples := test.CountHistogramSamples(va.metrics.validationTime.With(prometheus.Labels{
 | 
						|
		"type":        "dns-01",
 | 
						|
		"result":      "invalid",
 | 
						|
		"problemType": "unauthorized",
 | 
						|
	}))
 | 
						|
	if samples != 1 {
 | 
						|
		t.Errorf("Wrong number of samples for invalid validation. Expected 1, got %d", samples)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationEmpty(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chalDNS := createChallenge(core.ChallengeTypeDNS01)
 | 
						|
	_, prob := va.PerformValidation(
 | 
						|
		context.Background(),
 | 
						|
		"empty-txts.com",
 | 
						|
		chalDNS,
 | 
						|
		core.Authorization{})
 | 
						|
	test.AssertEquals(t, prob.Error(), "unauthorized :: No TXT record found at _acme-challenge.empty-txts.com")
 | 
						|
 | 
						|
	samples := test.CountHistogramSamples(va.metrics.validationTime.With(prometheus.Labels{
 | 
						|
		"type":        "dns-01",
 | 
						|
		"result":      "invalid",
 | 
						|
		"problemType": "unauthorized",
 | 
						|
	}))
 | 
						|
	if samples != 1 {
 | 
						|
		t.Errorf("Wrong number of samples for invalid validation. Expected 1, got %d", samples)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationWrong(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chalDNS := createChallenge(core.ChallengeTypeDNS01)
 | 
						|
	_, prob := va.PerformValidation(
 | 
						|
		context.Background(),
 | 
						|
		"wrong-dns01.com",
 | 
						|
		chalDNS,
 | 
						|
		core.Authorization{})
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Successful DNS validation with wrong TXT record")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"a\" found at _acme-challenge.wrong-dns01.com")
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationWrongMany(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chalDNS := createChallenge(core.ChallengeTypeDNS01)
 | 
						|
	_, prob := va.PerformValidation(
 | 
						|
		context.Background(),
 | 
						|
		"wrong-many-dns01.com",
 | 
						|
		chalDNS,
 | 
						|
		core.Authorization{})
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Successful DNS validation with wrong TXT record")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"a\" (and 4 more) found at _acme-challenge.wrong-many-dns01.com")
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationWrongLong(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chalDNS := createChallenge(core.ChallengeTypeDNS01)
 | 
						|
	_, prob := va.PerformValidation(
 | 
						|
		context.Background(),
 | 
						|
		"long-dns01.com",
 | 
						|
		chalDNS,
 | 
						|
		core.Authorization{})
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("Successful DNS validation with wrong TXT record")
 | 
						|
	}
 | 
						|
	test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\" found at _acme-challenge.long-dns01.com")
 | 
						|
}
 | 
						|
 | 
						|
func TestPerformValidationValid(t *testing.T) {
 | 
						|
	va, mockLog := setup(nil, 0)
 | 
						|
 | 
						|
	// create a challenge with well known token
 | 
						|
	chalDNS := core.DNSChallenge01()
 | 
						|
	chalDNS.Token = expectedToken
 | 
						|
	chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
 | 
						|
	_, prob := va.PerformValidation(context.Background(), "good-dns01.com", chalDNS, core.Authorization{})
 | 
						|
	test.Assert(t, prob == nil, fmt.Sprintf("validation failed: %#v", prob))
 | 
						|
 | 
						|
	samples := test.CountHistogramSamples(va.metrics.validationTime.With(prometheus.Labels{
 | 
						|
		"type":        "dns-01",
 | 
						|
		"result":      "valid",
 | 
						|
		"problemType": "",
 | 
						|
	}))
 | 
						|
	if samples != 1 {
 | 
						|
		t.Errorf("Wrong number of samples for successful validation. Expected 1, got %d", samples)
 | 
						|
	}
 | 
						|
	resultLog := mockLog.GetAllMatching(`Validation result`)
 | 
						|
	if len(resultLog) != 1 {
 | 
						|
		t.Fatalf("Wrong number of matching lines for 'Validation result'")
 | 
						|
	}
 | 
						|
	if !strings.Contains(resultLog[0], `"Hostname":"good-dns01.com"`) {
 | 
						|
		t.Errorf("PerformValidation didn't log validation hostname.")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestPerformValidationWildcard tests that the VA properly strips the `*.`
 | 
						|
// prefix from a wildcard name provided to the PerformValidation function.
 | 
						|
func TestPerformValidationWildcard(t *testing.T) {
 | 
						|
	va, mockLog := setup(nil, 0)
 | 
						|
 | 
						|
	// create a challenge with well known token
 | 
						|
	chalDNS := core.DNSChallenge01()
 | 
						|
	chalDNS.Token = expectedToken
 | 
						|
	chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
 | 
						|
	// perform a validation for a wildcard name
 | 
						|
	_, prob := va.PerformValidation(context.Background(), "*.good-dns01.com", chalDNS, core.Authorization{})
 | 
						|
	test.Assert(t, prob == nil, fmt.Sprintf("validation failed: %#v", prob))
 | 
						|
 | 
						|
	samples := test.CountHistogramSamples(va.metrics.validationTime.With(prometheus.Labels{
 | 
						|
		"type":        "dns-01",
 | 
						|
		"result":      "valid",
 | 
						|
		"problemType": "",
 | 
						|
	}))
 | 
						|
	if samples != 1 {
 | 
						|
		t.Errorf("Wrong number of samples for successful validation. Expected 1, got %d", samples)
 | 
						|
	}
 | 
						|
	resultLog := mockLog.GetAllMatching(`Validation result`)
 | 
						|
	if len(resultLog) != 1 {
 | 
						|
		t.Fatalf("Wrong number of matching lines for 'Validation result'")
 | 
						|
	}
 | 
						|
 | 
						|
	// We expect that the top level Hostname reflect the wildcard name
 | 
						|
	if !strings.Contains(resultLog[0], `"Hostname":"*.good-dns01.com"`) {
 | 
						|
		t.Errorf("PerformValidation didn't log correct validation hostname.")
 | 
						|
	}
 | 
						|
	// We expect that the ValidationRecord contain the correct non-wildcard
 | 
						|
	// hostname that was validated
 | 
						|
	if !strings.Contains(resultLog[0], `"hostname":"good-dns01.com"`) {
 | 
						|
		t.Errorf("PerformValidation didn't log correct validation record hostname.")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationFailure(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chalDNS := createChallenge(core.ChallengeTypeDNS01)
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("localhost"), chalDNS)
 | 
						|
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationInvalid(t *testing.T) {
 | 
						|
	var notDNS = core.AcmeIdentifier{
 | 
						|
		Type:  core.IdentifierType("iris"),
 | 
						|
		Value: "790DB180-A274-47A4-855F-31C428CB1072",
 | 
						|
	}
 | 
						|
 | 
						|
	chalDNS := core.DNSChallenge01()
 | 
						|
	chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
 | 
						|
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, notDNS, chalDNS)
 | 
						|
 | 
						|
	test.AssertEquals(t, prob.Type, probs.MalformedProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationNotSane(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chal0 := core.DNSChallenge01()
 | 
						|
	chal0.Token = ""
 | 
						|
 | 
						|
	chal1 := core.DNSChallenge01()
 | 
						|
	chal1.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_"
 | 
						|
 | 
						|
	chal2 := core.DNSChallenge01()
 | 
						|
	chal2.ProvidedKeyAuthorization = "a"
 | 
						|
 | 
						|
	var authz = core.Authorization{
 | 
						|
		ID:             core.NewToken(),
 | 
						|
		RegistrationID: 1,
 | 
						|
		Identifier:     dnsi("localhost"),
 | 
						|
		Challenges:     []core.Challenge{chal0, chal1, chal2},
 | 
						|
	}
 | 
						|
 | 
						|
	for i := 0; i < len(authz.Challenges); i++ {
 | 
						|
		_, prob := va.validateChallenge(ctx, dnsi("localhost"), authz.Challenges[i])
 | 
						|
		if prob.Type != probs.MalformedProblem {
 | 
						|
			t.Errorf("Got wrong error type for %d: expected %s, got %s",
 | 
						|
				i, prob.Type, probs.MalformedProblem)
 | 
						|
		}
 | 
						|
		if !strings.Contains(prob.Error(), "Challenge failed consistency check:") {
 | 
						|
			t.Errorf("Got wrong error: %s", prob.Error())
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationServFail(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	chalDNS := createChallenge(core.ChallengeTypeDNS01)
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("servfail.com"), chalDNS)
 | 
						|
 | 
						|
	test.AssertEquals(t, prob.Type, probs.DNSProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationNoServer(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
	va.dnsClient = bdns.NewTestDNSClientImpl(
 | 
						|
		time.Second*5,
 | 
						|
		nil,
 | 
						|
		metrics.NewNoopScope(),
 | 
						|
		clock.Default(),
 | 
						|
		1)
 | 
						|
 | 
						|
	chalDNS := createChallenge(core.ChallengeTypeDNS01)
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("localhost"), chalDNS)
 | 
						|
 | 
						|
	test.AssertEquals(t, prob.Type, probs.DNSProblem)
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationOK(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	// create a challenge with well known token
 | 
						|
	chalDNS := core.DNSChallenge01()
 | 
						|
	chalDNS.Token = expectedToken
 | 
						|
	chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("good-dns01.com"), chalDNS)
 | 
						|
 | 
						|
	test.Assert(t, prob == nil, "Should be valid.")
 | 
						|
}
 | 
						|
 | 
						|
func TestDNSValidationNoAuthorityOK(t *testing.T) {
 | 
						|
	va, _ := setup(nil, 0)
 | 
						|
 | 
						|
	// create a challenge with well known token
 | 
						|
	chalDNS := core.DNSChallenge01()
 | 
						|
	chalDNS.Token = expectedToken
 | 
						|
 | 
						|
	chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("no-authority-dns01.com"), chalDNS)
 | 
						|
 | 
						|
	test.Assert(t, prob == nil, "Should be valid.")
 | 
						|
}
 | 
						|
 | 
						|
func TestLimitedReader(t *testing.T) {
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, core.NewToken())
 | 
						|
 | 
						|
	hs := httpSrv(t, "012345\xff67890123456789012345678901234567890123456789012345678901234567890123456789")
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
	defer hs.Close()
 | 
						|
 | 
						|
	_, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
 | 
						|
 | 
						|
	test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
 | 
						|
	test.Assert(t, strings.HasPrefix(prob.Detail, "Invalid response from "),
 | 
						|
		"Expected failure due to truncation")
 | 
						|
 | 
						|
	if !utf8.ValidString(prob.Detail) {
 | 
						|
		t.Errorf("Problem Detail contained an invalid UTF-8 string")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func setup(srv *httptest.Server, maxRemoteFailures int) (*ValidationAuthorityImpl, *blog.Mock) {
 | 
						|
	logger := blog.NewMock()
 | 
						|
 | 
						|
	var portConfig cmd.PortConfig
 | 
						|
	if srv != nil {
 | 
						|
		port := getPort(srv)
 | 
						|
		portConfig = cmd.PortConfig{
 | 
						|
			HTTPPort: port,
 | 
						|
			TLSPort:  port,
 | 
						|
		}
 | 
						|
	}
 | 
						|
	va, err := NewValidationAuthorityImpl(
 | 
						|
		// Use the test server's port as both the HTTPPort and the TLSPort for the VA
 | 
						|
		&portConfig,
 | 
						|
		nil,
 | 
						|
		&bdns.MockDNSClient{},
 | 
						|
		nil,
 | 
						|
		maxRemoteFailures,
 | 
						|
		"user agent 1.0",
 | 
						|
		"letsencrypt.org",
 | 
						|
		metrics.NewNoopScope(),
 | 
						|
		clock.Default(),
 | 
						|
		logger,
 | 
						|
		accountURIPrefixes)
 | 
						|
	if err != nil {
 | 
						|
		panic(fmt.Sprintf("Failed to create validation authority: %v", err))
 | 
						|
	}
 | 
						|
	return va, logger
 | 
						|
}
 | 
						|
 | 
						|
func TestAvailableAddresses(t *testing.T) {
 | 
						|
	v6a := net.ParseIP("::1")
 | 
						|
	v6b := net.ParseIP("2001:db8::2:1") // 2001:DB8 is reserved for docs (RFC 3849)
 | 
						|
	v4a := net.ParseIP("127.0.0.1")
 | 
						|
	v4b := net.ParseIP("192.0.2.1") // 192.0.2.0/24 is reserved for docs (RFC 5737)
 | 
						|
 | 
						|
	testcases := []struct {
 | 
						|
		input []net.IP
 | 
						|
		v4    []net.IP
 | 
						|
		v6    []net.IP
 | 
						|
	}{
 | 
						|
		// An empty validation record
 | 
						|
		{
 | 
						|
			[]net.IP{},
 | 
						|
			[]net.IP{},
 | 
						|
			[]net.IP{},
 | 
						|
		},
 | 
						|
		// A validation record with one IPv4 address
 | 
						|
		{
 | 
						|
			[]net.IP{v4a},
 | 
						|
			[]net.IP{v4a},
 | 
						|
			[]net.IP{},
 | 
						|
		},
 | 
						|
		// A dual homed record with an IPv4 and IPv6 address
 | 
						|
		{
 | 
						|
			[]net.IP{v4a, v6a},
 | 
						|
			[]net.IP{v4a},
 | 
						|
			[]net.IP{v6a},
 | 
						|
		},
 | 
						|
		// The same as above but with the v4/v6 order flipped
 | 
						|
		{
 | 
						|
			[]net.IP{v6a, v4a},
 | 
						|
			[]net.IP{v4a},
 | 
						|
			[]net.IP{v6a},
 | 
						|
		},
 | 
						|
		// A validation record with just IPv6 addresses
 | 
						|
		{
 | 
						|
			[]net.IP{v6a, v6b},
 | 
						|
			[]net.IP{},
 | 
						|
			[]net.IP{v6a, v6b},
 | 
						|
		},
 | 
						|
		// A validation record with interleaved IPv4/IPv6 records
 | 
						|
		{
 | 
						|
			[]net.IP{v6a, v4a, v6b, v4b},
 | 
						|
			[]net.IP{v4a, v4b},
 | 
						|
			[]net.IP{v6a, v6b},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range testcases {
 | 
						|
		// Split the input record into v4/v6 addresses
 | 
						|
		v4result, v6result := availableAddresses(tc.input)
 | 
						|
 | 
						|
		// Test that we got the right number of v4 results
 | 
						|
		test.Assert(t, len(tc.v4) == len(v4result),
 | 
						|
			fmt.Sprintf("Wrong # of IPv4 results: expected %d, got %d", len(tc.v4), len(v4result)))
 | 
						|
 | 
						|
		// Check that all of the v4 results match expected values
 | 
						|
		for i, v4addr := range tc.v4 {
 | 
						|
			test.Assert(t, v4addr.String() == v4result[i].String(),
 | 
						|
				fmt.Sprintf("Wrong v4 result index %d: expected %q got %q", i, v4addr.String(), v4result[i].String()))
 | 
						|
		}
 | 
						|
 | 
						|
		// Test that we got the right number of v6 results
 | 
						|
		test.Assert(t, len(tc.v6) == len(v6result),
 | 
						|
			fmt.Sprintf("Wrong # of IPv6 results: expected %d, got %d", len(tc.v6), len(v6result)))
 | 
						|
 | 
						|
		// Check that all of the v6 results match expected values
 | 
						|
		for i, v6addr := range tc.v6 {
 | 
						|
			test.Assert(t, v6addr.String() == v6result[i].String(),
 | 
						|
				fmt.Sprintf("Wrong v6 result index %d: expected %q got %q", i, v6addr.String(), v6result[i].String()))
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestHTTP01DialerFallback tests the underlying dialer used by HTTP01
 | 
						|
// challenges. In particular it ensures that both the first IPv6 request and the
 | 
						|
// subsequent IPv4 request get a new dialer each.
 | 
						|
func TestHTTP01DialerFallback(t *testing.T) {
 | 
						|
	// Create a new challenge to use for the httpSrv
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, core.NewToken())
 | 
						|
 | 
						|
	// Create an IPv4 test server
 | 
						|
	hs := httpSrv(t, chall.Token)
 | 
						|
	defer hs.Close()
 | 
						|
 | 
						|
	// Create a test VA
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	// Create a test dialer for the dual homed host. There is only an IPv4 httpSrv
 | 
						|
	// so the IPv6 address returned in the AAAA record will always fail.
 | 
						|
	addrs, _ := va.getAddrs(context.Background(), "ipv4.and.ipv6.localhost")
 | 
						|
	d := va.newHTTP01Dialer("ipv4.and.ipv6.localhost", va.httpPort, addrs)
 | 
						|
 | 
						|
	// Try to dial the dialer
 | 
						|
	_, dialProb := d.DialContext(context.Background(), "", "ipv4.and.ipv6.localhost")
 | 
						|
 | 
						|
	// There shouldn't be a problem from this dial
 | 
						|
	test.AssertEquals(t, dialProb, nil)
 | 
						|
 | 
						|
	// We should have constructed two inner dialers, one for each connection
 | 
						|
	test.AssertEquals(t, d.dialerCount, 2)
 | 
						|
 | 
						|
	// We expect one validation record to be present
 | 
						|
	test.Assert(t, len(d.addrInfoChan) == 1, "there should be one address info struct in the dialer.addrInfoChan chan")
 | 
						|
	addrInfo := <-d.addrInfoChan
 | 
						|
	// We expect that the address used was the IPv4 localhost address
 | 
						|
	test.AssertEquals(t, addrInfo.used.String(), "127.0.0.1")
 | 
						|
	// We expect that one address was tried before the address used
 | 
						|
	test.AssertEquals(t, len(addrInfo.tried), 1)
 | 
						|
	// We expect that IPv6 address was tried before the address used
 | 
						|
	test.AssertEquals(t, addrInfo.tried[0].String(), "::1")
 | 
						|
}
 | 
						|
 | 
						|
func TestFallbackDialer(t *testing.T) {
 | 
						|
	// Create a new challenge to use for the httpSrv
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, core.NewToken())
 | 
						|
 | 
						|
	// Create an IPv4 test server
 | 
						|
	hs := httpSrv(t, chall.Token)
 | 
						|
	defer hs.Close()
 | 
						|
 | 
						|
	// Create a test VA
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	ctrl := gomock.NewController(t)
 | 
						|
	defer ctrl.Finish()
 | 
						|
	scope := mock_metrics.NewMockScope(ctrl)
 | 
						|
	va.stats = scope
 | 
						|
 | 
						|
	// We expect the IPV4 Fallback stat to be incremented
 | 
						|
	scope.EXPECT().Inc("IPv4Fallback", int64(1))
 | 
						|
 | 
						|
	// The validation is expected to succeed even though the V6 server
 | 
						|
	// doesn't exist because we fallback to the IPv4 address.
 | 
						|
	ident := dnsi("ipv4.and.ipv6.localhost")
 | 
						|
	records, prob := va.validateChallenge(ctx, ident, chall)
 | 
						|
	test.Assert(t, prob == nil, "validation failed with IPv6 fallback to IPv4")
 | 
						|
	// We expect one validation record to be present
 | 
						|
	test.AssertEquals(t, len(records), 1)
 | 
						|
	// We expect that the address used was the IPv4 localhost address
 | 
						|
	test.AssertEquals(t, records[0].AddressUsed.String(), "127.0.0.1")
 | 
						|
	// We expect that one address was tried before the address used
 | 
						|
	test.AssertEquals(t, len(records[0].AddressesTried), 1)
 | 
						|
	// We expect that IPv6 address was tried before the address used
 | 
						|
	test.AssertEquals(t, records[0].AddressesTried[0].String(), "::1")
 | 
						|
}
 | 
						|
 | 
						|
func TestFallbackTLS(t *testing.T) {
 | 
						|
	// Create a new challenge to use for the httpSrv
 | 
						|
	chall := createChallenge(core.ChallengeTypeTLSSNI01)
 | 
						|
 | 
						|
	// Create a TLS SNI 01 test server, this will be bound on 127.0.0.1 (e.g. IPv4
 | 
						|
	// only!)
 | 
						|
	hs := tlssni01Srv(t, chall)
 | 
						|
	defer hs.Close()
 | 
						|
 | 
						|
	// Create a test VA
 | 
						|
	va, _ := setup(hs, 0)
 | 
						|
 | 
						|
	ctrl := gomock.NewController(t)
 | 
						|
	defer ctrl.Finish()
 | 
						|
	scope := mock_metrics.NewMockScope(ctrl)
 | 
						|
	va.stats = scope
 | 
						|
 | 
						|
	// We expect the IPV4 Fallback stat to be incremented
 | 
						|
	scope.EXPECT().Inc("IPv4Fallback", int64(1))
 | 
						|
 | 
						|
	// The validation is expected to succeed  by the fallback to the IPv4 address
 | 
						|
	// that has a test server waiting
 | 
						|
	ident := dnsi("ipv4.and.ipv6.localhost")
 | 
						|
	records, prob := va.validateChallenge(ctx, ident, chall)
 | 
						|
	test.Assert(t, prob == nil, "validation failed with IPv6 fallback to IPv4")
 | 
						|
	// We expect one validation record to be present
 | 
						|
	test.AssertEquals(t, len(records), 1)
 | 
						|
	// We expect that the address eventually used was the IPv4 localhost address
 | 
						|
	test.AssertEquals(t, records[0].AddressUsed.String(), "127.0.0.1")
 | 
						|
	// We expect that one address was tried before the address used
 | 
						|
	test.AssertEquals(t, len(records[0].AddressesTried), 1)
 | 
						|
	// We expect that IPv6 localhost address was tried before the address used
 | 
						|
	test.AssertEquals(t, records[0].AddressesTried[0].String(), "::1")
 | 
						|
 | 
						|
	// Now try a validation for an IPv6 only host. E.g. one without an IPv4
 | 
						|
	// address. The IPv6 will fail without a server and we expect the overall
 | 
						|
	// validation to fail since there is no IPv4 address/listener to fall back to.
 | 
						|
	ident = dnsi("ipv6.localhost")
 | 
						|
	va.stats = metrics.NewNoopScope()
 | 
						|
	records, prob = va.validateChallenge(ctx, ident, chall)
 | 
						|
 | 
						|
	// The validation is expected to fail since there is no IPv4 to fall back to
 | 
						|
	// and a broken IPv6
 | 
						|
	records, prob = va.validateChallenge(ctx, ident, chall)
 | 
						|
	test.Assert(t, prob != nil, "validation succeeded with broken IPv6 and no IPv4 fallback")
 | 
						|
	// We expect that the problem has the correct error message about nothing to fallback to
 | 
						|
	test.AssertEquals(t, prob.Detail,
 | 
						|
		"Unable to contact \"ipv6.localhost\" at \"::1\", no IPv4 addresses to try as fallback")
 | 
						|
	// We expect one validation record to be present
 | 
						|
	test.AssertEquals(t, len(records), 1)
 | 
						|
	// We expect that the address eventually used was the IPv6 localhost address
 | 
						|
	test.AssertEquals(t, records[0].AddressUsed.String(), "::1")
 | 
						|
	// We expect that one address was tried
 | 
						|
	test.AssertEquals(t, len(records[0].AddressesTried), 1)
 | 
						|
	// We expect that IPv6 localhost address was tried
 | 
						|
	test.AssertEquals(t, records[0].AddressesTried[0].String(), "::1")
 | 
						|
}
 | 
						|
 | 
						|
type multiSrv struct {
 | 
						|
	*httptest.Server
 | 
						|
 | 
						|
	mu         sync.Mutex
 | 
						|
	allowedUAs map[string]struct{}
 | 
						|
}
 | 
						|
 | 
						|
func httpMultiSrv(t *testing.T, token string, allowedUAs map[string]struct{}) *multiSrv {
 | 
						|
	m := http.NewServeMux()
 | 
						|
 | 
						|
	server := httptest.NewUnstartedServer(m)
 | 
						|
	ms := &multiSrv{server, sync.Mutex{}, allowedUAs}
 | 
						|
 | 
						|
	m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		if r.UserAgent() == "slow remote" {
 | 
						|
			time.Sleep(time.Second * 5)
 | 
						|
		}
 | 
						|
		ms.mu.Lock()
 | 
						|
		defer ms.mu.Unlock()
 | 
						|
		if _, ok := ms.allowedUAs[r.UserAgent()]; ok {
 | 
						|
			ch := core.Challenge{Token: token}
 | 
						|
			keyAuthz, _ := ch.ExpectedKeyAuthorization(accountKey)
 | 
						|
			t.Logf("HTTPSRV: Key Authz = '%s%s'\n", keyAuthz, "\\n\\r \\t")
 | 
						|
			fmt.Fprint(w, keyAuthz, "\n\r \t")
 | 
						|
		} else {
 | 
						|
			fmt.Fprint(w, "???")
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	ms.Start()
 | 
						|
	return ms
 | 
						|
}
 | 
						|
 | 
						|
// cancelledVA is a mock that always returns context.Canceled for
 | 
						|
// PerformValidation or IsSafeDomain calls
 | 
						|
type cancelledVA struct{}
 | 
						|
 | 
						|
func (v cancelledVA) PerformValidation(_ context.Context, _ string, _ core.Challenge, _ core.Authorization) ([]core.ValidationRecord, error) {
 | 
						|
	return nil, context.Canceled
 | 
						|
}
 | 
						|
 | 
						|
func (v cancelledVA) IsSafeDomain(_ context.Context, _ *vaPB.IsSafeDomainRequest) (*vaPB.IsDomainSafe, error) {
 | 
						|
	return nil, context.Canceled
 | 
						|
}
 | 
						|
 | 
						|
func TestPerformRemoteValidation(t *testing.T) {
 | 
						|
	// Create a new challenge to use for the httpSrv
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, core.NewToken())
 | 
						|
 | 
						|
	// Create an IPv4 test server
 | 
						|
	ms := httpMultiSrv(t, chall.Token, map[string]struct{}{"remote 1": {}, "remote 2": {}})
 | 
						|
	// defer ms.Close()
 | 
						|
 | 
						|
	// Create a local test VA and two 'remote' VAs
 | 
						|
	localVA, _ := setup(ms.Server, 0)
 | 
						|
	localVA.userAgent = "local"
 | 
						|
	remoteVA1, _ := setup(ms.Server, 0)
 | 
						|
	remoteVA1.userAgent = "remote 1"
 | 
						|
	remoteVA2, _ := setup(ms.Server, 0)
 | 
						|
	remoteVA2.userAgent = "remote 2"
 | 
						|
	localVA.remoteVAs = []RemoteVA{
 | 
						|
		{remoteVA1, "remote 1"},
 | 
						|
		{remoteVA2, "remote 2"},
 | 
						|
	}
 | 
						|
 | 
						|
	// Both remotes working, should succeed
 | 
						|
	probCh := make(chan *probs.ProblemDetails, 1)
 | 
						|
	localVA.performRemoteValidation(context.Background(), "localhost", chall, core.Authorization{}, probCh)
 | 
						|
	prob := <-probCh
 | 
						|
	if prob != nil {
 | 
						|
		t.Errorf("performRemoteValidation failed: %s", prob)
 | 
						|
	}
 | 
						|
 | 
						|
	// Only remote 1 working, should fail
 | 
						|
	ms.mu.Lock()
 | 
						|
	delete(ms.allowedUAs, "remote 1")
 | 
						|
	ms.mu.Unlock()
 | 
						|
	mockLog := blog.NewMock()
 | 
						|
	localVA.performRemoteValidation(context.Background(), "localhost", chall, core.Authorization{}, probCh)
 | 
						|
	prob = <-probCh
 | 
						|
	if prob == nil {
 | 
						|
		t.Error("performRemoteValidation didn't fail when one 'remote' validation failed")
 | 
						|
	}
 | 
						|
 | 
						|
	ms.mu.Lock()
 | 
						|
	ms.allowedUAs["local"] = struct{}{}
 | 
						|
	ms.allowedUAs["remote 1"] = struct{}{}
 | 
						|
	ms.allowedUAs["remote 2"] = struct{}{}
 | 
						|
	ms.mu.Unlock()
 | 
						|
 | 
						|
	localVA.remoteVAs = []RemoteVA{
 | 
						|
		{remoteVA1, "remote 1"},
 | 
						|
		{cancelledVA{}, "remote 2"},
 | 
						|
	}
 | 
						|
 | 
						|
	// One remote cancelled, should return no err
 | 
						|
	localVA.performRemoteValidation(context.Background(), "localhost", chall, core.Authorization{}, probCh)
 | 
						|
	prob = <-probCh
 | 
						|
	if prob != nil {
 | 
						|
		t.Errorf("performRemoteValidation returned unexpected err from cancelled context: %s", prob)
 | 
						|
	}
 | 
						|
 | 
						|
	localVA.remoteVAs = []RemoteVA{
 | 
						|
		{cancelledVA{}, "remote 1"},
 | 
						|
		{cancelledVA{}, "remote 2"},
 | 
						|
	}
 | 
						|
 | 
						|
	// Both remotes cancelled, should return no err
 | 
						|
	localVA.performRemoteValidation(context.Background(), "localhost", chall, core.Authorization{}, probCh)
 | 
						|
	prob = <-probCh
 | 
						|
	if prob != nil {
 | 
						|
		t.Errorf("performRemoteValidation returned unexpected err from cancelled context: %s", prob)
 | 
						|
	}
 | 
						|
 | 
						|
	localVA.remoteVAs = []RemoteVA{
 | 
						|
		{remoteVA1, "remote 1"},
 | 
						|
		{remoteVA2, "remote 2"},
 | 
						|
	}
 | 
						|
 | 
						|
	// Both local and remotes working, should succeed
 | 
						|
	_, err := localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
 | 
						|
	if err != nil {
 | 
						|
		t.Errorf("PerformValidation failed: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Only remotes working, should fail
 | 
						|
	ms.mu.Lock()
 | 
						|
	delete(ms.allowedUAs, "local")
 | 
						|
	ms.mu.Unlock()
 | 
						|
	_, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
 | 
						|
	if err == nil {
 | 
						|
		t.Error("PerformValidation didn't fail when local validation failed")
 | 
						|
	}
 | 
						|
 | 
						|
	// Local and remote 2 working, should fail
 | 
						|
	localVA.log = mockLog
 | 
						|
	ms.mu.Lock()
 | 
						|
	ms.allowedUAs["local"] = struct{}{}
 | 
						|
	delete(ms.allowedUAs, "remote 1")
 | 
						|
	ms.mu.Unlock()
 | 
						|
	_, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
 | 
						|
	if err == nil {
 | 
						|
		t.Error("PerformValidation didn't fail when one 'remote' validation failed")
 | 
						|
	}
 | 
						|
	failLogs := mockLog.GetAllMatching(`Validation failed due to remote failures`)
 | 
						|
	if len(failLogs) == 0 {
 | 
						|
		t.Error("Expected log line about failure due to remote failures, didn't get it")
 | 
						|
	}
 | 
						|
	remoteFailMetric := test.CountCounter(localVA.metrics.remoteValidationFailures)
 | 
						|
	if remoteFailMetric != 1 {
 | 
						|
		t.Errorf("Expected remote_validation_failures to be incremented, but it wasn't")
 | 
						|
	}
 | 
						|
 | 
						|
	// Local and remote 2 working with maxRemoteFailures == 1, should succeed
 | 
						|
	localVA, _ = setup(ms.Server, 1)
 | 
						|
	localVA.userAgent = "local"
 | 
						|
	localVA.remoteVAs = []RemoteVA{
 | 
						|
		{remoteVA1, "remote 1"},
 | 
						|
		{remoteVA2, "remote 2"},
 | 
						|
	}
 | 
						|
	_, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
 | 
						|
	if err != nil {
 | 
						|
		t.Errorf("PerformValidation failed when one 'remote' validation failed but maxRemoteFailures is 1: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Only local working, should fail
 | 
						|
	ms.mu.Lock()
 | 
						|
	delete(ms.allowedUAs, "remote 2")
 | 
						|
	ms.mu.Unlock()
 | 
						|
	_, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
 | 
						|
	if err == nil {
 | 
						|
		t.Error("PerformValidation didn't fail when both 'remote' validations failed")
 | 
						|
	}
 | 
						|
 | 
						|
	// Local and remote 1 working, should succeed and return early
 | 
						|
	ms.mu.Lock()
 | 
						|
	ms.allowedUAs["remote 1"] = struct{}{}
 | 
						|
	ms.mu.Unlock()
 | 
						|
	remoteVA2.userAgent = "slow remote"
 | 
						|
	s := time.Now()
 | 
						|
	_, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
 | 
						|
	if err != nil {
 | 
						|
		t.Errorf("PerformValidation failed when one 'remote' validation failed but maxRemoteFailures is 1: %s", err)
 | 
						|
	}
 | 
						|
	took := time.Since(s)
 | 
						|
	if took >= (time.Second * 5) {
 | 
						|
		t.Errorf("PerformValidation didn't return early on success: took %s, expected <5s", took)
 | 
						|
	}
 | 
						|
 | 
						|
	// Only local working, should fail and return early
 | 
						|
	ms.mu.Lock()
 | 
						|
	delete(ms.allowedUAs, "remote 1")
 | 
						|
	ms.mu.Unlock()
 | 
						|
	localVA, _ = setup(ms.Server, 0)
 | 
						|
	localVA.userAgent = "local"
 | 
						|
	localVA.remoteVAs = []RemoteVA{
 | 
						|
		{remoteVA1, "remote 1"},
 | 
						|
		{remoteVA2, "remote 2"},
 | 
						|
	}
 | 
						|
	s = time.Now()
 | 
						|
	_, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
 | 
						|
	if err == nil {
 | 
						|
		t.Error("PerformValidation didn't fail when two validations failed")
 | 
						|
	}
 | 
						|
	took = time.Since(s)
 | 
						|
	if took >= (time.Second * 5) {
 | 
						|
		t.Errorf("PerformValidation didn't return early on failure: took %s, expected <5s", took)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// brokenRemoteVA is a mock for the core.ValidationAuthority interface mocked to
 | 
						|
// always return errors.
 | 
						|
type brokenRemoteVA struct{}
 | 
						|
 | 
						|
// brokenRemoteVAError is the error returned by a brokenRemoteVA's
 | 
						|
// PerformValidation and IsSafeDomain functions.
 | 
						|
var brokenRemoteVAError = errors.New("brokenRemoteVA is broken")
 | 
						|
 | 
						|
// PerformValidation returns brokenRemoteVAError unconditionally
 | 
						|
func (b *brokenRemoteVA) PerformValidation(
 | 
						|
	_ context.Context,
 | 
						|
	_ string,
 | 
						|
	_ core.Challenge,
 | 
						|
	_ core.Authorization) ([]core.ValidationRecord, error) {
 | 
						|
	return nil, brokenRemoteVAError
 | 
						|
}
 | 
						|
 | 
						|
// IsSafeDomain returns brokenRemoteVAError unconditionally
 | 
						|
func (b *brokenRemoteVA) IsSafeDomain(
 | 
						|
	_ context.Context,
 | 
						|
	_ *vaPB.IsSafeDomainRequest) (*vaPB.IsDomainSafe, error) {
 | 
						|
	return nil, brokenRemoteVAError
 | 
						|
}
 | 
						|
 | 
						|
func TestPerformRemoteValidationFailure(t *testing.T) {
 | 
						|
	// Create a new challenge to use for the httpSrv
 | 
						|
	chall := core.HTTPChallenge01()
 | 
						|
	setChallengeToken(&chall, core.NewToken())
 | 
						|
 | 
						|
	// Create an IPv4 test server
 | 
						|
	ms := httpMultiSrv(t, chall.Token, map[string]struct{}{"remote 1": {}, "remote 2": {}})
 | 
						|
	defer ms.Close()
 | 
						|
 | 
						|
	// Create a local test VA configured with a mock logger
 | 
						|
	mockLog := blog.NewMock()
 | 
						|
	localVA, _ := setup(ms.Server, 0)
 | 
						|
	localVA.userAgent = "local"
 | 
						|
	localVA.log = mockLog
 | 
						|
 | 
						|
	// Create a remote test VA
 | 
						|
	remoteVA, _ := setup(ms.Server, 0)
 | 
						|
 | 
						|
	// Create a broken remote test VA
 | 
						|
	brokenVA := &brokenRemoteVA{}
 | 
						|
	brokenVAAddr := "broken"
 | 
						|
 | 
						|
	// Set the local VA to use the two remotes
 | 
						|
	localVA.remoteVAs = []RemoteVA{
 | 
						|
		{remoteVA, "good"},
 | 
						|
		{brokenVA, brokenVAAddr},
 | 
						|
	}
 | 
						|
 | 
						|
	// Performing a validation should return a problem on the channel because of
 | 
						|
	// the broken remote VA.
 | 
						|
	probCh := make(chan *probs.ProblemDetails, 1)
 | 
						|
	localVA.performRemoteValidation(
 | 
						|
		context.Background(),
 | 
						|
		"localhost",
 | 
						|
		chall,
 | 
						|
		core.Authorization{},
 | 
						|
		probCh)
 | 
						|
	prob := <-probCh
 | 
						|
	if prob == nil {
 | 
						|
		t.Fatalf("performRemoteValidation with a broken remote VA did not " +
 | 
						|
			"return a problem")
 | 
						|
	}
 | 
						|
 | 
						|
	// The problem returned should be a server internal problem with the correct
 | 
						|
	// user facing detail message.
 | 
						|
	if prob.Type != "serverInternal" {
 | 
						|
		t.Errorf("performRemoteValidation with a broken remote VA did not " +
 | 
						|
			"return a serverInternal problem")
 | 
						|
	}
 | 
						|
	if prob.Detail != "Remote PerformValidation RPCs failed" {
 | 
						|
		t.Errorf("performRemoteValidation with a broken remote VA did not " +
 | 
						|
			"return a serverInternal problem with the correct detail")
 | 
						|
	}
 | 
						|
 | 
						|
	// The mock logger should have an audit err log line that includes the true
 | 
						|
	// underlying error that caused the server internal problem.
 | 
						|
	expectedLine := fmt.Sprintf(
 | 
						|
		`ERR: \[AUDIT\] Remote VA %q.PerformValidation failed: %s`,
 | 
						|
		brokenVAAddr, brokenRemoteVAError.Error())
 | 
						|
	failLogs := mockLog.GetAllMatching(expectedLine)
 | 
						|
	if len(failLogs) == 0 {
 | 
						|
		t.Error("Expected log line with broken remote VA error message. Found none")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestDetailedError(t *testing.T) {
 | 
						|
	cases := []struct {
 | 
						|
		err      error
 | 
						|
		expected string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			&net.OpError{
 | 
						|
				Op:  "dial",
 | 
						|
				Net: "tcp",
 | 
						|
				Err: &os.SyscallError{
 | 
						|
					Syscall: "getsockopt",
 | 
						|
					Err:     syscall.ECONNREFUSED,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			"Connection refused",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			&net.OpError{
 | 
						|
				Op:  "dial",
 | 
						|
				Net: "tcp",
 | 
						|
				Err: &os.SyscallError{
 | 
						|
					Syscall: "getsockopt",
 | 
						|
					Err:     syscall.ECONNRESET,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			"Connection reset by peer",
 | 
						|
		},
 | 
						|
	}
 | 
						|
	for _, tc := range cases {
 | 
						|
		actual := detailedError(tc.err).Detail
 | 
						|
		if actual != tc.expected {
 | 
						|
			t.Errorf("Wrong detail for %v. Got %q, expected %q", tc.err, actual, tc.expected)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |