boulder/va/va_test.go

902 lines
30 KiB
Go

package va
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
"github.com/jmhodges/clock"
"github.com/miekg/dns"
"github.com/square/go-jose"
"golang.org/x/net/context"
"github.com/letsencrypt/boulder/bdns"
"github.com/letsencrypt/boulder/cdr"
"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/mocks"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test"
)
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()}
var ident = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "localhost"}
var ctx = context.Background()
// 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 pathRedirectPort = "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.HasPrefix(r.Host, "localhost:") && !strings.HasPrefix(r.Host, "other.valid:") {
t.Errorf("Bad Host header: " + r.Host)
}
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, err := getPort(server)
test.AssertNotError(t, err, "failed to get server test port")
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")
http.Redirect(w, r, fmt.Sprintf("http://other.valid/%s", 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, pathRedirectPort) {
t.Logf("HTTPSRV: Got a port redirect req\n")
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 tlssniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
h := sha256.New()
h.Write([]byte(chall.ProvidedKeyAuthorization))
Z := hex.EncodeToString(h.Sum(nil))
ZName := fmt.Sprintf("%s.%s.acme.invalid", Z[:32], Z[32:])
template := &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: []string{ZName},
}
certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
cert := &tls.Certificate{
Certificate: [][]byte{certBytes},
PrivateKey: &TheKey,
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*cert},
ClientAuth: tls.NoClientCert,
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if clientHello.ServerName != ZName {
time.Sleep(time.Second * 10)
return nil, nil
}
return cert, nil
},
NextProtos: []string{"http/1.1"},
}
hs := httptest.NewUnstartedServer(http.DefaultServeMux)
hs.TLS = tlsConfig
hs.StartTLS()
return hs
}
func TestHTTP(t *testing.T) {
chall := core.HTTPChallenge01()
setChallengeToken(&chall, expectedToken)
hs := httpSrv(t, chall.Token)
defer hs.Close()
goodPort, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
// Attempt to fail a challenge by telling the VA to connect to a port we are
// not listening on.
badPort := goodPort + 1
if badPort == 65536 {
badPort = goodPort - 1
}
va, _, log := setup()
va.httpPort = badPort
_, prob := va.validateHTTP01(ctx, ident, chall)
if prob == nil {
t.Fatalf("Server's down; expected refusal. Where did we connect?")
}
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
va.httpPort = goodPort
log.Clear()
t.Logf("Trying to validate: %+v\n", chall)
_, prob = va.validateHTTP01(ctx, ident, 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, ident, 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, ident, 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, ident, 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, ident, 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)
setChallengeToken(&chall, pathWaitLong)
started := time.Now()
_, prob = va.validateHTTP01(ctx, ident, chall)
took := time.Since(started)
// Check that the HTTP connection times out after 5 seconds and doesn't block for 10 seconds
test.Assert(t, (took > (time.Second * 5)), "HTTP timed out before 5 seconds")
test.Assert(t, (took < (time.Second * 10)), "HTTP connection didn't timeout after 5 seconds")
if prob == nil {
t.Fatalf("Connection should've timed out")
}
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
}
func TestHTTPRedirectLookup(t *testing.T) {
chall := core.HTTPChallenge01()
setChallengeToken(&chall, expectedToken)
hs := httpSrv(t, expectedToken)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, log := setup()
va.httpPort = port
setChallengeToken(&chall, pathMoved)
_, prob := va.validateHTTP01(ctx, ident, 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 \[using 127.0.0.1\]: \[127.0.0.1\]`)), 2)
log.Clear()
setChallengeToken(&chall, pathFound)
_, prob = va.validateHTTP01(ctx, ident, 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 \[using 127.0.0.1\]: \[127.0.0.1\]`)), 3)
log.Clear()
setChallengeToken(&chall, pathReLookupInvalid)
_, err = va.validateHTTP01(ctx, ident, chall)
test.AssertError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[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, ident, 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 \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
log.Clear()
setChallengeToken(&chall, pathRedirectPort)
_, err = va.validateHTTP01(ctx, ident, chall)
test.AssertError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/port-redirect" to ".*other.valid:8080/path"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
// 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, ident, chall)
test.AssertNotNil(t, prob, "Problem Details should not be nil")
test.AssertEquals(t, prob.Detail, "Could not connect to other.valid")
}
func TestHTTPRedirectLoop(t *testing.T) {
chall := core.HTTPChallenge01()
setChallengeToken(&chall, "looper")
hs := httpSrv(t, expectedToken)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, _ := setup()
va.httpPort = port
_, prob := va.validateHTTP01(ctx, ident, 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()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, _ := setup()
va.userAgent = rejectUserAgent
va.httpPort = port
setChallengeToken(&chall, pathMoved)
_, prob := va.validateHTTP01(ctx, ident, chall)
if prob == nil {
t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathMoved)
}
setChallengeToken(&chall, pathFound)
_, prob = va.validateHTTP01(ctx, ident, chall)
if prob == nil {
t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathFound)
}
}
func getPort(hs *httptest.Server) (int, error) {
url, err := url.Parse(hs.URL)
if err != nil {
return 0, err
}
_, portString, err := net.SplitHostPort(url.Host)
if err != nil {
return 0, err
}
port, err := strconv.ParseInt(portString, 10, 64)
if err != nil {
return 0, err
}
return int(port), nil
}
func TestTLSSNI(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssniSrv(t, chall)
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, log := setup()
va.tlsPort = port
_, prob := va.validateTLSSNI01(ctx, ident, chall)
if prob != nil {
t.Fatalf("Unexpected failre in validateTLSSNI01: %s", prob)
}
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[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"))
}
log.Clear()
_, prob = va.validateTLSSNI01(ctx, core.AcmeIdentifier{
Type: core.IdentifierType("ip"),
Value: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", port)),
}, chall)
if prob == nil {
t.Fatalf("IdentifierType IP shouldn't have worked.")
}
test.AssertEquals(t, prob.Type, probs.MalformedProblem)
log.Clear()
_, 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)
// Need to create a new authorized keys object to get an unknown SNI (from the signature value)
chall.Token = core.NewToken()
chall.ProvidedKeyAuthorization = "invalid"
log.Clear()
started := time.Now()
_, prob = va.validateTLSSNI01(ctx, ident, chall)
took := time.Since(started)
if prob == nil {
t.Fatalf("Validation should've failed")
}
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
// Check that the TLS connection times out after 5 seconds and doesn't block for 10 seconds
test.Assert(t, (took > (time.Second * 5)), "TLS returned before 5 seconds")
test.Assert(t, (took < (time.Second * 10)), "TLS connection didn't timeout after 5 seconds")
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
// Take down validation server and check that validation fails.
hs.Close()
_, err = va.validateTLSSNI01(ctx, ident, chall)
if err == nil {
t.Fatalf("Server's down; expected refusal. Where did we connect?")
}
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
}
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()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, _ := setup()
va.tlsPort = port
_, prob := va.validateTLSSNI01(ctx, ident, chall)
if prob == nil {
t.Fatalf("TLS validation should have failed: What cert was used?")
}
test.AssertEquals(t, prob.Type, probs.TLSProblem)
}
func TestValidateHTTP(t *testing.T) {
chall := core.HTTPChallenge01()
setChallengeToken(&chall, core.NewToken())
hs := httpSrv(t, chall.Token)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, _ := setup()
va.httpPort = port
_, prob := va.validateChallenge(ctx, ident, chall)
test.Assert(t, prob == nil, "validation failed")
}
// 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 := tlssniSrv(t, chall)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, _ := setup()
va.tlsPort = port
_, prob := va.validateChallenge(ctx, ident, chall)
test.Assert(t, prob == nil, "validation failed")
}
func TestValidateTLSSNINotSane(t *testing.T) {
va, _, _ := setup()
chall := createChallenge(core.ChallengeTypeTLSSNI01)
chall.Token = "not sane"
_, prob := va.validateChallenge(ctx, ident, chall)
test.AssertEquals(t, prob.Type, probs.MalformedProblem)
}
func TestCAATimeout(t *testing.T) {
va, _, _ := setup()
err := va.checkCAA(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "caa-timeout.com"})
if err.Type != probs.ConnectionProblem {
t.Errorf("Expected timeout error type %s, got %s", probs.ConnectionProblem, err.Type)
}
expected := "DNS problem: query timed out looking up CAA for always.timeout"
if err.Detail != expected {
t.Errorf("checkCAA: got %#v, expected %#v", err.Detail, expected)
}
}
func TestCAAChecking(t *testing.T) {
type CAATest struct {
Domain string
Present bool
Valid bool
}
tests := []CAATest{
// Reserved
{"reserved.com", true, false},
// Critical
{"critical.com", true, false},
{"nx.critical.com", true, false},
// Good (absent)
{"absent.com", false, true},
{"example.co.uk", false, true},
// Good (present)
{"present.com", true, true},
{"present.servfail.com", true, true},
// Good (multiple critical, one matching)
{"multi-crit-present.com", true, true},
// Bad (unknown critical)
{"unknown-critical.com", true, false},
{"unknown-critical2.com", true, false},
// Good (unknown noncritical, no issue/issuewild records)
{"unknown-noncritical.com", true, true},
// Good (issue record with unknown parameters)
{"present-with-parameter.com", true, true},
// Bad (unsatisfiable issue record)
{"unsatisfiable.com", true, false},
}
va, _, _ := setup()
for _, caaTest := range tests {
present, valid, err := va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: caaTest.Domain})
if err != nil {
t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err)
}
if present != caaTest.Present {
t.Errorf("checkCAARecords presence mismatch for %s: got %t expected %t", caaTest.Domain, present, caaTest.Present)
}
if valid != caaTest.Valid {
t.Errorf("checkCAARecords validity mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
}
}
present, valid, err := va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.com"})
test.AssertError(t, err, "servfail.com")
test.Assert(t, !present, "Present should be false")
test.Assert(t, !valid, "Valid should be false")
_, _, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.com"})
if err == nil {
t.Errorf("Should have returned error on CAA lookup, but did not: %s", "servfail.com")
}
present, valid, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.present.com"})
test.AssertError(t, err, "servfail.present.com")
test.Assert(t, !present, "Present should be false")
test.Assert(t, !valid, "Valid should be false")
_, _, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.present.com"})
if err == nil {
t.Errorf("Should have returned error on CAA lookup, but did not: %s", "servfail.present.com")
}
}
func TestPerformValidationInvalid(t *testing.T) {
va, stats, _ := setup()
chalDNS := createChallenge(core.ChallengeTypeDNS01)
_, prob := va.PerformValidation(context.Background(), "foo.com", chalDNS, core.Authorization{})
test.Assert(t, prob != nil, "validation succeeded")
test.AssertEquals(t, stats.TimingDurationCalls[0].Metric, "VA.Validations.dns-01.invalid")
}
func TestPerformValidationValid(t *testing.T) {
va, stats, _ := setup()
// 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))
test.AssertEquals(t, stats.TimingDurationCalls[0].Metric, "VA.Validations.dns-01.valid")
}
func TestDNSValidationFailure(t *testing.T) {
va, _, _ := setup()
chalDNS := createChallenge(core.ChallengeTypeDNS01)
_, prob := va.validateChallenge(ctx, ident, 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()
_, prob := va.validateChallenge(ctx, notDNS, chalDNS)
test.AssertEquals(t, prob.Type, probs.MalformedProblem)
}
func TestDNSValidationNotSane(t *testing.T) {
va, _, _ := setup()
chal0 := core.DNSChallenge01()
chal0.Token = ""
chal1 := core.DNSChallenge01()
chal1.Token = "yfCBb-bRTLz8Wd1C0lTUQK3qlKj3-t2tYGwx5Hj7r_"
chal2 := core.DNSChallenge01()
chal2.ProvidedKeyAuthorization = ""
var authz = core.Authorization{
ID: core.NewToken(),
RegistrationID: 1,
Identifier: ident,
Challenges: []core.Challenge{chal0, chal1, chal2},
}
for i := 0; i < len(authz.Challenges); i++ {
_, prob := va.validateChallenge(ctx, ident, 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 sanity check.") {
t.Errorf("Got wrong error: %s", prob.Error())
}
}
}
func TestDNSValidationServFail(t *testing.T) {
va, _, _ := setup()
chalDNS := createChallenge(core.ChallengeTypeDNS01)
badIdent := core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: "servfail.com",
}
_, prob := va.validateChallenge(ctx, badIdent, chalDNS)
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
}
func TestDNSValidationNoServer(t *testing.T) {
va, _, _ := setup()
va.dnsResolver = bdns.NewTestDNSResolverImpl(
time.Second*5,
nil,
metrics.NewNoopScope(),
clock.Default(),
1)
chalDNS := createChallenge(core.ChallengeTypeDNS01)
_, prob := va.validateChallenge(ctx, ident, chalDNS)
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
}
func TestDNSValidationOK(t *testing.T) {
va, _, _ := setup()
// create a challenge with well known token
chalDNS := core.DNSChallenge01()
chalDNS.Token = expectedToken
chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
goodIdent := core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: "good-dns01.com",
}
_, prob := va.validateChallenge(ctx, goodIdent, chalDNS)
test.Assert(t, prob == nil, "Should be valid.")
}
func TestDNSValidationNoAuthorityOK(t *testing.T) {
va, _, _ := setup()
// create a challenge with well known token
chalDNS := core.DNSChallenge01()
chalDNS.Token = expectedToken
chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
goodIdent := core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: "no-authority-dns01.com",
}
_, prob := va.validateChallenge(ctx, goodIdent, chalDNS)
test.Assert(t, prob == nil, "Should be valid.")
}
func TestCAAFailure(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssniSrv(t, chall)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, _ := setup()
va.tlsPort = port
ident.Value = "reserved.com"
_, prob := va.validateChallengeAndCAA(ctx, ident, chall)
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
}
func TestLimitedReader(t *testing.T) {
chall := core.HTTPChallenge01()
setChallengeToken(&chall, core.NewToken())
ident.Value = "localhost"
hs := httpSrv(t, "01234567890123456789012345678901234567890123456789012345678901234567890123456789")
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, _ := setup()
va.httpPort = port
_, prob := va.validateChallenge(ctx, ident, chall)
test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
test.Assert(t, strings.HasPrefix(prob.Detail, "Invalid response from "),
"Expected failure due to truncation")
}
func setup() (*ValidationAuthorityImpl, *mocks.Statter, *blog.Mock) {
stats := mocks.NewStatter()
logger := blog.NewMock()
va := NewValidationAuthorityImpl(
&cmd.PortConfig{},
nil,
nil,
nil,
&bdns.MockDNSResolver{},
"user agent 1.0",
"letsencrypt.org",
stats,
clock.Default(),
logger)
return va, stats, logger
}
func TestCheckCAAFallback(t *testing.T) {
testSrv := httptest.NewServer(http.HandlerFunc(mocks.GPDNSHandler))
defer testSrv.Close()
stats := mocks.NewStatter()
logger := blog.NewMock()
caaDR, err := cdr.New(metrics.NewNoopScope(), time.Second, 1, nil, blog.NewMock())
test.AssertNotError(t, err, "Failed to create CAADistributedResolver")
caaDR.URI = testSrv.URL
caaDR.Clients["1.1.1.1"] = new(http.Client)
va := NewValidationAuthorityImpl(
&cmd.PortConfig{},
nil,
nil,
caaDR,
&bdns.MockDNSResolver{},
"user agent 1.0",
"ca.com",
stats,
clock.Default(),
logger)
prob := va.checkCAA(ctx, core.AcmeIdentifier{Value: "bad-local-resolver.com", Type: "dns"})
test.Assert(t, prob == nil, fmt.Sprintf("returned ProblemDetails was non-nil: %#v", prob))
va.caaDR = nil
prob = va.checkCAA(ctx, core.AcmeIdentifier{Value: "bad-local-resolver.com", Type: "dns"})
test.Assert(t, prob != nil, "returned ProblemDetails was nil")
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
test.AssertEquals(t, prob.Detail, "server failure at resolver")
}
func TestParseResults(t *testing.T) {
r := []caaResult{}
s, err := parseResults(r)
test.Assert(t, s == nil, "set is not nil")
test.Assert(t, err == nil, "error is not nil")
test.AssertNotError(t, err, "no error should be returned")
r = []caaResult{{nil, errors.New("")}, {[]*dns.CAA{&dns.CAA{Value: "test"}}, nil}}
s, err = parseResults(r)
test.Assert(t, s == nil, "set is not nil")
test.AssertEquals(t, err.Error(), "")
expected := dns.CAA{Value: "other-test"}
r = []caaResult{{[]*dns.CAA{&expected}, nil}, {[]*dns.CAA{&dns.CAA{Value: "test"}}, nil}}
s, err = parseResults(r)
test.AssertEquals(t, len(s.Unknown), 1)
test.Assert(t, s.Unknown[0] == &expected, "Incorrect record returned")
test.AssertNotError(t, err, "no error should be returned")
}